1
1
Fork 0
mirror of https://github.com/mcuadros/ascode synced 2024-05-08 16:46:18 +02:00

starlark/types: validate function and CallStack reccord

This commit is contained in:
Máximo Cuadros 2020-04-10 00:00:11 +02:00
parent 2a189546ee
commit 4a1a60b57a
No known key found for this signature in database
GPG Key ID: 17A5DFEDC735AE4B
9 changed files with 312 additions and 22 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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),
}

View File

@ -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
}

View File

@ -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
}

29
starlark/types/testdata/validate.star vendored Normal file
View File

@ -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)

212
starlark/types/validate.go Normal file
View File

@ -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>
// 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
}

View File

@ -0,0 +1,9 @@
package types
import (
"testing"
)
func TestValidate(t *testing.T) {
doTest(t, "testdata/validate.star")
}