diff --git a/terraform/eval_validate_selfref.go b/terraform/eval_validate_selfref.go new file mode 100644 index 000000000..ae4436a2e --- /dev/null +++ b/terraform/eval_validate_selfref.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalValidateResourceSelfRef is an EvalNode implementation that validates that +// a configuration doesn't contain a reference to the resource itself. +// +// This must be done prior to interpolating configuration in order to avoid +// any infinite loop scenarios. +type EvalValidateResourceSelfRef struct { + Addr **ResourceAddress + Config **config.RawConfig +} + +func (n *EvalValidateResourceSelfRef) Eval(ctx EvalContext) (interface{}, error) { + addr := *n.Addr + conf := *n.Config + + // Go through the variables and find self references + var errs []error + for k, raw := range conf.Variables { + rv, ok := raw.(*config.ResourceVariable) + if !ok { + continue + } + + // Build an address from the variable + varAddr := &ResourceAddress{ + Path: addr.Path, + Mode: rv.Mode, + Type: rv.Type, + Name: rv.Name, + Index: rv.Index, + InstanceType: TypePrimary, + } + + // If the variable access is a multi-access (*), then we just + // match the index so that we'll match our own addr if everything + // else matches. + if rv.Multi && rv.Index == -1 { + varAddr.Index = addr.Index + } + + // This is a weird thing where ResourceAddres has index "-1" when + // index isn't set at all. This means index "0" for resource access. + // So, if we have this scenario, just set our varAddr to -1 so it + // matches. + if addr.Index == -1 && varAddr.Index == 0 { + varAddr.Index = -1 + } + + // If the addresses match, then this is a self reference + if varAddr.Equals(addr) && varAddr.Index == addr.Index { + errs = append(errs, fmt.Errorf( + "%s: self reference not allowed: %q", + addr, k)) + } + } + + // If no errors, no errors! + if len(errs) == 0 { + return nil, nil + } + + // Wrap the errors in the proper wrapper so we can handle validation + // formatting properly upstream. + return nil, &EvalValidateError{ + Errors: errs, + } +} diff --git a/terraform/eval_validate_selfref_test.go b/terraform/eval_validate_selfref_test.go new file mode 100644 index 000000000..f5e70cb66 --- /dev/null +++ b/terraform/eval_validate_selfref_test.go @@ -0,0 +1,99 @@ +package terraform + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/config" +) + +func TestEvalValidateResourceSelfRef(t *testing.T) { + cases := []struct { + Name string + Addr string + Config map[string]interface{} + Err bool + }{ + { + "no interpolations", + "aws_instance.foo", + map[string]interface{}{ + "foo": "bar", + }, + false, + }, + + { + "non self reference", + "aws_instance.foo", + map[string]interface{}{ + "foo": "${aws_instance.bar.id}", + }, + false, + }, + + { + "self reference", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.id}", + }, + true, + }, + + { + "self reference other index", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.4.id}", + }, + false, + }, + + { + "self reference same index", + "aws_instance.foo[4]", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.4.id}", + }, + true, + }, + + { + "self reference multi", + "aws_instance.foo[4]", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.*.id}", + }, + true, + }, + + { + "self reference multi single", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.*.id}", + }, + true, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + addr, err := ParseResourceAddress(tc.Addr) + if err != nil { + t.Fatalf("err: %s", err) + } + conf := config.TestRawConfig(t, tc.Config) + + n := &EvalValidateResourceSelfRef{Addr: &addr, Config: &conf} + result, err := n.Eval(nil) + if result != nil { + t.Fatal("result should always be nil") + } + if (err != nil) != tc.Err { + t.Fatalf("err: %s", err) + } + }) + } +} diff --git a/terraform/node_resource_abstract_count.go b/terraform/node_resource_abstract_count.go index 131ebccf9..9b4df757f 100644 --- a/terraform/node_resource_abstract_count.go +++ b/terraform/node_resource_abstract_count.go @@ -6,6 +6,10 @@ package terraform // The embedder should implement `DynamicExpand` to process the count. type NodeAbstractCountResource struct { *NodeAbstractResource + + // Validate, if true, will perform the validation for the count. + // This should only be turned on for the "validate" operation. + Validate bool } // GraphNodeEvalable @@ -21,6 +25,16 @@ func (n *NodeAbstractCountResource) EvalTree() EvalNode { &EvalInterpolate{Config: n.Config.RawCount}, &EvalCountCheckComputed{Resource: n.Config}, + + // If validation is enabled, perform the validation + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + return n.Validate, nil + }, + + Then: &EvalValidateCount{Resource: n.Config}, + }, + &EvalCountFixZeroOneBoundary{Resource: n.Config}, }, } diff --git a/terraform/node_resource_validate.go b/terraform/node_resource_validate.go index 8e99075c4..3ea5bfd12 100644 --- a/terraform/node_resource_validate.go +++ b/terraform/node_resource_validate.go @@ -10,6 +10,14 @@ type NodeValidatableResource struct { *NodeAbstractCountResource } +// GraphNodeEvalable +func (n *NodeValidatableResource) EvalTree() EvalNode { + // Ensure we're validating + c := n.NodeAbstractCountResource + c.Validate = true + return c.EvalTree() +} + // GraphNodeDynamicExpandable func (n *NodeValidatableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { // Grab the state which we read @@ -91,6 +99,10 @@ func (n *NodeValidatableResourceInstance) EvalTree() EvalNode { seq := &EvalSequence{ Nodes: []EvalNode{ + &EvalValidateResourceSelfRef{ + Addr: &addr, + Config: &n.Config.RawConfig, + }, &EvalGetProvider{ Name: n.ProvidedBy()[0], Output: &provider,