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:
parent
2a189546ee
commit
4a1a60b57a
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
doTest(t, "testdata/validate.star")
|
||||
}
|
Loading…
Reference in New Issue