diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 15b51fcb2f0c..202e591774c2 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -2,7 +2,9 @@ package terraform import ( "fmt" + "log" "os" + "strconv" "strings" "sync" @@ -327,6 +329,11 @@ func (i *Interpolater) computeResourceVariable( return attr, nil } + // computed list attribute + if _, ok := r.Primary.Attributes[v.Field+".#"]; ok { + return i.interpolateListAttribute(v.Field, r.Primary.Attributes) + } + // 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. @@ -410,8 +417,8 @@ func (i *Interpolater) computeResourceMultiVariable( } var values []string - for i := 0; i < count; i++ { - id := fmt.Sprintf("%s.%d", v.ResourceId(), i) + 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. @@ -430,7 +437,15 @@ func (i *Interpolater) computeResourceMultiVariable( attr, ok := r.Primary.Attributes[v.Field] if !ok { - continue + // 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 + } } values = append(values, attr) @@ -448,6 +463,33 @@ func (i *Interpolater) computeResourceMultiVariable( return strings.Join(values, config.InterpSplitDelim), 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) + + value, err := strconv.ParseInt(attr, 0, 0) + if err != nil { + return "", err + } + + count := int(value) + if count == 0 { + return "", nil + } + + var members []string + for i := 0; i < count; i++ { + id := fmt.Sprintf("%s.%d", resourceID, i) + members = append(members, attributes[id]) + } + + return strings.Join(members, config.InterpSplitDelim), nil +} + func (i *Interpolater) resourceVariableInfo( scope *InterpolationScope, v *config.ResourceVariable) (*ModuleState, *config.Resource, error) { diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index 52896a54b8b6..f43679d737d6 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "strings" "sync" "testing" @@ -136,6 +137,208 @@ func TestInterpolater_pathRoot(t *testing.T) { }) } +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 := strings.Join(name_servers, config.InterpSplitDelim) + + // 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: "red", + Type: ast.TypeString, + }) + + // Zero elements + testInterpolate(t, i, scope, "aws_route53_zone.yada.nothing", ast.Variable{ + Value: "", + 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 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-999.awsdns-62.com", + "ns-666.awsdns-11.net", + } + + // More than 1 element + expectedNameServers := strings.Join(name_servers[0:4], config.InterpSplitDelim) + 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 = strings.Join(name_servers[0:8], config.InterpSplitDelim) + 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: "red", + Type: ast.TypeString, + }) + // Exactly 1 element in both + testInterpolate(t, i, scope, "aws_route53_zone.terra.*.listeners", ast.Variable{ + Value: strings.Join([]string{"red", "blue"}, config.InterpSplitDelim), + Type: ast.TypeString, + }) + + // Zero elements + testInterpolate(t, i, scope, "aws_route53_zone.terra.0.nothing", ast.Variable{ + Value: "", + Type: ast.TypeString, + }) + // Zero + zero elements + testInterpolate(t, i, scope, "aws_route53_zone.terra.*.nothing", ast.Variable{ + Value: config.InterpSplitDelim, // Not sure if this is desired behaviour? + Type: ast.TypeString, + }) + // Zero + 1 element + testInterpolate(t, i, scope, "aws_route53_zone.terra.*.special", ast.Variable{ + Value: "extra", + 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: strings.Join([]string{"reindeer", "white-hart"}, config.InterpSplitDelim), + Type: ast.TypeString, + }) +} + +func getInterpolaterFixture(t *testing.T) *Interpolater { + lock := new(sync.RWMutex) + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_route53_zone.terra.0": &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", + }, + }, + }, + "aws_route53_zone.terra.1": &ResourceState{ + Type: "aws_route53_zone", + Dependencies: []string{}, + Primary: &InstanceState{ + ID: "EEEFFFGGGHHHIII", + Attributes: map[string]string{ + "name_servers.#": "4", + "name_servers.0": "ns-000.awsdns-38.org", + "name_servers.1": "ns-444.awsdns-18.co.uk", + "name_servers.2": "ns-999.awsdns-62.com", + "name_servers.3": "ns-666.awsdns-11.net", + "listeners.#": "1", + "listeners.0": "blue", + "special.#": "1", + "special.0": "extra", + "tags.#": "1", + "tags.Name": "white-hart", + "nothing.#": "0", + }, + }, + }, + }, + }, + }, + } + + return &Interpolater{ + Module: testModule(t, "interpolate-multi-vars"), + StateLock: lock, + State: state, + } +} + func testInterpolate( t *testing.T, i *Interpolater, scope *InterpolationScope, @@ -156,6 +359,6 @@ func testInterpolate( "foo": expectedVar, } if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) + t.Fatalf("actual: %#v\nexpected: %#v", actual, expected) } } diff --git a/terraform/test-fixtures/interpolate-multi-vars/main.tf b/terraform/test-fixtures/interpolate-multi-vars/main.tf new file mode 100644 index 000000000000..b24d02f98fba --- /dev/null +++ b/terraform/test-fixtures/interpolate-multi-vars/main.tf @@ -0,0 +1,7 @@ +resource "aws_route53_zone" "yada" { + +} + +resource "aws_route53_zone" "terra" { + count = 2 +}