From 4a1a60b57a55e3efcfafeb78cf8415f432eb5fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Fri, 10 Apr 2020 00:00:11 +0200 Subject: [PATCH] starlark/types: validate function and CallStack reccord --- starlark/types/backend.go | 10 +- starlark/types/collection.go | 29 +++- starlark/types/provider.go | 6 +- starlark/types/provider_test.go | 1 + starlark/types/provisioner.go | 6 +- starlark/types/resource.go | 32 +++- starlark/types/testdata/validate.star | 29 ++++ starlark/types/validate.go | 212 ++++++++++++++++++++++++++ starlark/types/validate_test.go | 9 ++ 9 files changed, 312 insertions(+), 22 deletions(-) create mode 100644 starlark/types/testdata/validate.star create mode 100644 starlark/types/validate.go create mode 100644 starlark/types/validate_test.go diff --git a/starlark/types/backend.go b/starlark/types/backend.go index b2c790e..b4716e5 100644 --- a/starlark/types/backend.go +++ b/starlark/types/backend.go @@ -53,7 +53,7 @@ func MakeBackend( } pm := t.Local(PluginManagerLocal).(*terraform.PluginManager) - p, err := NewBackend(pm, name.GoString()) + p, err := NewBackend(pm, name.GoString(), t.CallStack()) if err != nil { return nil, err } @@ -101,7 +101,7 @@ var _ starlark.HasAttrs = &Backend{} var _ starlark.Comparable = &Backend{} // NewBackend returns a new Backend instance based on given arguments, -func NewBackend(pm *terraform.PluginManager, typ string) (*Backend, error) { +func NewBackend(pm *terraform.PluginManager, typ string, cs starlark.CallStack) (*Backend, error) { fn := binit.Backend(typ) if fn == nil { return nil, fmt.Errorf("unable to find backend %q", typ) @@ -112,7 +112,7 @@ func NewBackend(pm *terraform.PluginManager, typ string) (*Backend, error) { return &Backend{ pm: pm, b: b, - Resource: NewResource("", typ, BackendKind, b.ConfigSchema(), nil, nil), + Resource: NewResource("", typ, BackendKind, b.ConfigSchema(), nil, nil, cs), }, nil } @@ -261,7 +261,7 @@ func (s *State) initialize(state *states.State, mod *states.Module) error { addrs := state.ProviderAddrs() for _, addr := range addrs { typ := addr.ProviderConfig.Type.Type - p, err := NewProvider(s.pm, typ, "", addr.ProviderConfig.Alias) + p, err := NewProvider(s.pm, typ, "", addr.ProviderConfig.Alias, nil) if err != nil { return err } @@ -297,7 +297,7 @@ func (s *State) initializeResource(p *Provider, r *states.Resource) error { multi := r.EachMode != states.NoEach for _, instance := range r.Instances { - r := NewResource(name, typ, ResourceKind, schema.Block, p, p.Resource) + r := NewResource(name, typ, ResourceKind, schema.Block, p, p.Resource, nil) var val interface{} if err := json.Unmarshal(instance.Current.AttrsJSON, &val); err != nil { diff --git a/starlark/types/collection.go b/starlark/types/collection.go index f582344..e0dd060 100644 --- a/starlark/types/collection.go +++ b/starlark/types/collection.go @@ -55,11 +55,13 @@ import ( // Value to match in the given key. // type ResourceCollection struct { - typ string - kind Kind - block *configschema.Block - provider *Provider - parent *Resource + typ string + kind Kind + block *configschema.Block + nestedblock *configschema.NestedBlock + provider *Provider + parent *Resource + *starlark.List } @@ -82,6 +84,21 @@ func NewResourceCollection( } } +// NewNestedResourceCollection returns +func NewNestedResourceCollection( + typ string, block *configschema.NestedBlock, provider *Provider, parent *Resource, +) *ResourceCollection { + return &ResourceCollection{ + typ: typ, + kind: NestedKind, + block: &block.Block, + nestedblock: block, + provider: provider, + parent: parent, + List: starlark.NewList(nil), + } +} + // LoadList loads a list of dicts on the collection. It clears the collection. func (c *ResourceCollection) LoadList(l *starlark.List) error { if err := c.List.Clear(); err != nil { @@ -94,7 +111,7 @@ func (c *ResourceCollection) LoadList(l *starlark.List) error { return fmt.Errorf("%d: expected dict, got %s", i, l.Index(i).Type()) } - r := NewResource("", c.typ, c.kind, c.block, c.provider, c.parent) + r := NewResource("", c.typ, c.kind, c.block, c.provider, c.parent, nil) if dict != nil && dict.Len() != 0 { if err := r.loadDict(dict); err != nil { return err diff --git a/starlark/types/provider.go b/starlark/types/provider.go index 528cfff..c64f78d 100644 --- a/starlark/types/provider.go +++ b/starlark/types/provider.go @@ -50,7 +50,7 @@ func MakeProvider( } pm := t.Local(PluginManagerLocal).(*terraform.PluginManager) - p, err := NewProvider(pm, name.GoString(), version.GoString(), alias.GoString()) + p, err := NewProvider(pm, name.GoString(), version.GoString(), alias.GoString(), t.CallStack()) if err != nil { return nil, err } @@ -112,7 +112,7 @@ var _ starlark.HasAttrs = &Provider{} var _ starlark.Comparable = &Provider{} // NewProvider returns a new Provider instance from a given type, version and name. -func NewProvider(pm *terraform.PluginManager, typ, version, name string) (*Provider, error) { +func NewProvider(pm *terraform.PluginManager, typ, version, name string, cs starlark.CallStack) (*Provider, error) { cli, meta, err := pm.Provider(typ, version, false) if err != nil { return nil, err @@ -141,7 +141,7 @@ func NewProvider(pm *terraform.PluginManager, typ, version, name string) (*Provi meta: meta, } - p.Resource = NewResource(name, typ, ProviderKind, response.Provider.Block, p, nil) + p.Resource = NewResource(name, typ, ProviderKind, response.Provider.Block, p, nil, cs) p.dataSources = NewResourceCollectionGroup(p, DataSourceKind, response.DataSources) p.resources = NewResourceCollectionGroup(p, ResourceKind, response.ResourceTypes) diff --git a/starlark/types/provider_test.go b/starlark/types/provider_test.go index fbd4bfa..1e73e6e 100644 --- a/starlark/types/provider_test.go +++ b/starlark/types/provider_test.go @@ -81,6 +81,7 @@ func doTestPrint(t *testing.T, filename string, print func(*starlark.Thread, str "hcl": BuiltinHCL(), "fn": BuiltinFunctionAttribute(), "evaluate": BuiltinEvaluate(), + "validate": BuiltinValidate(), "tf": NewTerraform(pm), } diff --git a/starlark/types/provisioner.go b/starlark/types/provisioner.go index cf4469c..9f44ac8 100644 --- a/starlark/types/provisioner.go +++ b/starlark/types/provisioner.go @@ -44,7 +44,7 @@ func MakeProvisioner( return nil, fmt.Errorf("unexpected positional arguments count") } - p, err := NewProvisioner(pm, name.GoString()) + p, err := NewProvisioner(pm, name.GoString(), t.CallStack()) if err != nil { return nil, err } @@ -79,7 +79,7 @@ type Provisioner struct { } // NewProvisioner returns a new Provisioner for the given type. -func NewProvisioner(pm *terraform.PluginManager, typ string) (*Provisioner, error) { +func NewProvisioner(pm *terraform.PluginManager, typ string, cs starlark.CallStack) (*Provisioner, error) { cli, meta, err := pm.Provisioner(typ) if err != nil { return nil, err @@ -103,7 +103,7 @@ func NewProvisioner(pm *terraform.PluginManager, typ string) (*Provisioner, erro provisioner: provisioner, meta: meta, - Resource: NewResource(NameGenerator(), typ, ProvisionerKind, response.Provisioner, nil, nil), + Resource: NewResource(NameGenerator(), typ, ProvisionerKind, response.Provisioner, nil, nil, cs), }, nil } diff --git a/starlark/types/resource.go b/starlark/types/resource.go index 2443152..eb97830 100644 --- a/starlark/types/resource.go +++ b/starlark/types/resource.go @@ -20,7 +20,7 @@ var NameGenerator = func() string { return fmt.Sprintf("id_%s", ulid.MustNew(ulid.Timestamp(t), entropy)) } -// Kind describes what kind of resource is represented by a Resource isntance. +// Kind describes what kind of resource is represented by a Resource instance. type Kind string // IsNamed returns true if this kind of resources contains a name. @@ -65,7 +65,7 @@ func MakeResource( name = NameGenerator() } - r := NewResource(name, c.typ, c.kind, c.block, c.provider, c.parent) + r := NewResource(name, c.typ, c.kind, c.block, c.provider, c.parent, t.CallStack()) if dict != nil && dict.Len() != 0 { if err := r.loadDict(dict); err != nil { return nil, err @@ -208,6 +208,8 @@ type Resource struct { parent *Resource dependencies []*Resource provisioners []*Provisioner + + cs starlark.CallStack } var _ starlark.Value = &Resource{} @@ -217,7 +219,11 @@ var _ starlark.Comparable = &Resource{} // NewResource returns a new resource of the given kind, type based on the // given configschema.Block. -func NewResource(name, typ string, k Kind, b *configschema.Block, provider *Provider, parent *Resource) *Resource { +func NewResource( + name, typ string, k Kind, + b *configschema.Block, provider *Provider, parent *Resource, + cs starlark.CallStack, +) *Resource { return &Resource{ name: name, typ: typ, @@ -226,6 +232,7 @@ func NewResource(name, typ string, k Kind, b *configschema.Block, provider *Prov values: NewValues(), provider: provider, parent: parent, + cs: cs, } } @@ -337,11 +344,14 @@ func (r *Resource) attrBlock(name string, b *configschema.NestedBlock) (starlark return v.Starlark(), nil } + var output starlark.Value if b.MaxItems != 1 { - return r.values.Set(name, MustValue(NewResourceCollection(name, NestedKind, &b.Block, r.provider, r))).Starlark(), nil + output = NewNestedResourceCollection(name, b, r.provider, r) + } else { + output = NewResource("", name, NestedKind, &b.Block, r.provider, r, nil) } - return r.values.Set(name, MustValue(NewResource("", name, NestedKind, &b.Block, r.provider, r))).Starlark(), nil + return r.values.Set(name, MustValue(output)).Starlark(), nil } func (r *Resource) attrValue(name string, attr *configschema.Attribute) (starlark.Value, error) { @@ -559,3 +569,15 @@ func (r *Resource) doCompareSameType(y *Resource, depth int) (bool, error) { return true, nil } + +func (r *Resource) CallStack() starlark.CallStack { + if r.cs != nil { + return r.cs + } + + if r.parent != nil { + return r.parent.CallStack() + } + + return nil +} diff --git a/starlark/types/testdata/validate.star b/starlark/types/testdata/validate.star new file mode 100644 index 0000000..ac1467f --- /dev/null +++ b/starlark/types/testdata/validate.star @@ -0,0 +1,29 @@ +load("assert.star", "assert") + +helm = tf.provider("helm", "1.0.0", "default") +helm.kubernetes.token = "foo" + +# require scalar arguments +helm.resource.release() +errors = validate(helm) +assert.eq(len(errors), 2) +assert.eq(errors[0].pos, "testdata/validate.star:7:22") +assert.eq(errors[1].pos, "testdata/validate.star:7:22") + +# require list arguments +google = tf.provider("google") +r = google.resource.organization_iam_custom_role(role_id="foo", org_id="bar", title="qux") +r.permissions = ["foo"] +assert.eq(len(validate(google)), 0) + +r.permissions.pop() +assert.eq(len(validate(google)), 1) + +# require blocks +google = tf.provider("google") +r = google.resource.compute_global_forwarding_rule(target="foo", name="bar") +r.metadata_filters() +assert.eq(len(validate(google)), 2) + +errors = validate(google) +for e in errors: print(e.pos, e.msg) \ No newline at end of file diff --git a/starlark/types/validate.go b/starlark/types/validate.go new file mode 100644 index 0000000..e759318 --- /dev/null +++ b/starlark/types/validate.go @@ -0,0 +1,212 @@ +package types + +import ( + "fmt" + "sort" + + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" +) + +// ValidationError is an error returned by Validabler.Validate. +type ValidationError struct { + // Msg reason of the error + Msg string + // CallStack of the instantiation of the value being validated. + CallStack starlark.CallStack +} + +// NewValidationError returns a new ValidationError. +func NewValidationError(cs starlark.CallStack, format string, args ...interface{}) *ValidationError { + return &ValidationError{ + Msg: fmt.Sprintf(format, args...), + CallStack: cs, + } +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("%s: %s", e.CallStack.At(1).Pos, e.Msg) +} + +// Value returns the error as a starlark.Value. +func (e *ValidationError) Value() starlark.Value { + values := []starlark.Tuple{ + {starlark.String("msg"), starlark.String(e.Msg)}, + {starlark.String("pos"), starlark.String(e.CallStack.At(1).Pos.String())}, + } + + return starlarkstruct.FromKeywords(starlarkstruct.Default, values) +} + +// ValidationErrors represents a list of ValidationErrors. +type ValidationErrors []*ValidationError + +// Value returns the errors as a starlark.Value. +func (e ValidationErrors) Value() starlark.Value { + values := make([]starlark.Value, len(e)) + for i, err := range e { + values[i] = err.Value() + } + + return starlark.NewList(values) +} + +// Validabler defines if the resource is validable. +type Validabler interface { + Validate() ValidationErrors +} + +// BuiltinValidate returns a starlak.Builtin function to validate objects +// implementing the Validabler interface. +// +// outline: types +// functions: +// validate(resource) list +// Returns a list with validating errors if any. A validating error is +// a struct with two fields: `msg` and `pos` +// params: +// resource +// resource to be validated. +// +func BuiltinValidate() starlark.Value { + return starlark.NewBuiltin("validate", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { + if args.Len() != 1 { + return nil, fmt.Errorf("exactly one argument is required") + } + + value := args.Index(0) + v, ok := value.(Validabler) + if !ok { + return nil, fmt.Errorf("value type %s doesn't support validation", value.Type()) + } + + errors := v.Validate() + return errors.Value(), nil + }) +} + +// Validate honors the Vadiabler interface. +func (t *Terraform) Validate() (errs ValidationErrors) { + if t.b != nil { + errs = append(errs, t.b.Validate()...) + } + + errs = append(errs, t.b.Validate()...) + return +} + +// Validate honors the Vadiabler interface. +func (d *Dict) Validate() (errs ValidationErrors) { + for _, v := range d.Keys() { + p, _, _ := d.Get(v) + t, ok := p.(Validabler) + if !ok { + continue + } + + errs = append(errs, t.Validate()...) + } + + return +} + +// Validate honors the Vadiabler interface. +func (p *Provider) Validate() (errs ValidationErrors) { + errs = append(errs, p.Resource.Validate()...) + errs = append(errs, p.dataSources.Validate()...) + errs = append(errs, p.resources.Validate()...) + + return +} + +// Validate honors the Vadiabler interface. +func (g *ResourceCollectionGroup) Validate() (errs ValidationErrors) { + names := make(sort.StringSlice, len(g.collections)) + var i int + for name := range g.collections { + names[i] = name + i++ + } + + sort.Sort(names) + for _, name := range names { + errs = append(errs, g.collections[name].Validate()...) + } + + return +} + +// Validate honors the Vadiabler interface. +func (c *ResourceCollection) Validate() (errs ValidationErrors) { + if c.nestedblock != nil { + l := c.Len() + max, min := c.nestedblock.MaxItems, c.nestedblock.MinItems + if max != 0 && l > max { + errs = append(errs, NewValidationError(c.parent.CallStack(), + "%s: max. length is %d, current len %d", c, max, l, + )) + } + + if l < min { + errs = append(errs, NewValidationError(c.parent.CallStack(), + "%s: min. length is %d, current len %d", c, min, l, + )) + } + } + + for i := 0; i < c.Len(); i++ { + errs = append(errs, c.Index(i).(*Resource).Validate()...) + } + + return +} + +// Validate honors the Vadiabler interface. +func (r *Resource) Validate() ValidationErrors { + return append( + r.doValidateAttributes(), + r.doValidateBlocks()..., + ) +} + +func (r *Resource) doValidateAttributes() (errs ValidationErrors) { + for k, attr := range r.block.Attributes { + if attr.Optional { + continue + } + + v := r.values.Get(k) + if attr.Required { + fails := v == nil + if !fails { + if l, ok := v.Starlark().(*starlark.List); ok && l.Len() == 0 { + fails = true + } + } + + if fails { + errs = append(errs, NewValidationError(r.CallStack(), "%s: attr %q is required", r, k)) + } + } + } + + return +} + +func (r *Resource) doValidateBlocks() (errs ValidationErrors) { + for k, block := range r.block.BlockTypes { + v := r.values.Get(k) + if block.MinItems > 0 && v == nil { + errs = append(errs, NewValidationError(r.CallStack(), "%s: attr %q is required", r, k)) + continue + } + + if v == nil { + continue + } + + errs = append(errs, v.Starlark().(Validabler).Validate()...) + } + + return +} diff --git a/starlark/types/validate_test.go b/starlark/types/validate_test.go new file mode 100644 index 0000000..b0b76cd --- /dev/null +++ b/starlark/types/validate_test.go @@ -0,0 +1,9 @@ +package types + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + doTest(t, "testdata/validate.star") +}