diff --git a/config/config.go b/config/config.go index 5de811487..2def8c842 100644 --- a/config/config.go +++ b/config/config.go @@ -388,6 +388,17 @@ func (c *Config) Validate() error { } } + // Check that all variables are in the proper context + for source, rc := range c.rawConfigs() { + walker := &interpolationWalker{ + ContextF: c.validateVarContextFn(source, &errs), + } + if err := reflectwalk.Walk(rc.Raw, walker); err != nil { + errs = append(errs, fmt.Errorf( + "%s: error reading config: %s", source, err)) + } + } + if len(errs) > 0 { return &multierror.Error{Errors: errs} } @@ -400,33 +411,57 @@ func (c *Config) Validate() error { // are valid in the Validate step. func (c *Config) InterpolatedVariables() map[string][]InterpolatedVariable { result := make(map[string][]InterpolatedVariable) - for _, pc := range c.ProviderConfigs { - source := fmt.Sprintf("provider config '%s'", pc.Name) - for _, v := range pc.RawConfig.Variables { + for source, rc := range c.rawConfigs() { + for _, v := range rc.Variables { result[source] = append(result[source], v) } } + return result +} + +// rawConfigs returns all of the RawConfigs that are available keyed by +// a human-friendly source. +func (c *Config) rawConfigs() map[string]*RawConfig { + result := make(map[string]*RawConfig) + for _, pc := range c.ProviderConfigs { + source := fmt.Sprintf("provider config '%s'", pc.Name) + result[source] = pc.RawConfig + } for _, rc := range c.Resources { source := fmt.Sprintf("resource '%s'", rc.Id()) - for _, v := range rc.RawCount.Variables { - result[source] = append(result[source], v) - } - for _, v := range rc.RawConfig.Variables { - result[source] = append(result[source], v) - } + result[source+" count"] = rc.RawCount + result[source+" config"] = rc.RawConfig } for _, o := range c.Outputs { source := fmt.Sprintf("output '%s'", o.Name) - for _, v := range o.RawConfig.Variables { - result[source] = append(result[source], v) - } + result[source] = o.RawConfig } return result } +func (c *Config) validateVarContextFn( + source string, errs *[]error) interpolationWalkerContextFunc { + return func(loc reflectwalk.Location, i Interpolation) { + vi, ok := i.(*VariableInterpolation) + if !ok { + return + } + + rv, ok := vi.Variable.(*ResourceVariable) + if !ok { + return + } + + if rv.Multi && loc != reflectwalk.SliceElem { + *errs = append(*errs, fmt.Errorf( + "%s: multi-variable must be in a slice", source)) + } + } +} + func (m *Module) mergerName() string { return m.Id() } diff --git a/config/config_test.go b/config/config_test.go index ef93e9792..abd2c0bbc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -221,6 +221,13 @@ func TestConfigValidate_varDefaultInterpolate(t *testing.T) { } } +func TestConfigValidate_varMultiNonSlice(t *testing.T) { + c := testConfig(t, "validate-var-multi-non-slice") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_varModule(t *testing.T) { c := testConfig(t, "validate-var-module") if err := c.Validate(); err != nil { diff --git a/config/interpolate_walk.go b/config/interpolate_walk.go index 538475c3a..b867deaf2 100644 --- a/config/interpolate_walk.go +++ b/config/interpolate_walk.go @@ -22,8 +22,17 @@ var interpRegexp *regexp.Regexp = regexp.MustCompile( // (github.com/mitchellh/reflectwalk) that can be used to automatically // execute a callback for an interpolation. type interpolationWalker struct { - F interpolationWalkerFunc - Replace bool + // F is the function to call for every interpolation. It can be nil. + // + // If Replace is true, then the return value of F will be used to + // replace the interpolation. + F interpolationWalkerFunc + Replace bool + + // ContextF is an advanced version of F that also receives the + // location of where it is in the structure. This lets you do + // context-aware validation. + ContextF interpolationWalkerContextFunc key []string lastValue reflect.Value @@ -43,6 +52,14 @@ type interpolationWalker struct { // value can be anything as it will have no effect. type interpolationWalkerFunc func(Interpolation) (string, error) +// interpolationWalkerContextFunc is called by interpolationWalk if +// ContextF is set. This receives both the interpolation and the location +// where the interpolation is. +// +// This callback can be used to validate the location of the interpolation +// within the configuration. +type interpolationWalkerContextFunc func(reflectwalk.Location, Interpolation) + func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { w.loc = loc return nil @@ -129,6 +146,14 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error { return err } + if w.ContextF != nil { + w.ContextF(w.loc, i) + } + + if w.F == nil { + continue + } + replaceVal, err := w.F(i) if err != nil { return fmt.Errorf( diff --git a/config/test-fixtures/validate-var-multi-non-slice/main.tf b/config/test-fixtures/validate-var-multi-non-slice/main.tf new file mode 100644 index 000000000..3f3a996ff --- /dev/null +++ b/config/test-fixtures/validate-var-multi-non-slice/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + count = 3 +} + +resource "aws_instance" "bar" { + foo = "${aws_instance.foo.*.id}" +}