diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index f6d310761..b33919baf 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -2921,3 +2921,30 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { t.Fatalf("missing diff for data.aws_vpc.bar.1") } } + +// interpolated lists need to be stored in the original order. +func TestContext2Plan_listOrder(t *testing.T) { + m := testModule(t, "plan-list-order") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan() + if err != nil { + t.Fatalf("err: %s", err) + } + + rDiffs := plan.Diff.Modules[0].Resources + rDiffA := rDiffs["aws_instance.a"] + rDiffB := rDiffs["aws_instance.b"] + + if !rDiffA.Equal(rDiffB) { + t.Fatal("aws_instance.a and aws_instance.b diffs should match:\n", plan) + } +} diff --git a/terraform/context_test.go b/terraform/context_test.go index 977b9e578..91babd75f 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -210,10 +210,6 @@ func testDiffFn( } for k, v := range c.Raw { - if _, ok := v.(string); !ok { - continue - } - // Ignore __-prefixed keys since they're used for magic if k[0] == '_' && k[1] == '_' { continue diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 252fb7637..152e60971 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -620,6 +620,33 @@ func (i *Interpolater) computeResourceMultiVariable( return &variable, err } +type indexKeys []string + +// we need to separate the index integer from the ID, and sort numerically +func (i indexKeys) Less(j, k int) bool { + jDot := strings.LastIndex(i[j], ".") + kDot := strings.LastIndex(i[j], ".") + + // These should all be properly formatted, but check the indexes and return + // a safe value just in case. + if jDot < 0 || kDot < 0 { + return i[j] < i[k] + } + + jIdx, _ := strconv.Atoi(i[j][jDot+1:]) + kIdx, _ := strconv.Atoi(i[k][kDot+1:]) + + return jIdx < kIdx +} + +func (i indexKeys) Swap(j, k int) { + i[j], i[k] = i[k], i[j] +} + +func (i indexKeys) Len() int { + return len(i) +} + func (i *Interpolater) interpolateComplexTypeAttribute( resourceID string, attributes map[string]string) (ast.Variable, error) { @@ -648,7 +675,9 @@ func (i *Interpolater) interpolateComplexTypeAttribute( keys = append(keys, id) } } - sort.Strings(keys) + + // sort the keys by their index number, rather than lexicographically by the key + sort.Sort(indexKeys(keys)) var members []string for _, key := range keys { diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index 9613fc776..de2469211 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "sort" "sync" "testing" @@ -674,6 +675,68 @@ func TestInterpolater_selfVarWithoutResource(t *testing.T) { } } +// Verify sorting by key index number +func TestInterpolator_indexKeySort(t *testing.T) { + keys := []string{"a.1", "a.2", "a.10", "a.20", "a.3"} + sorted := []string{"a.1", "a.2", "a.3", "a.10", "a.20"} + + sort.Sort(indexKeys(keys)) + for i := range keys { + if keys[i] != sorted[i] { + t.Fatalf("indexes out of order\nexpected: %q\ngot: %q", sorted, keys) + } + } +} + +func TestInterpolator_interpolatedListOrder(t *testing.T) { + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_route53_zone.list": &ResourceState{ + Type: "aws_route53_zone", + Dependencies: []string{}, + Primary: &InstanceState{ + ID: "null", + Attributes: map[string]string{ + "foo.#": "12", + "foo.0": "a", + "foo.1": "b", + "foo.2": "c", + "foo.3": "d", + "foo.4": "e", + "foo.5": "f", + "foo.6": "g", + "foo.7": "h", + "foo.8": "i", + "foo.9": "j", + "foo.10": "k", + "foo.11": "l", + }, + }, + }, + }, + }, + }, + } + + i := &Interpolater{ + Module: testModule(t, "interpolate-multi-vars"), + StateLock: new(sync.RWMutex), + State: state, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + list := []interface{}{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"} + + testInterpolate(t, i, scope, "aws_route53_zone.list.foo", + interfaceToVariableSwallowError(list)) +} + func getInterpolaterFixture(t *testing.T) *Interpolater { lock := new(sync.RWMutex) state := &State{ diff --git a/terraform/test-fixtures/plan-list-order/main.tf b/terraform/test-fixtures/plan-list-order/main.tf new file mode 100644 index 000000000..77db3d059 --- /dev/null +++ b/terraform/test-fixtures/plan-list-order/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "a" { + foo = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20] +} + +resource "aws_instance" "b" { + foo = "${aws_instance.a.foo}" +}