func (i *Interpolater) valueUserVar( scope *InterpolationScope, n string, v *config.UserVariable, result map[string]ast.Variable) error { i.VariableValuesLock.Lock() defer i.VariableValuesLock.Unlock() val, ok := i.VariableValues[v.Name] if ok { varValue, err := hil.InterfaceToVariable(val) if err != nil { return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s", v.Name, val, err) } result[n] = varValue return nil } if _, ok := result[n]; !ok && i.Operation == walkValidate { result[n] = unknownVariable() return nil } // Look up if we have any variables with this prefix because // those are map overrides. Include those. for k, val := range i.VariableValues { if strings.HasPrefix(k, v.Name+".") { keyComponents := strings.Split(k, ".") overrideKey := keyComponents[len(keyComponents)-1] mapInterface, ok := result["var."+v.Name] if !ok { return fmt.Errorf("override for non-existent variable: %s", v.Name) } mapVariable := mapInterface.Value.(map[string]ast.Variable) varValue, err := hil.InterfaceToVariable(val) if err != nil { return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s", v.Name, val, err) } mapVariable[overrideKey] = varValue } } return nil }
func (i *Interpolater) interpolateComplexTypeAttribute( resourceID string, attributes map[string]string) (ast.Variable, error) { attr := attributes[resourceID+".#"] log.Printf("[DEBUG] Interpolating computed complex type attribute %s (%s)", resourceID, attr) // In Terraform's internal dotted representation of list-like attributes, the // ".#" count field is marked as unknown to indicate "this whole list is // unknown". We must honor that meaning here so computed references can be // treated properly during the plan phase. if attr == config.UnknownVariableValue { return unknownVariable(), nil } // At this stage we don't know whether the item is a list or a map, so we // examine the keys to see whether they are all numeric. var numericKeys []string var allKeys []string numberedListKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") otherListKey := regexp.MustCompile("^" + resourceID + "\\.([^#]+)$") for id, _ := range attributes { if numberedListKey.MatchString(id) { numericKeys = append(numericKeys, id) } if submatches := otherListKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 { allKeys = append(allKeys, submatches[0][1]) } } if len(numericKeys) == len(allKeys) { // This is a list var members []string for _, key := range numericKeys { members = append(members, attributes[key]) } sort.Strings(members) return hil.InterfaceToVariable(members) } else { // This is a map members := make(map[string]interface{}) for _, key := range allKeys { members[key] = attributes[resourceID+"."+key] } return hil.InterfaceToVariable(members) } }
// interpolationFuncValues implements the "values" function that yields a list of // keys of map types within a Terraform configuration. func interpolationFuncValues(vs map[string]ast.Variable) ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap}, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { mapVar := args[0].(map[string]ast.Variable) keys := make([]string, 0) for k, _ := range mapVar { keys = append(keys, k) } sort.Strings(keys) values := make([]string, len(keys)) for index, key := range keys { if value, ok := mapVar[key].Value.(string); ok { values[index] = value } else { return "", fmt.Errorf("values(): %q has element with bad type %s", key, mapVar[key].Type) } } variable, err := hil.InterfaceToVariable(values) if err != nil { return nil, err } return variable.Value, nil }, } }
// 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) { if scope == nil { scope = &InterpolationScope{} } result := make(map[string]ast.Variable, len(vars)) // Copy the default variables if i.Module != nil && scope != nil { mod := i.Module if len(scope.Path) > 1 { mod = i.Module.Child(scope.Path[1:]) } for _, v := range mod.Config().Variables { // Set default variables if v.Default == nil { continue } n := fmt.Sprintf("var.%s", v.Name) variable, err := hil.InterfaceToVariable(v.Default) if err != nil { return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default) } result[n] = variable } } 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.SelfVariable: err = i.valueSelfVar(scope, n, v, result) case *config.SimpleVariable: err = i.valueSimpleVar(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) valueModuleVar( scope *InterpolationScope, n string, v *config.ModuleVariable, result map[string]ast.Variable) error { // 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 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. result[n] = unknownVariable() // During apply this is always an error if i.Operation == walkApply { return fmt.Errorf( "Couldn't find module %q for var: %s", v.Name, v.FullKey()) } } else { // Get the value from the outputs if outputState, ok := mod.Outputs[v.Field]; ok { output, err := hil.InterfaceToVariable(outputState.Value) if err != nil { return err } result[n] = output } else { // Same reasons as the comment above. result[n] = unknownVariable() // During apply this is always an error if i.Operation == walkApply { return fmt.Errorf( "Couldn't find output %q for module var: %s", v.Field, v.FullKey()) } } } return nil }
func (i *Interpolater) interpolateComplexTypeAttribute( resourceID string, attributes map[string]string) (ast.Variable, error) { // We can now distinguish between lists and maps in state by the count field: // - lists (and by extension, sets) use the traditional .# notation // - maps use the newer .% notation // Consequently here we can decide how to deal with the keys appropriately // based on whether the type is a map of list. if lengthAttr, isList := attributes[resourceID+".#"]; isList { log.Printf("[DEBUG] Interpolating computed list element attribute %s (%s)", resourceID, lengthAttr) // In Terraform's internal dotted representation of list-like attributes, the // ".#" count field is marked as unknown to indicate "this whole list is // unknown". We must honor that meaning here so computed references can be // treated properly during the plan phase. if lengthAttr == config.UnknownVariableValue { return unknownVariable(), nil } expanded := flatmap.Expand(attributes, resourceID) return hil.InterfaceToVariable(expanded) } if lengthAttr, isMap := attributes[resourceID+".%"]; isMap { log.Printf("[DEBUG] Interpolating computed map element attribute %s (%s)", resourceID, lengthAttr) // In Terraform's internal dotted representation of map attributes, the // ".%" count field is marked as unknown to indicate "this whole list is // unknown". We must honor that meaning here so computed references can be // treated properly during the plan phase. if lengthAttr == config.UnknownVariableValue { return unknownVariable(), nil } expanded := flatmap.Expand(attributes, resourceID) return hil.InterfaceToVariable(expanded) } return ast.Variable{}, fmt.Errorf("No complex type %s found", resourceID) }
// interpolationFuncMap creates a map from the parameters passed // to it. func interpolationFuncMap() ast.Function { return ast.Function{ ArgTypes: []ast.Type{}, ReturnType: ast.TypeMap, Variadic: true, VariadicType: ast.TypeAny, Callback: func(args []interface{}) (interface{}, error) { outputMap := make(map[string]ast.Variable) if len(args)%2 != 0 { return nil, fmt.Errorf("requires an even number of arguments, got %d", len(args)) } var firstType *ast.Type for i := 0; i < len(args); i += 2 { key, ok := args[i].(string) if !ok { return nil, fmt.Errorf("argument %d represents a key, so it must be a string", i+1) } val := args[i+1] variable, err := hil.InterfaceToVariable(val) if err != nil { return nil, err } // Enforce map type homogeneity if firstType == nil { firstType = &variable.Type } else if variable.Type != *firstType { return nil, fmt.Errorf("all map values must have the same type, got %s then %s", firstType.Printable(), variable.Type.Printable()) } // Check for duplicate keys if _, ok := outputMap[key]; ok { return nil, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) } outputMap[key] = variable } return outputMap, nil }, } }
func interfaceToVariableSwallowError(input interface{}) ast.Variable { variable, _ := hil.InterfaceToVariable(input) return variable }
func TestResourceConfigGetRaw(t *testing.T) { cases := []struct { Config map[string]interface{} Vars map[string]interface{} Key string Value interface{} }{ // Referencing a list-of-maps variable doesn't work from GetRaw. // The ConfigFieldReader currently catches this case and looks up the // variable in the config. { Vars: map[string]interface{}{ "maplist": []interface{}{ map[string]interface{}{ "key": "a", }, map[string]interface{}{ "key": "b", }, }, }, Config: map[string]interface{}{ "maplist": "${var.maplist}", }, Key: "maplist.0", Value: nil, }, // Reference a map-of-lists variable. // The ConfigFieldReader currently catches this case and looks up the // variable in the config. { Vars: map[string]interface{}{ "listmap": map[string]interface{}{ "key1": []interface{}{"a", "b"}, "key2": []interface{}{"c", "d"}, }, }, Config: map[string]interface{}{ "listmap": "${var.listmap}", }, Key: "listmap.key1", Value: nil, }, } for i, tc := range cases { var rawC *config.RawConfig if tc.Config != nil { var err error rawC, err = config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } } if tc.Vars != nil { vs := make(map[string]ast.Variable) for k, v := range tc.Vars { hilVar, err := hil.InterfaceToVariable(v) if err != nil { t.Fatalf("%#v to var: %s", v, err) } vs["var."+k] = hilVar } if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } rc := NewResourceConfig(rawC) rc.interpolateForce() // Test getting a key t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) { v, ok := rc.GetRaw(tc.Key) if ok && v == nil { t.Fatal("(nil, true) returned from GetRaw") } if !reflect.DeepEqual(v, tc.Value) { t.Fatalf("%d bad: %#v", i, v) } }) } }
func (i *Interpolater) interpolateComplexTypeAttribute( resourceID string, attributes map[string]string) (ast.Variable, error) { // We can now distinguish between lists and maps in state by the count field: // - lists (and by extension, sets) use the traditional .# notation // - maps use the newer .% notation // Consequently here we can decide how to deal with the keys appropriately // based on whether the type is a map of list. if lengthAttr, isList := attributes[resourceID+".#"]; isList { log.Printf("[DEBUG] Interpolating computed list element attribute %s (%s)", resourceID, lengthAttr) // In Terraform's internal dotted representation of list-like attributes, the // ".#" count field is marked as unknown to indicate "this whole list is // unknown". We must honor that meaning here so computed references can be // treated properly during the plan phase. if lengthAttr == config.UnknownVariableValue { return unknownVariable(), nil } keys := make([]string, 0) listElementKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") for id, _ := range attributes { if listElementKey.MatchString(id) { keys = append(keys, id) } } sort.Strings(keys) var members []string for _, key := range keys { members = append(members, attributes[key]) } return hil.InterfaceToVariable(members) } if lengthAttr, isMap := attributes[resourceID+".%"]; isMap { log.Printf("[DEBUG] Interpolating computed map element attribute %s (%s)", resourceID, lengthAttr) // In Terraform's internal dotted representation of map attributes, the // ".%" count field is marked as unknown to indicate "this whole list is // unknown". We must honor that meaning here so computed references can be // treated properly during the plan phase. if lengthAttr == config.UnknownVariableValue { return unknownVariable(), nil } resourceFlatMap := make(map[string]string) mapElementKey := regexp.MustCompile("^" + resourceID + "\\.([^%]+)$") for id, val := range attributes { if mapElementKey.MatchString(id) { resourceFlatMap[id] = val } } expanded := flatmap.Expand(resourceFlatMap, resourceID) return hil.InterfaceToVariable(expanded) } return ast.Variable{}, fmt.Errorf("No complex type %s found", resourceID) }
func (i *Interpolater) computeResourceMultiVariable( scope *InterpolationScope, v *config.ResourceVariable) (*ast.Variable, error) { i.StateLock.RLock() defer i.StateLock.RUnlock() unknownVariable := unknownVariable() // 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 nil, err } // Get the count so we know how many to iterate over count, err := cr.Count() if err != nil { return nil, 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 &ast.Variable{Type: ast.TypeList, Value: []ast.Variable{}}, nil } var values []string for j := 0; j < count; j++ { id := fmt.Sprintf("%s.%d", v.ResourceId(), j) // 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 } if singleAttr, ok := r.Primary.Attributes[v.Field]; ok { if singleAttr == config.UnknownVariableValue { return &unknownVariable, nil } values = append(values, singleAttr) continue } // computed list attribute _, ok = r.Primary.Attributes[v.Field+".#"] if !ok { continue } multiAttr, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes) if err != nil { return nil, err } if multiAttr == unknownVariable { return &ast.Variable{Type: ast.TypeString, Value: ""}, nil } for _, element := range multiAttr.Value.([]ast.Variable) { strVal := element.Value.(string) if strVal == config.UnknownVariableValue { return &unknownVariable, nil } values = append(values, strVal) } } if len(values) == 0 { // If the operation is refresh, it isn't an error for a value to // be unknown. Instead, we return that the value is computed so // that the graph can continue to refresh other nodes. It doesn't // matter because the config isn't interpolated anyways. // // For a Destroy, we're also fine with computed values, since our goal is // only to get destroy nodes for existing resources. // // For an input walk, computed values are okay to return because we're only // looking for missing variables to prompt the user for. if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput { return &unknownVariable, nil } return nil, fmt.Errorf( "Resource '%s' does not have attribute '%s' "+ "for variable '%s'", v.ResourceId(), v.Field, v.FullKey()) } variable, err := hil.InterfaceToVariable(values) return &variable, err }
func TestResourceConfigIsComputed(t *testing.T) { cases := []struct { Name string Config map[string]interface{} Vars map[string]interface{} Key string Result bool }{ { Name: "basic value", Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{ "foo": unknownValue(), }, Key: "foo", Result: true, }, { Name: "set with a computed element", Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{ "foo": []string{ "a", unknownValue(), }, }, Key: "foo", Result: true, }, { Name: "set with no computed elements", Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{ "foo": []string{ "a", "b", }, }, Key: "foo", Result: false, }, /* { Name: "set count with computed elements", Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{ "foo": []string{ "a", unknownValue(), }, }, Key: "foo.#", Result: true, }, */ { Name: "set count with computed elements", Config: map[string]interface{}{ "foo": []interface{}{"${var.foo}"}, }, Vars: map[string]interface{}{ "foo": []string{ "a", unknownValue(), }, }, Key: "foo.#", Result: true, }, { Name: "nested set with computed elements", Config: map[string]interface{}{ "route": []map[string]interface{}{ map[string]interface{}{ "index": "1", "gateway": []interface{}{"${var.foo}"}, }, }, }, Vars: map[string]interface{}{ "foo": unknownValue(), }, Key: "route.0.gateway", Result: true, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { var rawC *config.RawConfig if tc.Config != nil { var err error rawC, err = config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } } if tc.Vars != nil { vs := make(map[string]ast.Variable) for k, v := range tc.Vars { hilVar, err := hil.InterfaceToVariable(v) if err != nil { t.Fatalf("%#v to var: %s", v, err) } vs["var."+k] = hilVar } if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } rc := NewResourceConfig(rawC) rc.interpolateForce() t.Logf("Config: %#v", rc) actual := rc.IsComputed(tc.Key) if actual != tc.Result { t.Fatalf("bad: %#v", actual) } }) } }
func (i *Interpolater) computeResourceMultiVariable( scope *InterpolationScope, v *config.ResourceVariable) (*ast.Variable, error) { i.StateLock.RLock() defer i.StateLock.RUnlock() unknownVariable := unknownVariable() // 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 nil, err } // Get the keys for all the resources that are created for this resource countMax, err := i.resourceCountMax(module, cr, v) if err != nil { return nil, err } // If count is zero, we return an empty list if countMax == 0 { return &ast.Variable{Type: ast.TypeList, Value: []ast.Variable{}}, nil } // If we have no module in the state yet or count, return unknown if module == nil || len(module.Resources) == 0 { return &unknownVariable, nil } var values []interface{} for idx := 0; idx < countMax; idx++ { id := fmt.Sprintf("%s.%d", v.ResourceId(), idx) // ID doesn't have a trailing index. We try both here, but if a value // without a trailing index is found we prefer that. This choice // is for legacy reasons: older versions of TF preferred it. if id == v.ResourceId()+".0" { potential := v.ResourceId() if _, ok := module.Resources[potential]; ok { id = potential } } r, ok := module.Resources[id] if !ok { continue } if r.Primary == nil { continue } if singleAttr, ok := r.Primary.Attributes[v.Field]; ok { if singleAttr == config.UnknownVariableValue { return &unknownVariable, nil } values = append(values, singleAttr) continue } // computed list or map attribute _, isList := r.Primary.Attributes[v.Field+".#"] _, isMap := r.Primary.Attributes[v.Field+".%"] if !(isList || isMap) { continue } multiAttr, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes) if err != nil { return nil, err } if multiAttr == unknownVariable { return &unknownVariable, nil } values = append(values, multiAttr) } if len(values) == 0 { // If the operation is refresh, it isn't an error for a value to // be unknown. Instead, we return that the value is computed so // that the graph can continue to refresh other nodes. It doesn't // matter because the config isn't interpolated anyways. // // For a Destroy, we're also fine with computed values, since our goal is // only to get destroy nodes for existing resources. // // For an input walk, computed values are okay to return because we're only // looking for missing variables to prompt the user for. if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput { return &unknownVariable, nil } return nil, fmt.Errorf( "Resource '%s' does not have attribute '%s' "+ "for variable '%s'", v.ResourceId(), v.Field, v.FullKey()) } variable, err := hil.InterfaceToVariable(values) return &variable, err }
func TestResourceConfigGet(t *testing.T) { cases := []struct { Config map[string]interface{} Vars map[string]interface{} Key string Value interface{} }{ { Config: nil, Key: "foo", Value: nil, }, { Config: map[string]interface{}{ "foo": "bar", }, Key: "foo", Value: "bar", }, { Config: map[string]interface{}{ "foo": "${var.foo}", }, Key: "foo", Value: "${var.foo}", }, { Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{"foo": unknownValue()}, Key: "foo", Value: "${var.foo}", }, { Config: map[string]interface{}{ "foo": []interface{}{1, 2, 5}, }, Key: "foo.0", Value: 1, }, { Config: map[string]interface{}{ "foo": []interface{}{1, 2, 5}, }, Key: "foo.5", Value: nil, }, // get from map { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{"key": 1}, }, }, Key: "mapname.0.key", Value: 1, }, // get from map with dot in key { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{"key.name": 1}, }, }, Key: "mapname.0.key.name", Value: 1, }, // get from map with overlapping key names { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.2": 2, }, }, }, Key: "mapname.0.key.name.2", Value: 2, }, { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.foo": 2, }, }, }, Key: "mapname.0.key.name", Value: 1, }, { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "listkey": []map[string]interface{}{ {"key": 3}, }, }, }, }, Key: "mapname.0.listkey.0.key", Value: 3, }, // FIXME: this is ambiguous, and matches the nested map // leaving here to catch this behaviour if it changes. { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.0": 2, "key": map[string]interface{}{"name": 3}, }, }, }, Key: "mapname.0.key.name", Value: 3, }, /* // TODO: can't access this nested list at all. // FIXME: key with name matching substring of nested list can panic { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": []map[string]interface{}{ {"subkey": 1}, }, "key": 3, }, }, }, Key: "mapname.0.key.name.0.subkey", Value: 3, }, */ } for i, tc := range cases { var rawC *config.RawConfig if tc.Config != nil { var err error rawC, err = config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } } if tc.Vars != nil { vs := make(map[string]ast.Variable) for k, v := range tc.Vars { hilVar, err := hil.InterfaceToVariable(v) if err != nil { t.Fatalf("%#v to var: %s", v, err) } vs["var."+k] = hilVar } if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } rc := NewResourceConfig(rawC) rc.interpolateForce() // Test getting a key t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) { v, _ := rc.Get(tc.Key) if !reflect.DeepEqual(v, tc.Value) { t.Fatalf("%d bad: %#v", i, v) } }) // If we have vars, we don't test copying if len(tc.Vars) > 0 { continue } // Test copying and equality t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) { copy := rc.DeepCopy() if !reflect.DeepEqual(copy, rc) { t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc) } if !copy.Equal(rc) { t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc) } if !rc.Equal(copy) { t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc) } }) } }
func (i *Interpolater) computeResourceVariable( scope *InterpolationScope, v *config.ResourceVariable) (*ast.Variable, error) { id := v.ResourceId() if v.Multi { id = fmt.Sprintf("%s.%d", id, v.Index) } i.StateLock.RLock() defer i.StateLock.RUnlock() unknownVariable := unknownVariable() // These variables must be declared early because of the use of GOTO var isList bool var isMap bool // 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 nil, err } // If we're requesting "count" its a special variable that we grab // directly from the config itself. if v.Field == "count" { var count int if cr != nil { count, err = cr.Count() } else { count, err = i.resourceCountMax(module, cr, v) } if err != nil { return nil, fmt.Errorf( "Error reading %s count: %s", v.ResourceId(), err) } return &ast.Variable{Type: ast.TypeInt, Value: count}, 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. var r *ResourceState if module != nil && len(module.Resources) > 0 { var ok bool 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 || r.Primary == nil { if i.Operation == walkApply || i.Operation == walkPlan { return nil, fmt.Errorf( "Resource '%s' not found for variable '%s'", v.ResourceId(), v.FullKey()) } // If we have no module in the state yet or count, return empty. // NOTE(@mitchellh): I actually don't know why this is here. During // a refactor I kept this here to maintain the same behavior, but // I'm not sure why its here. if module == nil || len(module.Resources) == 0 { return nil, nil } goto MISSING } if attr, ok := r.Primary.Attributes[v.Field]; ok { v, err := hil.InterfaceToVariable(attr) return &v, err } // computed list or map attribute _, isList = r.Primary.Attributes[v.Field+".#"] _, isMap = r.Primary.Attributes[v.Field+".%"] if isList || isMap { variable, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes) return &variable, err } // 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 || i.Operation == walkDestroy { 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 { v, err := hil.InterfaceToVariable(attr) return &v, err } // Maps make this key = fmt.Sprintf("%s", strings.Join(parts[:i], ".")) if attr, ok := r.Primary.Attributes[key]; ok { v, err := hil.InterfaceToVariable(attr) return &v, err } } } MISSING: // Validation for missing interpolations should happen at a higher // semantic level. If we reached this point and don't have variables, // just return the computed value. if scope == nil && scope.Resource == nil { return &unknownVariable, nil } // If the operation is refresh, it isn't an error for a value to // be unknown. Instead, we return that the value is computed so // that the graph can continue to refresh other nodes. It doesn't // matter because the config isn't interpolated anyways. // // For a Destroy, we're also fine with computed values, since our goal is // only to get destroy nodes for existing resources. // // For an input walk, computed values are okay to return because we're only // looking for missing variables to prompt the user for. if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput { return &unknownVariable, nil } return nil, fmt.Errorf( "Resource '%s' does not have attribute '%s' "+ "for variable '%s'", id, v.Field, v.FullKey()) }
func TestResourceConfigCheckSet(t *testing.T) { cases := []struct { Name string Config map[string]interface{} Vars map[string]interface{} Input []string Errs bool }{ { Name: "computed basic", Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{ "foo": unknownValue(), }, Input: []string{"foo"}, Errs: false, }, { Name: "basic", Config: map[string]interface{}{ "foo": "bar", }, Vars: nil, Input: []string{"foo"}, Errs: false, }, { Name: "basic with not set", Config: map[string]interface{}{ "foo": "bar", }, Vars: nil, Input: []string{"foo", "bar"}, Errs: true, }, { Name: "basic with one computed", Config: map[string]interface{}{ "foo": "bar", "bar": "${var.foo}", }, Vars: map[string]interface{}{ "foo": unknownValue(), }, Input: []string{"foo", "bar"}, Errs: false, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { var rawC *config.RawConfig if tc.Config != nil { var err error rawC, err = config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } } if tc.Vars != nil { vs := make(map[string]ast.Variable) for k, v := range tc.Vars { hilVar, err := hil.InterfaceToVariable(v) if err != nil { t.Fatalf("%#v to var: %s", v, err) } vs["var."+k] = hilVar } if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } rc := NewResourceConfig(rawC) rc.interpolateForce() t.Logf("Config: %#v", rc) errs := rc.CheckSet(tc.Input) if tc.Errs != (len(errs) > 0) { t.Fatalf("bad: %#v", errs) } }) } }
func TestResourceConfigGet(t *testing.T) { cases := []struct { Config map[string]interface{} Vars map[string]interface{} Key string Value interface{} }{ { Config: nil, Key: "foo", Value: nil, }, { Config: map[string]interface{}{ "foo": "bar", }, Key: "foo", Value: "bar", }, { Config: map[string]interface{}{ "foo": "${var.foo}", }, Key: "foo", Value: "${var.foo}", }, { Config: map[string]interface{}{ "foo": "${var.foo}", }, Vars: map[string]interface{}{"foo": unknownValue()}, Key: "foo", Value: "${var.foo}", }, { Config: map[string]interface{}{ "foo": []interface{}{1, 2, 5}, }, Key: "foo.0", Value: 1, }, { Config: map[string]interface{}{ "foo": []interface{}{1, 2, 5}, }, Key: "foo.5", Value: nil, }, // get from map { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{"key": 1}, }, }, Key: "mapname.0.key", Value: 1, }, // get from map with dot in key { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{"key.name": 1}, }, }, Key: "mapname.0.key.name", Value: 1, }, // get from map with overlapping key names { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.2": 2, }, }, }, Key: "mapname.0.key.name.2", Value: 2, }, { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.foo": 2, }, }, }, Key: "mapname.0.key.name", Value: 1, }, { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "listkey": []map[string]interface{}{ {"key": 3}, }, }, }, }, Key: "mapname.0.listkey.0.key", Value: 3, }, // A map assigned to a list via interpolation should Get a non-existent // value. The test code now also checks that Get doesn't return (nil, // true), which it previously did for this configuration. { Config: map[string]interface{}{ "maplist": "${var.maplist}", }, Key: "maplist.0", Value: nil, }, // Reference list of maps variable. // This does not work from GetRaw. { Vars: map[string]interface{}{ "maplist": []interface{}{ map[string]interface{}{ "key": "a", }, map[string]interface{}{ "key": "b", }, }, }, Config: map[string]interface{}{ "maplist": "${var.maplist}", }, Key: "maplist.0", Value: map[string]interface{}{"key": "a"}, }, // Reference a map-of-lists variable. // This does not work from GetRaw. { Vars: map[string]interface{}{ "listmap": map[string]interface{}{ "key1": []interface{}{"a", "b"}, "key2": []interface{}{"c", "d"}, }, }, Config: map[string]interface{}{ "listmap": "${var.listmap}", }, Key: "listmap.key1", Value: []interface{}{"a", "b"}, }, // FIXME: this is ambiguous, and matches the nested map // leaving here to catch this behaviour if it changes. { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": 1, "key.name.0": 2, "key": map[string]interface{}{"name": 3}, }, }, }, Key: "mapname.0.key.name", Value: 3, }, /* // TODO: can't access this nested list at all. // FIXME: key with name matching substring of nested list can panic { Config: map[string]interface{}{ "mapname": []map[string]interface{}{ map[string]interface{}{ "key.name": []map[string]interface{}{ {"subkey": 1}, }, "key": 3, }, }, }, Key: "mapname.0.key.name.0.subkey", Value: 3, }, */ } for i, tc := range cases { var rawC *config.RawConfig if tc.Config != nil { var err error rawC, err = config.NewRawConfig(tc.Config) if err != nil { t.Fatalf("err: %s", err) } } if tc.Vars != nil { vs := make(map[string]ast.Variable) for k, v := range tc.Vars { hilVar, err := hil.InterfaceToVariable(v) if err != nil { t.Fatalf("%#v to var: %s", v, err) } vs["var."+k] = hilVar } if err := rawC.Interpolate(vs); err != nil { t.Fatalf("err: %s", err) } } rc := NewResourceConfig(rawC) rc.interpolateForce() // Test getting a key t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) { v, ok := rc.Get(tc.Key) if ok && v == nil { t.Fatal("(nil, true) returned from Get") } if !reflect.DeepEqual(v, tc.Value) { t.Fatalf("%d bad: %#v", i, v) } }) // If we have vars, we don't test copying if len(tc.Vars) > 0 { continue } // Test copying and equality t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) { copy := rc.DeepCopy() if !reflect.DeepEqual(copy, rc) { t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc) } if !copy.Equal(rc) { t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc) } if !rc.Equal(copy) { t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc) } }) } }