From 76ce6e45f77600748f2d9c72cc7e420c440bdca5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Feb 2015 19:47:06 -0500 Subject: [PATCH] terraform: extract interpolation to its own struct This is not really improving the way we do interpolation so much as its just shuffling bits around. I don't want to refactor interpolation in this branch so I needed to make the current way reusable so that I can reuse it in the new Context. --- terraform/context_old.go | 354 ++-------------------------- terraform/interpolate.go | 423 ++++++++++++++++++++++++++++++++++ terraform/interpolate_test.go | 135 +++++++++++ 3 files changed, 578 insertions(+), 334 deletions(-) create mode 100644 terraform/interpolate.go create mode 100644 terraform/interpolate_test.go diff --git a/terraform/context_old.go b/terraform/context_old.go index e1de3d2097..ed93bdf393 100644 --- a/terraform/context_old.go +++ b/terraform/context_old.go @@ -3,7 +3,6 @@ package terraform import ( "fmt" "log" - "os" "sort" "strconv" "strings" @@ -1505,125 +1504,30 @@ func (c *walkContext) computeVars( return nil } - // Copy the default variables - vs := make(map[string]ast.Variable) - for k, v := range c.defaultVariables { - vs[k] = ast.Variable{ - Value: v, - Type: ast.TypeString, - } + // Build the interpolater + i := &Interpolater{ + Operation: c.Operation, + Module: c.Context.module, + State: c.Context.state, + StateLock: &c.Context.sl, + Variables: c.Variables, + } + scope := &InterpolationScope{ + Path: c.Path, + Resource: r, + } + vs, err := i.Values(scope, raw.Variables) + if err != nil { + return err } - // Next, the actual computed variables - for n, rawV := range raw.Variables { - switch v := rawV.(type) { - case *config.CountVariable: - switch v.Type { - case config.CountValueIndex: - if r != nil { - vs[n] = ast.Variable{ - Value: int(r.CountIndex), - Type: ast.TypeInt, - } - } - } - case *config.ModuleVariable: - if c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - value, err := c.computeModuleVariable(v) - if err != nil { - return err - } - - vs[n] = ast.Variable{ - Value: value, + // Copy the default variables + for k, v := range c.defaultVariables { + if _, ok := vs[k]; !ok { + vs[k] = ast.Variable{ + Value: v, Type: ast.TypeString, } - case *config.PathVariable: - switch v.Type { - case config.PathValueCwd: - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf( - "Couldn't get cwd for var %s: %s", - v.FullKey(), err) - } - - vs[n] = ast.Variable{ - Value: wd, - Type: ast.TypeString, - } - case config.PathValueModule: - if t := c.Context.module.Child(c.Path[1:]); t != nil { - vs[n] = ast.Variable{ - Value: t.Config().Dir, - Type: ast.TypeString, - } - } - case config.PathValueRoot: - vs[n] = ast.Variable{ - Value: c.Context.module.Config().Dir, - Type: ast.TypeString, - } - } - case *config.ResourceVariable: - if c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - var attr string - var err error - if v.Multi && v.Index == -1 { - attr, err = c.computeResourceMultiVariable(v) - } else { - attr, err = c.computeResourceVariable(v) - } - if err != nil { - return err - } - - vs[n] = ast.Variable{ - Value: attr, - Type: ast.TypeString, - } - case *config.UserVariable: - val, ok := c.Variables[v.Name] - if ok { - vs[n] = ast.Variable{ - Value: val, - Type: ast.TypeString, - } - continue - } - - if _, ok := vs[n]; !ok && c.Operation == walkValidate { - vs[n] = ast.Variable{ - Value: config.UnknownVariableValue, - Type: ast.TypeString, - } - continue - } - - // Look up if we have any variables with this prefix because - // those are map overrides. Include those. - for k, val := range c.Variables { - if strings.HasPrefix(k, v.Name+".") { - vs["var."+k] = ast.Variable{ - Value: val, - Type: ast.TypeString, - } - } - } } } @@ -1631,224 +1535,6 @@ func (c *walkContext) computeVars( return raw.Interpolate(vs) } -func (c *walkContext) computeModuleVariable( - v *config.ModuleVariable) (string, error) { - // Build the path to our child - path := make([]string, len(c.Path), len(c.Path)+1) - copy(path, c.Path) - path = append(path, v.Name) - - // Grab some locks - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get that module from our state - mod := c.Context.state.ModuleByPath(path) - if mod == nil { - // If the module doesn't exist, then we can return an empty string. - // This happens usually only in Refresh() when we haven't populated - // a state. During validation, we semantically verify that all - // modules reference other modules, and graph ordering should - // ensure that the module is in the state, so if we reach this - // point otherwise it really is a panic. - return config.UnknownVariableValue, nil - } - - value, ok := mod.Outputs[v.Field] - if !ok { - // Same reasons as the comment above. - return config.UnknownVariableValue, nil - } - - return value, nil -} - -func (c *walkContext) computeResourceVariable( - v *config.ResourceVariable) (string, error) { - id := v.ResourceId() - if v.Multi { - id = fmt.Sprintf("%s.%d", id, v.Index) - } - - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get the information about this resource variable, and verify - // that it exists and such. - module, _, err := c.resourceVariableInfo(v) - if err != nil { - return "", err - } - - // If we have no module in the state yet or count, return empty - if module == nil || len(module.Resources) == 0 { - return "", nil - } - - // Get the resource out from the state. We know the state exists - // at this point and if there is a state, we expect there to be a - // resource with the given name. - r, ok := module.Resources[id] - if !ok && v.Multi && v.Index == 0 { - r, ok = module.Resources[v.ResourceId()] - } - if !ok { - r = nil - } - if r == nil { - return "", fmt.Errorf( - "Resource '%s' not found for variable '%s'", - id, - v.FullKey()) - } - - if r.Primary == nil { - goto MISSING - } - - if attr, ok := r.Primary.Attributes[v.Field]; ok { - return attr, nil - } - - // At apply time, we can't do the "maybe has it" check below - // that we need for plans since parent elements might be computed. - // Therefore, it is an error and we're missing the key. - // - // TODO: test by creating a state and configuration that is referencing - // a non-existent variable "foo.bar" where the state only has "foo" - // and verify plan works, but apply doesn't. - if c.Operation == walkApply { - goto MISSING - } - - // We didn't find the exact field, so lets separate the dots - // and see if anything along the way is a computed set. i.e. if - // we have "foo.0.bar" as the field, check to see if "foo" is - // a computed list. If so, then the whole thing is computed. - if parts := strings.Split(v.Field, "."); len(parts) > 1 { - for i := 1; i < len(parts); i++ { - // Lists and sets make this - key := fmt.Sprintf("%s.#", strings.Join(parts[:i], ".")) - if attr, ok := r.Primary.Attributes[key]; ok { - return attr, nil - } - - // Maps make this - key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) - if attr, ok := r.Primary.Attributes[key]; ok { - return attr, nil - } - } - } - -MISSING: - return "", fmt.Errorf( - "Resource '%s' does not have attribute '%s' "+ - "for variable '%s'", - id, - v.Field, - v.FullKey()) -} - -func (c *walkContext) computeResourceMultiVariable( - v *config.ResourceVariable) (string, error) { - c.Context.sl.RLock() - defer c.Context.sl.RUnlock() - - // Get the information about this resource variable, and verify - // that it exists and such. - module, cr, err := c.resourceVariableInfo(v) - if err != nil { - return "", err - } - - // Get the count so we know how many to iterate over - count, err := cr.Count() - if err != nil { - return "", fmt.Errorf( - "Error reading %s count: %s", - v.ResourceId(), - err) - } - - // If we have no module in the state yet or count, return empty - if module == nil || len(module.Resources) == 0 || count == 0 { - return "", nil - } - - var values []string - for i := 0; i < count; i++ { - id := fmt.Sprintf("%s.%d", v.ResourceId(), i) - - // If we're dealing with only a single resource, then the - // ID doesn't have a trailing index. - if count == 1 { - id = v.ResourceId() - } - - r, ok := module.Resources[id] - if !ok { - continue - } - - if r.Primary == nil { - continue - } - - attr, ok := r.Primary.Attributes[v.Field] - if !ok { - continue - } - - values = append(values, attr) - } - - if len(values) == 0 { - return "", fmt.Errorf( - "Resource '%s' does not have attribute '%s' "+ - "for variable '%s'", - v.ResourceId(), - v.Field, - v.FullKey()) - } - - return strings.Join(values, config.InterpSplitDelim), nil -} - -func (c *walkContext) resourceVariableInfo( - v *config.ResourceVariable) (*ModuleState, *config.Resource, error) { - // Get the module tree that contains our current path. This is - // either the current module (path is empty) or a child. - var modTree *module.Tree - childPath := c.Path[1:len(c.Path)] - if len(childPath) == 0 { - modTree = c.Context.module - } else { - modTree = c.Context.module.Child(childPath) - } - - // Get the resource from the configuration so we can verify - // that the resource is in the configuration and so we can access - // the configuration if we need to. - var cr *config.Resource - for _, r := range modTree.Config().Resources { - if r.Id() == v.ResourceId() { - cr = r - break - } - } - if cr == nil { - return nil, nil, fmt.Errorf( - "Resource '%s' not found for variable '%s'", - v.ResourceId(), - v.FullKey()) - } - - // Get the relevant module - module := c.Context.state.ModuleByPath(c.Path) - return module, cr, nil -} - type walkInputMeta struct { sync.Mutex diff --git a/terraform/interpolate.go b/terraform/interpolate.go new file mode 100644 index 0000000000..111aed63ed --- /dev/null +++ b/terraform/interpolate.go @@ -0,0 +1,423 @@ +package terraform + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/lang/ast" + "github.com/hashicorp/terraform/config/module" +) + +// Interpolater is the structure responsible for determining the values +// for interpolations such as `aws_instance.foo.bar`. +type Interpolater struct { + Operation walkOperation + Module *module.Tree + State *State + StateLock *sync.RWMutex + Variables map[string]string +} + +// InterpolationScope is the current scope of execution. This is required +// since some variables which are interpolated are dependent on what we're +// operating on and where we are. +type InterpolationScope struct { + Path []string + Resource *Resource +} + +// Values returns the values for all the variables in the given map. +func (i *Interpolater) Values( + scope *InterpolationScope, + vars map[string]config.InterpolatedVariable) (map[string]ast.Variable, error) { + result := make(map[string]ast.Variable, len(vars)) + for n, rawV := range vars { + var err error + switch v := rawV.(type) { + case *config.CountVariable: + err = i.valueCountVar(scope, n, v, result) + case *config.ModuleVariable: + err = i.valueModuleVar(scope, n, v, result) + case *config.PathVariable: + err = i.valuePathVar(scope, n, v, result) + case *config.ResourceVariable: + err = i.valueResourceVar(scope, n, v, result) + case *config.UserVariable: + err = i.valueUserVar(scope, n, v, result) + default: + err = fmt.Errorf("%s: unknown variable type: %T", n, rawV) + } + + if err != nil { + return nil, err + } + } + + return result, nil +} + +func (i *Interpolater) valueCountVar( + scope *InterpolationScope, + n string, + v *config.CountVariable, + result map[string]ast.Variable) error { + switch v.Type { + case config.CountValueIndex: + result[n] = ast.Variable{ + Value: scope.Resource.CountIndex, + Type: ast.TypeInt, + } + return nil + default: + return fmt.Errorf("%s: unknown count type: %#v", n, v.Type) + } +} + +func (i *Interpolater) valueModuleVar( + scope *InterpolationScope, + n string, + v *config.ModuleVariable, + result map[string]ast.Variable) error { + // If we're computing all dynamic fields, then module vars count + // and we mark it as computed. + if i.Operation == walkValidate { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + // Build the path to the child module we want + path := make([]string, len(scope.Path), len(scope.Path)+1) + copy(path, scope.Path) + path = append(path, v.Name) + + // Grab the lock so that if other interpolations are running or + // state is being modified, we'll be safe. + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the module where we're looking for the value + var value string + mod := i.State.ModuleByPath(path) + if mod == nil { + // If the module doesn't exist, then we can return an empty string. + // This happens usually only in Refresh() when we haven't populated + // a state. During validation, we semantically verify that all + // modules reference other modules, and graph ordering should + // ensure that the module is in the state, so if we reach this + // point otherwise it really is a panic. + value = config.UnknownVariableValue + } else { + // Get the value from the outputs + var ok bool + value, ok = mod.Outputs[v.Field] + if !ok { + // Same reasons as the comment above. + value = config.UnknownVariableValue + } + } + + result[n] = ast.Variable{ + Value: value, + Type: ast.TypeString, + } + return nil +} + +func (i *Interpolater) valuePathVar( + scope *InterpolationScope, + n string, + v *config.PathVariable, + result map[string]ast.Variable) error { + switch v.Type { + case config.PathValueCwd: + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf( + "Couldn't get cwd for var %s: %s", + v.FullKey(), err) + } + + result[n] = ast.Variable{ + Value: wd, + Type: ast.TypeString, + } + case config.PathValueModule: + if t := i.Module.Child(scope.Path[1:]); t != nil { + result[n] = ast.Variable{ + Value: t.Config().Dir, + Type: ast.TypeString, + } + } + case config.PathValueRoot: + result[n] = ast.Variable{ + Value: i.Module.Config().Dir, + Type: ast.TypeString, + } + default: + return fmt.Errorf("%s: unknown path type: %#v", n, v.Type) + } + + return nil + +} + +func (i *Interpolater) valueResourceVar( + scope *InterpolationScope, + n string, + v *config.ResourceVariable, + result map[string]ast.Variable) error { + // If we're computing all dynamic fields, then module vars count + // and we mark it as computed. + if i.Operation == walkValidate { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + var attr string + var err error + if v.Multi && v.Index == -1 { + attr, err = i.computeResourceMultiVariable(scope, v) + } else { + attr, err = i.computeResourceVariable(scope, v) + } + if err != nil { + return err + } + + result[n] = ast.Variable{ + Value: attr, + Type: ast.TypeString, + } + return nil +} + +func (i *Interpolater) valueUserVar( + scope *InterpolationScope, + n string, + v *config.UserVariable, + result map[string]ast.Variable) error { + val, ok := i.Variables[v.Name] + if ok { + result[n] = ast.Variable{ + Value: val, + Type: ast.TypeString, + } + return nil + } + + if _, ok := result[n]; !ok && i.Operation == walkValidate { + result[n] = ast.Variable{ + Value: config.UnknownVariableValue, + Type: ast.TypeString, + } + return nil + } + + // Look up if we have any variables with this prefix because + // those are map overrides. Include those. + for k, val := range i.Variables { + if strings.HasPrefix(k, v.Name+".") { + result["var."+k] = ast.Variable{ + Value: val, + Type: ast.TypeString, + } + } + } + + return nil +} + +func (i *Interpolater) computeResourceVariable( + scope *InterpolationScope, + v *config.ResourceVariable) (string, error) { + id := v.ResourceId() + if v.Multi { + id = fmt.Sprintf("%s.%d", id, v.Index) + } + + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the information about this resource variable, and verify + // that it exists and such. + module, _, err := i.resourceVariableInfo(scope, v) + if err != nil { + return "", err + } + + // If we have no module in the state yet or count, return empty + if module == nil || len(module.Resources) == 0 { + return "", nil + } + + // Get the resource out from the state. We know the state exists + // at this point and if there is a state, we expect there to be a + // resource with the given name. + r, ok := module.Resources[id] + if !ok && v.Multi && v.Index == 0 { + r, ok = module.Resources[v.ResourceId()] + } + if !ok { + r = nil + } + if r == nil { + return "", fmt.Errorf( + "Resource '%s' not found for variable '%s'", + id, + v.FullKey()) + } + + if r.Primary == nil { + goto MISSING + } + + if attr, ok := r.Primary.Attributes[v.Field]; ok { + return attr, nil + } + + // At apply time, we can't do the "maybe has it" check below + // that we need for plans since parent elements might be computed. + // Therefore, it is an error and we're missing the key. + // + // TODO: test by creating a state and configuration that is referencing + // a non-existent variable "foo.bar" where the state only has "foo" + // and verify plan works, but apply doesn't. + if i.Operation == walkApply { + goto MISSING + } + + // We didn't find the exact field, so lets separate the dots + // and see if anything along the way is a computed set. i.e. if + // we have "foo.0.bar" as the field, check to see if "foo" is + // a computed list. If so, then the whole thing is computed. + if parts := strings.Split(v.Field, "."); len(parts) > 1 { + for i := 1; i < len(parts); i++ { + // Lists and sets make this + key := fmt.Sprintf("%s.#", strings.Join(parts[:i], ".")) + if attr, ok := r.Primary.Attributes[key]; ok { + return attr, nil + } + + // Maps make this + key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) + if attr, ok := r.Primary.Attributes[key]; ok { + return attr, nil + } + } + } + +MISSING: + return "", fmt.Errorf( + "Resource '%s' does not have attribute '%s' "+ + "for variable '%s'", + id, + v.Field, + v.FullKey()) +} + +func (i *Interpolater) computeResourceMultiVariable( + scope *InterpolationScope, + v *config.ResourceVariable) (string, error) { + i.StateLock.RLock() + defer i.StateLock.RUnlock() + + // Get the information about this resource variable, and verify + // that it exists and such. + module, cr, err := i.resourceVariableInfo(scope, v) + if err != nil { + return "", err + } + + // Get the count so we know how many to iterate over + count, err := cr.Count() + if err != nil { + return "", fmt.Errorf( + "Error reading %s count: %s", + v.ResourceId(), + err) + } + + // If we have no module in the state yet or count, return empty + if module == nil || len(module.Resources) == 0 || count == 0 { + return "", nil + } + + var values []string + for i := 0; i < count; i++ { + id := fmt.Sprintf("%s.%d", v.ResourceId(), i) + + // If we're dealing with only a single resource, then the + // ID doesn't have a trailing index. + if count == 1 { + id = v.ResourceId() + } + + r, ok := module.Resources[id] + if !ok { + continue + } + + if r.Primary == nil { + continue + } + + attr, ok := r.Primary.Attributes[v.Field] + if !ok { + continue + } + + values = append(values, attr) + } + + if len(values) == 0 { + return "", fmt.Errorf( + "Resource '%s' does not have attribute '%s' "+ + "for variable '%s'", + v.ResourceId(), + v.Field, + v.FullKey()) + } + + return strings.Join(values, config.InterpSplitDelim), nil +} + +func (i *Interpolater) resourceVariableInfo( + scope *InterpolationScope, + v *config.ResourceVariable) (*ModuleState, *config.Resource, error) { + // Get the module tree that contains our current path. This is + // either the current module (path is empty) or a child. + modTree := i.Module + if len(scope.Path) > 1 { + modTree = i.Module.Child(scope.Path[1:]) + } + + // Get the resource from the configuration so we can verify + // that the resource is in the configuration and so we can access + // the configuration if we need to. + var cr *config.Resource + for _, r := range modTree.Config().Resources { + if r.Id() == v.ResourceId() { + cr = r + break + } + } + if cr == nil { + return nil, nil, fmt.Errorf( + "Resource '%s' not found for variable '%s'", + v.ResourceId(), + v.FullKey()) + } + + // Get the relevant module + module := i.State.ModuleByPath(scope.Path) + return module, cr, nil +} diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go new file mode 100644 index 0000000000..13d56fffb6 --- /dev/null +++ b/terraform/interpolate_test.go @@ -0,0 +1,135 @@ +package terraform + +import ( + "os" + "reflect" + "sync" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/lang/ast" +) + +func TestInterpolater_countIndex(t *testing.T) { + i := &Interpolater{} + + scope := &InterpolationScope{ + Path: rootModulePath, + Resource: &Resource{CountIndex: 42}, + } + + testInterpolate(t, i, scope, "count.index", ast.Variable{ + Value: 42, + Type: ast.TypeInt, + }) +} + +func TestInterpolater_moduleVariable(t *testing.T) { + lock := new(sync.RWMutex) + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "bar", + }, + }, + }, + }, + &ModuleState{ + Path: []string{RootModuleName, "child"}, + Outputs: map[string]string{ + "foo": "bar", + }, + }, + }, + } + + i := &Interpolater{ + State: state, + StateLock: lock, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolate(t, i, scope, "module.child.foo", ast.Variable{ + Value: "bar", + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathCwd(t *testing.T) { + i := &Interpolater{} + scope := &InterpolationScope{} + + expected, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + + testInterpolate(t, i, scope, "path.cwd", ast.Variable{ + Value: expected, + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathModule(t *testing.T) { + mod := testModule(t, "interpolate-path-module") + i := &Interpolater{ + Module: mod, + } + scope := &InterpolationScope{ + Path: []string{RootModuleName, "child"}, + } + + path := mod.Child([]string{"child"}).Config().Dir + testInterpolate(t, i, scope, "path.module", ast.Variable{ + Value: path, + Type: ast.TypeString, + }) +} + +func TestInterpolater_pathRoot(t *testing.T) { + mod := testModule(t, "interpolate-path-module") + i := &Interpolater{ + Module: mod, + } + scope := &InterpolationScope{ + Path: []string{RootModuleName, "child"}, + } + + path := mod.Config().Dir + testInterpolate(t, i, scope, "path.root", ast.Variable{ + Value: path, + Type: ast.TypeString, + }) +} + +func testInterpolate( + t *testing.T, i *Interpolater, + scope *InterpolationScope, + n string, expectedVar ast.Variable) { + v, err := config.NewInterpolatedVariable(n) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual, err := i.Values(scope, map[string]config.InterpolatedVariable{ + "foo": v, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := map[string]ast.Variable{ + "foo": expectedVar, + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +}