terraform: validate self references

This commit is contained in:
Mitchell Hashimoto 2017-01-25 21:00:45 -08:00
parent 4a9cafcd67
commit ae6bf241ec
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
4 changed files with 199 additions and 0 deletions

View File

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

View File

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

View File

@ -6,6 +6,10 @@ package terraform
// The embedder should implement `DynamicExpand` to process the count. // The embedder should implement `DynamicExpand` to process the count.
type NodeAbstractCountResource struct { type NodeAbstractCountResource struct {
*NodeAbstractResource *NodeAbstractResource
// Validate, if true, will perform the validation for the count.
// This should only be turned on for the "validate" operation.
Validate bool
} }
// GraphNodeEvalable // GraphNodeEvalable
@ -21,6 +25,16 @@ func (n *NodeAbstractCountResource) EvalTree() EvalNode {
&EvalInterpolate{Config: n.Config.RawCount}, &EvalInterpolate{Config: n.Config.RawCount},
&EvalCountCheckComputed{Resource: n.Config}, &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}, &EvalCountFixZeroOneBoundary{Resource: n.Config},
}, },
} }

View File

@ -10,6 +10,14 @@ type NodeValidatableResource struct {
*NodeAbstractCountResource *NodeAbstractCountResource
} }
// GraphNodeEvalable
func (n *NodeValidatableResource) EvalTree() EvalNode {
// Ensure we're validating
c := n.NodeAbstractCountResource
c.Validate = true
return c.EvalTree()
}
// GraphNodeDynamicExpandable // GraphNodeDynamicExpandable
func (n *NodeValidatableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { func (n *NodeValidatableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
// Grab the state which we read // Grab the state which we read
@ -91,6 +99,10 @@ func (n *NodeValidatableResourceInstance) EvalTree() EvalNode {
seq := &EvalSequence{ seq := &EvalSequence{
Nodes: []EvalNode{ Nodes: []EvalNode{
&EvalValidateResourceSelfRef{
Addr: &addr,
Config: &n.Config.RawConfig,
},
&EvalGetProvider{ &EvalGetProvider{
Name: n.ProvidedBy()[0], Name: n.ProvidedBy()[0],
Output: &provider, Output: &provider,