func (i *Interpolater) interpolateListAttribute( resourceID string, attributes map[string]string) (string, error) { attr := attributes[resourceID+".#"] log.Printf("[DEBUG] Interpolating computed list 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 attr, nil } // Otherwise we gather the values from the list-like attribute and return // them. var members []string numberedListMember := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") for id, value := range attributes { if numberedListMember.MatchString(id) { members = append(members, value) } } sort.Strings(members) return config.NewStringList(members).String(), nil }
func (i *Interpolater) interpolateListAttribute( resourceID string, attributes map[string]string) (string, error) { attr := attributes[resourceID+".#"] log.Printf("[DEBUG] Interpolating computed list attribute %s (%s)", resourceID, attr) var members []string numberedListMember := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$") for id, value := range attributes { if numberedListMember.MatchString(id) { members = append(members, value) } } sort.Strings(members) return config.NewStringList(members).String(), nil }
func TestInterpolator_resourceMultiAttributesWithResourceCount(t *testing.T) { i := getInterpolaterFixture(t) scope := &InterpolationScope{ Path: rootModulePath, } name_servers := []string{ "ns-1334.awsdns-38.org", "ns-1680.awsdns-18.co.uk", "ns-498.awsdns-62.com", "ns-601.awsdns-11.net", "ns-000.awsdns-38.org", "ns-444.awsdns-18.co.uk", "ns-666.awsdns-11.net", "ns-999.awsdns-62.com", } // More than 1 element expectedNameServers := config.NewStringList(name_servers[0:4]).String() testInterpolate(t, i, scope, "aws_route53_zone.terra.0.name_servers", ast.Variable{ Value: expectedNameServers, Type: ast.TypeString, }) // More than 1 element in both expectedNameServers = config.NewStringList(name_servers).String() testInterpolate(t, i, scope, "aws_route53_zone.terra.*.name_servers", ast.Variable{ Value: expectedNameServers, Type: ast.TypeString, }) // Exactly 1 element testInterpolate(t, i, scope, "aws_route53_zone.terra.0.listeners", ast.Variable{ Value: config.NewStringList([]string{"red"}).String(), Type: ast.TypeString, }) // Exactly 1 element in both testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners", ast.Variable{ Value: config.NewStringList([]string{"red", "blue"}).String(), Type: ast.TypeString, }) // Zero elements testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing", ast.Variable{ Value: config.NewStringList([]string{}).String(), Type: ast.TypeString, }) // Zero + zero elements testInterpolate(t, i, scope, "aws_route53_zone.terra.*.nothing", ast.Variable{ Value: config.NewStringList([]string{"", ""}).String(), Type: ast.TypeString, }) // Zero + 1 element testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special", ast.Variable{ Value: config.NewStringList([]string{"extra"}).String(), Type: ast.TypeString, }) // Maps still need to work testInterpolate(t, i, scope, "aws_route53_zone.terra.0.tags.Name", ast.Variable{ Value: "reindeer", Type: ast.TypeString, }) // Maps still need to work in both testInterpolate(t, i, scope, "aws_route53_zone.terra.*.tags.Name", ast.Variable{ Value: config.NewStringList([]string{"reindeer", "white-hart"}).String(), Type: ast.TypeString, }) }
func TestInterpolator_resourceMultiAttributes(t *testing.T) { lock := new(sync.RWMutex) state := &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "aws_route53_zone.yada": &ResourceState{ Type: "aws_route53_zone", Dependencies: []string{}, Primary: &InstanceState{ ID: "AAABBBCCCDDDEEE", Attributes: map[string]string{ "name_servers.#": "4", "name_servers.0": "ns-1334.awsdns-38.org", "name_servers.1": "ns-1680.awsdns-18.co.uk", "name_servers.2": "ns-498.awsdns-62.com", "name_servers.3": "ns-601.awsdns-11.net", "listeners.#": "1", "listeners.0": "red", "tags.#": "1", "tags.Name": "reindeer", "nothing.#": "0", }, }, }, }, }, }, } i := &Interpolater{ Module: testModule(t, "interpolate-multi-vars"), StateLock: lock, State: state, } scope := &InterpolationScope{ Path: rootModulePath, } name_servers := []string{ "ns-1334.awsdns-38.org", "ns-1680.awsdns-18.co.uk", "ns-498.awsdns-62.com", "ns-601.awsdns-11.net", } expectedNameServers := config.NewStringList(name_servers).String() // More than 1 element testInterpolate(t, i, scope, "aws_route53_zone.yada.name_servers", ast.Variable{ Value: expectedNameServers, Type: ast.TypeString, }) // Exactly 1 element testInterpolate(t, i, scope, "aws_route53_zone.yada.listeners", ast.Variable{ Value: config.NewStringList([]string{"red"}).String(), Type: ast.TypeString, }) // Zero elements testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing", ast.Variable{ Value: config.NewStringList([]string{}).String(), Type: ast.TypeString, }) // Maps still need to work testInterpolate(t, i, scope, "aws_route53_zone.yada.tags.Name", ast.Variable{ Value: "reindeer", Type: ast.TypeString, }) }
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 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 } attr, ok := r.Primary.Attributes[v.Field] if !ok { // computed list attribute _, ok := r.Primary.Attributes[v.Field+".#"] if !ok { continue } attr, err = i.interpolateListAttribute(v.Field, r.Primary.Attributes) if err != nil { return "", err } } if config.IsStringList(attr) { for _, s := range config.StringList(attr).Slice() { values = append(values, s) } continue } // If any value is unknown, the whole thing is unknown if attr == config.UnknownVariableValue { return config.UnknownVariableValue, nil } values = append(values, attr) } 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 config.UnknownVariableValue, nil } return "", fmt.Errorf( "Resource '%s' does not have attribute '%s' "+ "for variable '%s'", v.ResourceId(), v.Field, v.FullKey()) } return config.NewStringList(values).String(), nil }
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 { // 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. if i.Operation == walkRefresh { return config.UnknownVariableValue, nil } return "", fmt.Errorf( "Resource '%s' does not have attribute '%s' "+ "for variable '%s'", v.ResourceId(), v.Field, v.FullKey()) } return config.NewStringList(values).String(), nil }