From e8ca1d926e508e85f4ef38c23e4c7eff3d76a2e0 Mon Sep 17 00:00:00 2001 From: Kyle Adams Date: Fri, 20 Mar 2026 09:55:01 -0700 Subject: [PATCH 1/4] add support for changes to computed arrays and maps --- array_attribute.go | 2 +- map_attribute.go | 2 +- parse_test.go | 65 ++++++++++++++++++++++++++++++++++++++ test/computedobject.stdout | 24 ++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 test/computedobject.stdout diff --git a/array_attribute.go b/array_attribute.go index 9751a0c..c8e9711 100644 --- a/array_attribute.go +++ b/array_attribute.go @@ -27,7 +27,7 @@ func IsArrayAttributeChangeLine(line string) bool { // IsArrayAttributeTerminator returns true if the line is "]" or "] -> null" func IsArrayAttributeTerminator(line string) bool { - return strings.TrimSuffix(strings.TrimSpace(line), " -> null") == "]" + return strings.TrimSuffix(strings.TrimSpace(line), " -> null") == "]" || strings.TrimSuffix(strings.TrimSpace(line), " -> (known after apply)") == "]" } // IsOneLineEmptyArrayAttribute returns true if the line ends with a "[]" diff --git a/map_attribute.go b/map_attribute.go index 7adc817..76fc14b 100644 --- a/map_attribute.go +++ b/map_attribute.go @@ -24,7 +24,7 @@ func IsMapAttributeChangeLine(line string) bool { // IsMapAttributeTerminator returns true if the line is a "}" or "}," func IsMapAttributeTerminator(line string) bool { - return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> null") == "}" + return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> null") == "}" || strings.TrimSuffix(strings.TrimSuffix(strings.TrimSpace(line), ","), " -> (known after apply)") == "}" } // IsOneLineEmptyMapAttribute returns true if the line ends with a "{}" diff --git a/parse_test.go b/parse_test.go index f42b1e1..15cf29f 100644 --- a/parse_test.go +++ b/parse_test.go @@ -242,6 +242,71 @@ func TestParse(t *testing.T) { }, }, }, + "computedmap": { + file: "test/computedobject.stdout", + expected: []*ResourceChange{ + &ResourceChange{ + Address: "module.my-project.google_project_services.gcp_enabled_services[0]", + ModuleAddress: "module.my-project", + Type: "google_project_services", + Name: "gcp_enabled_services", + Index: 0, + UpdateType: UpdateInPlaceResource, + AttributeChanges: []attributeChange{ + &AttributeChange{ + Name: "disable_on_destroy", + OldValue: false, + NewValue: true, + UpdateType: UpdateInPlaceResource, + }, + &AttributeChange{ + Name: "id", + OldValue: "my-project", + NewValue: "test", + UpdateType: UpdateInPlaceResource, + }, + &AttributeChange{ + Name: "project", + OldValue: "my-project", + NewValue: "test2", + UpdateType: UpdateInPlaceResource, + }, + &ArrayAttributeChange{ + Name: "computed_services", + AttributeChanges: []attributeChange{ + &AttributeChange{ + OldValue: "appengine.googleapis.com", + NewValue: nil, + UpdateType: DestroyResource, + }, + &AttributeChange{ + OldValue: "audit.googleapis.com", + NewValue: nil, + UpdateType: DestroyResource, + }, + }, + UpdateType: UpdateInPlaceResource, + }, + &MapAttributeChange{ + Name: "computed_tags", + AttributeChanges: []attributeChange{ + &AttributeChange{ + Name: "key1", + OldValue: "old", + NewValue: "new", + UpdateType: UpdateInPlaceResource, + }, + }, + UpdateType: UpdateInPlaceResource, + }, + &MapAttributeChange{ + Name: "timeouts", + UpdateType: UpdateInPlaceResource, + }, + }, + }, + }, + }, "nested map": { file: "test/nestedmap.stdout", expected: []*ResourceChange{ diff --git a/test/computedobject.stdout b/test/computedobject.stdout new file mode 100644 index 0000000..4c2f081 --- /dev/null +++ b/test/computedobject.stdout @@ -0,0 +1,24 @@ +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # module.my-project.google_project_services.gcp_enabled_services[0] will be updated in-place + ~ resource "google_project_services" "gcp_enabled_services" { + ~ disable_on_destroy = false -> true + ~ id = "my-project" -> "test" + ~ project = "my-project" -> "test2" + ~ computed_services = [ + - "appengine.googleapis.com", + - "audit.googleapis.com", + ] -> (known after apply) + ~ computed_tags = { + ~ key1 = "old" -> "new" + } -> (known after apply) + ~ timeouts {} + } + +Plan: 0 to add, 0 to change, 1 to destroy. \ No newline at end of file From 8752953cd07d8ca4106b6cc929711da0fe9446be Mon Sep 17 00:00:00 2001 From: Kyle Adams Date: Thu, 26 Mar 2026 08:34:36 -0700 Subject: [PATCH 2/4] fix parsing of force replace comment on maps and arrays --- array_attribute.go | 16 ++++-- array_attribute_test.go | 8 +++ attribute.go | 19 +++++-- attribute_test.go | 10 ++++ heredoc_attribute.go | 2 +- jsonencode.go | 2 +- map_attribute.go | 16 ++++-- map_attribute_test.go | 8 +++ parse_test.go | 109 +++++++++++++++++++++++++++++++++++++++ test/forcereplace.stdout | 38 ++++++++++++++ 10 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 test/forcereplace.stdout diff --git a/array_attribute.go b/array_attribute.go index 9751a0c..71359bc 100644 --- a/array_attribute.go +++ b/array_attribute.go @@ -18,10 +18,11 @@ var _ attributeChange = &ArrayAttributeChange{} // IsArrayAttributeChangeLine returns true if the line is a valid attribute change // This requires the line to start with "+", "-" or "~", not be followed with "resource" or "data", and ends with "[". +// Terraform may append ForcesReplacementComment after the opening bracket. func IsArrayAttributeChangeLine(line string) bool { line = strings.TrimSpace(line) - // validPrefix := strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "~") - validSuffix := strings.HasSuffix(line, "[") || IsOneLineEmptyArrayAttribute(line) + base := strings.TrimSpace(strings.TrimSuffix(line, ForcesReplacementComment)) + validSuffix := strings.HasSuffix(base, "[") || IsOneLineEmptyArrayAttribute(line) return validSuffix && !IsResourceChangeLine(line) } @@ -32,7 +33,9 @@ func IsArrayAttributeTerminator(line string) bool { // IsOneLineEmptyArrayAttribute returns true if the line ends with a "[]" func IsOneLineEmptyArrayAttribute(line string) bool { - return strings.HasSuffix(line, "[]") + line = strings.TrimSpace(line) + line = strings.TrimSuffix(line, ForcesReplacementComment) + return strings.HasSuffix(strings.TrimSpace(line), "[]") } // NewArrayAttributeChangeFromLine initializes an ArrayAttributeChange from a line containing an array attribute change @@ -57,10 +60,13 @@ func NewArrayAttributeChangeFromLine(line string) (*ArrayAttributeChange, error) UpdateType: DestroyResource, }, nil } else if strings.HasPrefix(line, "~") { - // replace + updateType := UpdateInPlaceResource + if strings.HasSuffix(strings.TrimSpace(line), ForcesReplacementComment) { + updateType = ForceReplaceResource + } return &ArrayAttributeChange{ Name: attributeName, - UpdateType: UpdateInPlaceResource, + UpdateType: updateType, }, nil } else { return &ArrayAttributeChange{ diff --git a/array_attribute_test.go b/array_attribute_test.go index bff3ea3..1d20abd 100644 --- a/array_attribute_test.go +++ b/array_attribute_test.go @@ -64,6 +64,14 @@ func TestNewArrayAttributeChangeFromLine(t *testing.T) { UpdateType: UpdateInPlaceResource, }, }, + "attribute changed forces replacement": { + line: `~ array_test = [` + ForcesReplacementComment, + shouldError: false, + expected: &ArrayAttributeChange{ + Name: "array_test", + UpdateType: ForceReplaceResource, + }, + }, "attribute is unchanged": { line: `attribute [`, shouldError: false, diff --git a/attribute.go b/attribute.go index 78f287c..7aa9038 100644 --- a/attribute.go +++ b/attribute.go @@ -11,6 +11,8 @@ const ( ATTRIBUTE_DEFINITON_DELIMITER = " = " SENSITIVE_VALUE = "(sensitive value)" COMPUTED_VALUE = "(known after apply)" + // ForcesReplacementComment is the suffix Terraform appends to attributes that trigger replace. + ForcesReplacementComment = " # forces replacement" ) type attributeChange interface { @@ -92,9 +94,9 @@ func NewAttributeChangeFromLine(line string) (*AttributeChange, error) { // replace updateType := UpdateInPlaceResource - if strings.HasSuffix(attribute[1], " # forces replacement") { + if strings.HasSuffix(attribute[1], ForcesReplacementComment) { updateType = ForceReplaceResource - attribute[1] = strings.TrimSuffix(attribute[1], " # forces replacement") + attribute[1] = strings.TrimSuffix(attribute[1], ForcesReplacementComment) } values := strings.Split(attribute[1], ATTRIBUTE_CHANGE_DELIMITER) @@ -112,11 +114,18 @@ func NewAttributeChangeFromLine(line string) (*AttributeChange, error) { UpdateType: updateType, }, nil } else { + valStr := strings.TrimSpace(attribute[1]) + updateType := NoOpResource + if strings.HasSuffix(valStr, ForcesReplacementComment) { + valStr = strings.TrimSpace(strings.TrimSuffix(valStr, ForcesReplacementComment)) + updateType = ForceReplaceResource + } + conv := doTypeConversion(valStr) return &AttributeChange{ Name: dequote(strings.TrimSpace(attribute[0])), - OldValue: doTypeConversion(attribute[1]), - NewValue: doTypeConversion(attribute[1]), - UpdateType: NoOpResource, + OldValue: conv, + NewValue: conv, + UpdateType: updateType, }, nil } } diff --git a/attribute_test.go b/attribute_test.go index f016e83..62f1fd1 100644 --- a/attribute_test.go +++ b/attribute_test.go @@ -171,6 +171,16 @@ func TestNewAttributeChangeFromLine(t *testing.T) { UpdateType: NoOpResource, }, }, + "unchanged attribute forces replacement": { + line: `id = "namespace-id"` + ForcesReplacementComment, + shouldError: false, + expected: &AttributeChange{ + Name: "id", + OldValue: "namespace-id", + NewValue: "namespace-id", + UpdateType: ForceReplaceResource, + }, + }, "resource line": { line: `+ resource "type" "name" {`, shouldError: true, diff --git a/heredoc_attribute.go b/heredoc_attribute.go index 045cfdc..0f49b32 100644 --- a/heredoc_attribute.go +++ b/heredoc_attribute.go @@ -65,7 +65,7 @@ func NewHeredocAttributeChangeFromLine(line string) (*HeredocAttributeChange, er } else if strings.HasPrefix(line, "~") { // replace updateType := UpdateInPlaceResource - if strings.HasSuffix(attribute[1], " # forces replacement") { + if strings.HasSuffix(attribute[1], ForcesReplacementComment) { updateType = ForceReplaceResource } diff --git a/jsonencode.go b/jsonencode.go index d8fea4c..f84ea01 100644 --- a/jsonencode.go +++ b/jsonencode.go @@ -57,7 +57,7 @@ func NewJSONEncodeAttributeChangeFromLine(line string) (*JSONEncodeAttributeChan } else if strings.HasPrefix(line, "~") { // replace updateType := UpdateInPlaceResource - if strings.HasSuffix(attribute[1], " # forces replacement") { + if strings.HasSuffix(attribute[1], ForcesReplacementComment) { updateType = ForceReplaceResource } diff --git a/map_attribute.go b/map_attribute.go index 7adc817..6e00648 100644 --- a/map_attribute.go +++ b/map_attribute.go @@ -15,10 +15,11 @@ var _ attributeChange = &MapAttributeChange{} // IsMapAttributeChangeLine returns true if the line is a valid attribute change // This requires the line to start with "+", "-" or "~", not be followed with "resource" or "data", and ends with "{". +// Terraform may append ForcesReplacementComment after the opening brace. func IsMapAttributeChangeLine(line string) bool { line = strings.TrimSpace(line) - // validPrefix := strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "~") - validSuffix := strings.HasSuffix(line, "{") || IsOneLineEmptyMapAttribute(line) + base := strings.TrimSpace(strings.TrimSuffix(line, ForcesReplacementComment)) + validSuffix := strings.HasSuffix(base, "{") || IsOneLineEmptyMapAttribute(line) return validSuffix && !IsResourceChangeLine(line) } @@ -29,7 +30,9 @@ func IsMapAttributeTerminator(line string) bool { // IsOneLineEmptyMapAttribute returns true if the line ends with a "{}" func IsOneLineEmptyMapAttribute(line string) bool { - return strings.HasSuffix(line, "{}") + line = strings.TrimSpace(line) + line = strings.TrimSuffix(line, ForcesReplacementComment) + return strings.HasSuffix(strings.TrimSpace(line), "{}") } // NewMapAttributeChangeFromLine initializes an AttributeChange from a line containing an attribute change @@ -54,10 +57,13 @@ func NewMapAttributeChangeFromLine(line string) (*MapAttributeChange, error) { UpdateType: DestroyResource, }, nil } else if strings.HasPrefix(line, "~") { - // replace + updateType := UpdateInPlaceResource + if strings.HasSuffix(strings.TrimSpace(line), ForcesReplacementComment) { + updateType = ForceReplaceResource + } return &MapAttributeChange{ Name: attributeName, - UpdateType: UpdateInPlaceResource, + UpdateType: updateType, }, nil } else { return &MapAttributeChange{ diff --git a/map_attribute_test.go b/map_attribute_test.go index d388cff..0e1d20d 100644 --- a/map_attribute_test.go +++ b/map_attribute_test.go @@ -64,6 +64,14 @@ func TestNewMapAttributeChangeFromLine(t *testing.T) { UpdateType: UpdateInPlaceResource, }, }, + "attribute changed forces replacement": { + line: `~ triggers = {` + ForcesReplacementComment, + shouldError: false, + expected: &MapAttributeChange{ + Name: "triggers", + UpdateType: ForceReplaceResource, + }, + }, "attribute is unchanged": { line: `attribute {`, shouldError: false, diff --git a/parse_test.go b/parse_test.go index f42b1e1..e008eda 100644 --- a/parse_test.go +++ b/parse_test.go @@ -575,6 +575,115 @@ func TestParse(t *testing.T) { }, }, }, + "forcereplace": { + file: "test/forcereplace.stdout", + expected: []*ResourceChange{ + &ResourceChange{ + Address: "module.mymodule.kubernetes_namespace.mynamespace", + ModuleAddress: "module.mymodule", + Type: "kubernetes_namespace", + Name: "mynamespace", + UpdateType: ForceReplaceResource, + AttributeChanges: []attributeChange{ + &AttributeChange{ + Name: "id", + OldValue: "namespace-id", + NewValue: "namespace-id", + UpdateType: ForceReplaceResource, + }, + &MapAttributeChange{ + Name: "metadata", + AttributeChanges: []attributeChange{ + &MapAttributeChange{ + Name: "annotations", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "generation", + OldValue: 0, + NewValue: 0, + UpdateType: NoOpResource, + }, + &MapAttributeChange{ + Name: "labels", + AttributeChanges: []attributeChange{ + &AttributeChange{ + Name: "label", + OldValue: "value", + NewValue: "value", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "other", + OldValue: "label", + NewValue: "label", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "newLabel", + OldValue: nil, + NewValue: "newLabel", + UpdateType: NewResource, + }, + }, + UpdateType: UpdateInPlaceResource, + }, + &AttributeChange{ + Name: "name", + OldValue: "my-namespace", + NewValue: "my-namespace", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "resource_version", + OldValue: "123", + NewValue: "123", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "self_link", + OldValue: "/api/v1/namespaces/my-namespace", + NewValue: "/api/v1/namespaces/my-namespace", + UpdateType: NoOpResource, + }, + &AttributeChange{ + Name: "uid", + OldValue: "some-uid-123", + NewValue: "some-uid-123", + UpdateType: NoOpResource, + }, + }, + UpdateType: ForceReplaceResource, + }, + &ArrayAttributeChange{ + Name: "array_test", + AttributeChanges: []attributeChange{ + &AttributeChange{ + OldValue: nil, + NewValue: "entry1", + UpdateType: NewResource, + }, + &AttributeChange{ + OldValue: "entry2", + NewValue: nil, + UpdateType: DestroyResource, + }, + }, + UpdateType: ForceReplaceResource, + }, + &ArrayAttributeChange{ + Name: "array_test2", + AttributeChanges: nil, + UpdateType: ForceReplaceResource, + }, + &MapAttributeChange{ + Name: "timeouts", + UpdateType: NoOpResource, + }, + }, + }, + }, + }, "jsonencode": { file: "test/jsonencode.stdout", expected: []*ResourceChange{ diff --git a/test/forcereplace.stdout b/test/forcereplace.stdout new file mode 100644 index 0000000..ff10ace --- /dev/null +++ b/test/forcereplace.stdout @@ -0,0 +1,38 @@ +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # module.mymodule.kubernetes_namespace.mynamespace must be replaced +-/+ resource "kubernetes_namespace" "mynamespace" { + id = "namespace-id" # forces replacement + + ~ metadata { # forces replacement + annotations = {} + generation = 0 + ~ labels = { + "label" = "value" + "other" = "label" + + "newLabel" = "newLabel" + # (5 unchanged elements hidden) + } + name = "my-namespace" + resource_version = "123" + self_link = "/api/v1/namespaces/my-namespace" + uid = "some-uid-123" + # (8 unchanged attributes hidden) + } + ~ array_test = [ # forces replacement + + "entry1", + - "entry2", + ] + ~ array_test2 = null -> [] # forces replacement + + timeouts {} + # (1 unchanged block hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. From 5525c795f127fc407c55380c29292e0895300e24 Mon Sep 17 00:00:00 2001 From: Kyle Adams Date: Fri, 3 Apr 2026 07:59:44 -0700 Subject: [PATCH 3/4] fix double index parsing --- parse_test.go | 61 +++++++++++++++++++++++++++++++++++ resource.go | 67 +++++++++++++++++++++++++++++---------- resource_test.go | 46 +++++++++++++++++++++++++++ test/doubleindexed.stdout | 18 +++++++++++ 4 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 test/doubleindexed.stdout diff --git a/parse_test.go b/parse_test.go index 632be92..d1e96b8 100644 --- a/parse_test.go +++ b/parse_test.go @@ -73,6 +73,12 @@ func TestParse(t *testing.T) { Type: "github_team_membership", Name: "member", Index: 1, + FullIndex: []ResourceIndex{ + { + Index: 1, + Address: "module.my-module.github_team_membership.member", + }, + }, UpdateType: DestroyResource, AttributeChanges: []attributeChange{ &AttributeChange{ @@ -113,6 +119,12 @@ func TestParse(t *testing.T) { Type: "github_team_membership", Name: "member", Index: 2, + FullIndex: []ResourceIndex{ + { + Index: 2, + Address: "module.my-module.github_team_membership.member", + }, + }, UpdateType: DestroyResource, AttributeChanges: []attributeChange{ &AttributeChange{ @@ -153,6 +165,12 @@ func TestParse(t *testing.T) { Type: "github_team_membership", Name: "member", Index: 3, + FullIndex: []ResourceIndex{ + { + Index: 3, + Address: "module.my-module.github_team_membership.member", + }, + }, UpdateType: DestroyResource, AttributeChanges: []attributeChange{ &AttributeChange{ @@ -198,6 +216,12 @@ func TestParse(t *testing.T) { Type: "google_project_services", Name: "gcp_enabled_services", Index: 0, + FullIndex: []ResourceIndex{ + { + Index: 0, + Address: "module.my-project.google_project_services.gcp_enabled_services", + }, + }, UpdateType: DestroyResource, AttributeChanges: []attributeChange{ &AttributeChange{ @@ -242,6 +266,37 @@ func TestParse(t *testing.T) { }, }, }, + "doubleindexed": { + file: "test/doubleindexed.stdout", + expected: []*ResourceChange{ + &ResourceChange{ + Address: "module.mymodule.module.submodule[0].data.subsubmodule.this[0]", + ModuleAddress: "module.mymodule", + Type: "subsubmodule", + Name: "this", + Index: 0, + FullIndex: []ResourceIndex{ + { + Index: 0, + Address: "module.mymodule.module.submodule", + }, + { + Index: 0, + Address: "data.subsubmodule.this", + }, + }, + UpdateType: ReadResource, + AttributeChanges: []attributeChange{ + &AttributeChange{ + Name: "dummy", + OldValue: nil, + NewValue: "(known after apply)", + UpdateType: NewResource, + }, + }, + }, + }, + }, "computedmap": { file: "test/computedobject.stdout", expected: []*ResourceChange{ @@ -251,6 +306,12 @@ func TestParse(t *testing.T) { Type: "google_project_services", Name: "gcp_enabled_services", Index: 0, + FullIndex: []ResourceIndex{ + { + Index: 0, + Address: "module.my-project.google_project_services.gcp_enabled_services", + }, + }, UpdateType: UpdateInPlaceResource, AttributeChanges: []attributeChange{ &AttributeChange{ diff --git a/resource.go b/resource.go index 3821e7b..827b9ba 100644 --- a/resource.go +++ b/resource.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "regexp" ) const ( @@ -16,6 +17,11 @@ const ( RESOURCE_DESTROYED = " will be destroyed" ) +type ResourceIndex struct { + Index interface{} + Address string +} + type ResourceChange struct { // Address contains the absolute resource address Address string @@ -33,8 +39,12 @@ type ResourceChange struct { // The index key for resources created with "count" or "for_each" // "count" resources will be an int index, and "for_each" will be a string + // This will only contain the final index, if you need the full index path, look at FullIndex Index interface{} + // The full index path for resources created with "count" or "for_each" + FullIndex []ResourceIndex + // UpdateType contains the type of update // Refer to updatetype.go for possible values UpdateType UpdateType @@ -150,26 +160,51 @@ func NewResourceChangeFromComment(comment string) (*ResourceChange, error) { return rc, nil } -func (rc *ResourceChange) finalizeResourceInfo() error { - var address string +// extractIndexes Extracts the final index from a resource path, along with any indexes on parent resources +func extractIndexes(address string) []ResourceIndex { + getAllIndexs := regexp.MustCompile(`([^\[]*)(\[([^\]]*)\])?`) + matches := getAllIndexs.FindAllStringSubmatch(address, -1) + allIndexes := []ResourceIndex{} + //fmt.Println("matches", address, matches) + + hasIndexes := false + for _, match := range matches { + var realIndex interface{} + if len(match) > 2 && match[3] != "" { + if i, err := strconv.Atoi(match[3]); err == nil { + realIndex = i + } else { + realIndex = strings.Trim(strings.Trim(match[3], "\""), "'") + } + hasIndexes = true + } + allIndexes = append(allIndexes, ResourceIndex{ + Index: realIndex, + Address: strings.TrimPrefix(match[1], "."), + }) + } + if !hasIndexes { + return []ResourceIndex{} + } + return allIndexes +} +func (rc *ResourceChange) finalizeResourceInfo() error { // parse index first in case the index contains a "." - addressIndex := strings.Split(rc.Address, "[") - address = addressIndex[0] - - if len(addressIndex) == 2 { - index := dequote(strings.TrimSuffix(addressIndex[1], "]")) - - if i, err := strconv.Atoi(index); err == nil { - rc.Index = i - } else { - rc.Index = index + allIndexes := extractIndexes(rc.Address) + values := []string{} + if len(allIndexes) > 0 { + rc.Index = allIndexes[len(allIndexes)-1].Index + rc.FullIndex = allIndexes + for _, index := range allIndexes { + for _, part := range strings.Split(index.Address, ".") { + values = append(values, strings.TrimSuffix(strings.TrimPrefix(part, "."), ".")) + } } - } else if len(addressIndex) > 2 { - return fmt.Errorf("failed to parse resource info from address %s", rc.Address) + } else { + values = strings.Split(rc.Address, ".") } - - values := strings.Split(address, ".") + fmt.Println("values", rc.Address, values) // TODO: handle module.module_name.data.type.name better // TODO: eventually do something with "data" diff --git a/resource_test.go b/resource_test.go index abd24ca..fbe6edb 100644 --- a/resource_test.go +++ b/resource_test.go @@ -177,6 +177,12 @@ func TestNewResourceChangeFromComment(t *testing.T) { Type: "resource", Name: "path", Index: "index", + FullIndex: []ResourceIndex{ + { + Index: "index", + Address: "module.mymodule.resource.path", + }, + }, UpdateType: NewResource, }, }, @@ -189,6 +195,12 @@ func TestNewResourceChangeFromComment(t *testing.T) { Type: "resource", Name: "path", Index: "index@test.com", + FullIndex: []ResourceIndex{ + { + Index: "index@test.com", + Address: "module.mymodule.resource.path", + }, + }, UpdateType: NewResource, }, }, @@ -201,6 +213,12 @@ func TestNewResourceChangeFromComment(t *testing.T) { Type: "resource", Name: "path", Index: 1, + FullIndex: []ResourceIndex{ + { + Index: 1, + Address: "module.mymodule.resource.path", + }, + }, UpdateType: NewResource, }, }, @@ -224,6 +242,34 @@ func TestNewResourceChangeFromComment(t *testing.T) { Type: "mydata", Name: "path", Index: 0, + FullIndex: []ResourceIndex{ + { + Index: 0, + Address: "module.mymodule.data.mydata.path", + }, + }, + UpdateType: ReadResource, + }, + }, + "handles modules with data and two indexes": { + line: " # module.mymodule[2].data.mydata.path[0] will be read during apply", + shouldError: false, + expected: &ResourceChange{ + Address: "module.mymodule[2].data.mydata.path[0]", + ModuleAddress: "module.mymodule", + Type: "mydata", + Name: "path", + Index: 0, + FullIndex: []ResourceIndex{ + { + Index: 2, + Address: "module.mymodule", + }, + { + Index: 0, + Address: "data.mydata.path", + }, + }, UpdateType: ReadResource, }, }, diff --git a/test/doubleindexed.stdout b/test/doubleindexed.stdout new file mode 100644 index 0000000..80ab9dc --- /dev/null +++ b/test/doubleindexed.stdout @@ -0,0 +1,18 @@ + +------------------------------------------------------------------------ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + <= read (data resources) + +Terraform will perform the following actions: + + # module.mymodule.module.submodule[0].data.subsubmodule.this[0] will be read during apply + # (config refers to values not yet known) + <= data "double_nested_submodule_data_source" "this" { + + dummy = (known after apply) + } + +Plan: 0 to add, 0 to change, 0 to destroy. + + From 3e8969748f495727e8544e48a96b516ddaa82d97 Mon Sep 17 00:00:00 2001 From: Kyle Adams Date: Fri, 3 Apr 2026 10:45:57 -0700 Subject: [PATCH 4/4] remove debug statement --- resource.go | 1 - 1 file changed, 1 deletion(-) diff --git a/resource.go b/resource.go index 827b9ba..1b1044d 100644 --- a/resource.go +++ b/resource.go @@ -204,7 +204,6 @@ func (rc *ResourceChange) finalizeResourceInfo() error { } else { values = strings.Split(rc.Address, ".") } - fmt.Println("values", rc.Address, values) // TODO: handle module.module_name.data.type.name better // TODO: eventually do something with "data"