diff --git a/provider/collection.go b/provider/collection.go index f202c82..0120342 100644 --- a/provider/collection.go +++ b/provider/collection.go @@ -8,18 +8,20 @@ import ( ) type ResourceCollection struct { - typ string - kind ResourceKind - block *configschema.Block + typ string + kind ResourceKind + block *configschema.Block + parent *Resource *starlark.List } -func NewResourceCollection(typ string, k ResourceKind, block *configschema.Block) *ResourceCollection { +func NewResourceCollection(typ string, k ResourceKind, block *configschema.Block, parent *Resource) *ResourceCollection { return &ResourceCollection{ - typ: typ, - kind: k, - block: block, - List: starlark.NewList(nil), + typ: typ, + kind: k, + block: block, + parent: parent, + List: starlark.NewList(nil), } } @@ -67,7 +69,7 @@ func (c *ResourceCollection) CallInternal(thread *starlark.Thread, args starlark } } - resource := MakeResource(c.typ, c.kind, c.block) + resource := MakeResource(c.typ, c.kind, c.block, c.parent) if len(kwargs) != 0 { if err := resource.loadKeywordArgs(kwargs); err != nil { return nil, err diff --git a/provider/computed.go b/provider/computed.go index a3bb735..6fa3032 100644 --- a/provider/computed.go +++ b/provider/computed.go @@ -3,7 +3,8 @@ package provider import ( "fmt" - "github.com/hashicorp/terraform/configs/configschema" + "github.com/zclconf/go-cty/cty" + "go.starlark.net/starlark" ) @@ -11,21 +12,98 @@ type sString = starlark.String type Computed struct { r *Resource - a *configschema.Attribute + t cty.Type name string + path string + sString } -func NewComputed(r *Resource, a *configschema.Attribute, name string) *Computed { - hash, _ := r.Hash() +func NewComputed(r *Resource, t cty.Type, name string) *Computed { + var parts []string + var path string + + child := r + for { + if child.parent == nil { + hash, _ := r.Hash() + path = fmt.Sprintf("%s.%s.%d", child.kind, child.typ, hash) + break + } + + parts = append(parts, child.typ) + child = child.parent + } + + for i := len(parts) - 1; i >= 0; i-- { + path += "." + parts[i] + } + + return NewComputedWithPath(r, t, name, path+"."+name) +} + +func NewComputedWithPath(r *Resource, t cty.Type, name, path string) *Computed { return &Computed{ r: r, - a: a, + t: t, name: name, - sString: starlark.String(fmt.Sprintf("${%s.%s.%d.%s}", r.kind, r.typ, hash, name)), + path: path, + sString: starlark.String(fmt.Sprintf("${%s}", path)), } } func (*Computed) Type() string { return "computed" } + +func (c *Computed) InnerType() *Type { + t, _ := NewTypeFromCty(c.t) + return t +} + +func (c *Computed) Attr(name string) (starlark.Value, error) { + if !c.t.IsObjectType() { + return nil, nil + } + + if !c.t.HasAttribute(name) { + return nil, nil + } + + path := fmt.Sprintf("%s.%s", c.path, name) + return NewComputedWithPath(c.r, c.t.AttributeType(name), name, path), nil +} + +func (c *Computed) AttrNames() []string { + return nil +} + +func (c *Computed) doNested(name, path string, t cty.Type, index int) *Computed { + return &Computed{ + r: c.r, + t: t, + name: c.name, + } + +} +func (c *Computed) Index(i int) starlark.Value { + path := fmt.Sprintf("%s.%d", c.path, i) + + if c.t.IsSetType() { + return NewComputedWithPath(c.r, *c.t.SetElementType(), c.name, path) + } + + if c.t.IsListType() { + return NewComputedWithPath(c.r, *c.t.ListElementType(), c.name, path) + } + + return starlark.None +} + +func (c *Computed) Len() int { + if !c.t.IsSetType() && !c.t.IsListType() { + return 0 + } + + return 1024 +} diff --git a/provider/computed_test.go b/provider/computed_test.go new file mode 100644 index 0000000..5a598d6 --- /dev/null +++ b/provider/computed_test.go @@ -0,0 +1,7 @@ +package provider + +import "testing" + +func TestComputed(t *testing.T) { + test(t, "testdata/computed.star") +} diff --git a/provider/provider.go b/provider/provider.go index 688250b..f2cb349 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -111,7 +111,7 @@ func (m *MapSchema) Attr(name string) (starlark.Value, error) { } if schema, ok := m.schemas[name]; ok { - m.collections[name] = NewResourceCollection(name, m.kind, schema.Block) + m.collections[name] = NewResourceCollection(name, m.kind, schema.Block, nil) return m.collections[name], nil } diff --git a/provider/provider_test.go b/provider/provider_test.go index 9905a99..ed91964 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -31,10 +31,6 @@ func TestResource(t *testing.T) { test(t, "testdata/resource.star") } -func TestResourceOutput(t *testing.T) { - test(t, "testdata/resource-output.star") -} - func TestHCL(t *testing.T) { test(t, "testdata/hcl.star") } diff --git a/provider/resource.go b/provider/resource.go index 1fe3f5f..cc20308 100644 --- a/provider/resource.go +++ b/provider/resource.go @@ -8,8 +8,6 @@ import ( "go.starlark.net/syntax" ) -type fnSignature func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) - type ResourceKind string const ( @@ -22,14 +20,16 @@ type Resource struct { typ string kind ResourceKind block *configschema.Block + parent *Resource values map[string]*Value } -func MakeResource(typ string, k ResourceKind, b *configschema.Block) *Resource { +func MakeResource(typ string, k ResourceKind, b *configschema.Block, parent *Resource) *Resource { return &Resource{ typ: typ, kind: k, block: b, + parent: parent, values: make(map[string]*Value), } } @@ -100,37 +100,41 @@ func (r *Resource) Attr(name string) (starlark.Value, error) { return r.toDict(), nil } - if a, ok := r.block.Attributes[name]; (ok && a.Computed) || name == "id" { - return r.attrComputed(name, a) + if a, ok := r.block.Attributes[name]; ok { + return r.attrValue(name, a) } if b, ok := r.block.BlockTypes[name]; ok { return r.attrBlock(name, b) } - if v, ok := r.values[name]; ok { - return v.Value(), nil - } - return nil, nil } -func (r *Resource) attrComputed(name string, attr *configschema.Attribute) (starlark.Value, error) { - return NewComputed(r, attr, name), nil -} - func (r *Resource) attrBlock(name string, b *configschema.NestedBlock) (starlark.Value, error) { if b.MaxItems != 1 { if _, ok := r.values[name]; !ok { - r.values[name] = MustValue(NewResourceCollection(name, NestedK, &b.Block)) + r.values[name] = MustValue(NewResourceCollection(name, NestedK, &b.Block, r)) + } + } else { + if _, ok := r.values[name]; !ok { + r.values[name] = MustValue(MakeResource(name, NestedK, &b.Block, r)) } } - if _, ok := r.values[name]; !ok { - r.values[name] = MustValue(MakeResource(name, NestedK, &b.Block)) + return r.values[name].Value(), nil +} + +func (r *Resource) attrValue(name string, attr *configschema.Attribute) (starlark.Value, error) { + if attr.Computed { + return NewComputed(r, attr.Type, name), nil } - return r.values[name].Value(), nil + if v, ok := r.values[name]; ok { + return v.Value(), nil + } + + return starlark.None, nil } // AttrNames honors the starlark.HasAttrs interface. @@ -163,6 +167,10 @@ func (r *Resource) SetField(name string, v starlark.Value) error { return starlark.NoSuchAttrError(errmsg) } + if attr.Computed && !attr.Optional { + return fmt.Errorf("%s: can't set computed %s attribute", r.typ, name) + } + if err := MustTypeFromCty(attr.Type).Validate(v); err != nil { return err } @@ -181,10 +189,6 @@ func (r *Resource) setFieldFromNestedBlock(name string, b *configschema.NestedBl return fmt.Errorf("expected dict or list, got %s", v.Type()) } -func (r *Resource) localName() string { - return fmt.Sprintf("") -} - func (r *Resource) toDict() *starlark.Dict { d := starlark.NewDict(len(r.values)) for k, v := range r.values { @@ -229,7 +233,6 @@ func (x *Resource) doCompareSameType(y *Resource, depth int) (bool, error) { } for key, xval := range x.values { - fmt.Printf("%s = %T, %T\n", key, xval.Value(), y.values[key].Value()) yval, found := y.values[key] if !found { return false, nil diff --git a/provider/testdata/computed.star b/provider/testdata/computed.star new file mode 100644 index 0000000..433ebb2 --- /dev/null +++ b/provider/testdata/computed.star @@ -0,0 +1,24 @@ +load("assert.star", "assert") + +aws = provider("aws", "2.13.0") + +# compute of scalar +web = aws.resource.instance() +web.ami = aws.data.ami().id +assert.eq(type(web.ami), "computed") +assert.eq(str(web.ami), '"${data.aws_ami.8731.id}"') + +# compute of set +table = aws.data.dynamodb_table() +assert.eq(str(table.ttl), '"${data.aws_dynamodb_table.8731.ttl}"') +assert.eq(str(table.ttl[0]), '"${data.aws_dynamodb_table.8731.ttl.0}"') +assert.eq(str(table.ttl[0].attribute_name), '"${data.aws_dynamodb_table.8731.ttl.0.attribute_name}"') + +# compute of list +instance = aws.data.instance() +assert.eq(str(instance.credit_specification), '"${data.aws_instance.8731.credit_specification}"') +assert.eq(str(instance.credit_specification[0]), '"${data.aws_instance.8731.credit_specification.0}"') +assert.eq(str(instance.credit_specification[0].cpu_credits), '"${data.aws_instance.8731.credit_specification.0.cpu_credits}"') + +# compute of map +assert.eq(str(aws.resource.instance().root_block_device.volume_size), '"${resource.aws_instance.8731.root_block_device.volume_size}"') \ No newline at end of file diff --git a/provider/testdata/hcl.star b/provider/testdata/hcl.star index 4ecdc93..5ea96b4 100644 --- a/provider/testdata/hcl.star +++ b/provider/testdata/hcl.star @@ -34,4 +34,4 @@ group.mixed_instances_policy = { ami2 = aws.data.ami() ami2.most_recent = True -print(hcl(aws)) +#print(hcl(aws)) diff --git a/provider/testdata/resource-output.star b/provider/testdata/resource-output.star deleted file mode 100644 index e2312a1..0000000 --- a/provider/testdata/resource-output.star +++ /dev/null @@ -1,12 +0,0 @@ -load("assert.star", "assert") - -aws = provider("aws", "2.13.0") - -ami = aws.data.ami() -assert.eq(type(ami.architecture), "computed") -assert.eq(str(ami.architecture), '"${data.aws_ami.8731.architecture}"') - -web = aws.resource.instance() -web.ami = ami.id -assert.eq(type(web.ami), "computed") -assert.eq(str(web.ami), '"${data.aws_ami.8731.id}"') \ No newline at end of file diff --git a/provider/testdata/resource.star b/provider/testdata/resource.star index e781ed3..3372c7e 100644 --- a/provider/testdata/resource.star +++ b/provider/testdata/resource.star @@ -2,6 +2,32 @@ load("assert.star", "assert") p = provider("ignition", "1.1.0") +# attr +qux = p.data.user() +qux.uid = 42 +assert.eq(qux.uid, 42) + +# attr not-set +assert.eq(qux.name, None) + +# attr not-exists +assert.fails(lambda: qux.foo, "data has no .foo field or method") + +# attr id +assert.eq(type(qux.id), "computed") +assert.eq(str(qux.id), '"${data.ignition_user.3399129522.id}"') + +# attr output assignation +aws = provider("aws", "2.13.0") +def invalidOutput(): aws.data.instance().public_dns = "foo" +assert.fails(invalidOutput, "aws_instance: can't set computed public_dns attribute") + +# attr output in asignation +web = aws.resource.instance() +web.ami = web.id +def invalidType(): web.get_password_data = web.id +assert.fails(invalidType, "expected bool, got string") + # comparasion simple values assert.eq(p.data.disk(), p.data.disk()) assert.ne(p.data.disk(device="foo"), p.data.disk()) @@ -60,8 +86,6 @@ home.label = "home" home.start = root.size + root.start home.size = 4 * 1024 * 1024 -print(dict({"foo": disk})) - assert.eq(disk.__dict__, { "partition": [{ "label": "root", diff --git a/provider/type.go b/provider/type.go index 3eff125..afd7a52 100644 --- a/provider/type.go +++ b/provider/type.go @@ -226,9 +226,12 @@ func (t *Type) Validate(v starlark.Value) error { return nil } case *Computed: - if t.cty == v.(*Computed).a.Type { + if t.cty == v.(*Computed).t { return nil } + + vt := v.(*Computed).InnerType().Starlark() + return fmt.Errorf("expected %s, got %s", t.typ, vt) case *starlark.List: if t.cty.IsListType() || t.cty.IsSetType() { return t.validateListType(v.(*starlark.List), t.cty.ElementType())