diff --git a/integration/v4_to_v5/testdata/api_token/expected/api_token.tf b/integration/v4_to_v5/testdata/api_token/expected/api_token.tf index cb7a6a1a..208885c6 100644 --- a/integration/v4_to_v5/testdata/api_token/expected/api_token.tf +++ b/integration/v4_to_v5/testdata/api_token/expected/api_token.tf @@ -27,9 +27,9 @@ resource "cloudflare_api_token" "basic_token" { "com.cloudflare.api.account.*" = "*" }) permission_groups = [{ - id = "c8fed203ed3043cba015a93ad1616f1f" - }, { id = "82e64a83756745bbbb1c9c2701bf816b" + }, { + id = "c8fed203ed3043cba015a93ad1616f1f" }] }] } @@ -237,9 +237,9 @@ resource "cloudflare_api_token" "multi_perms_token" { "com.cloudflare.api.account.*" = "*" }) permission_groups = [{ - id = "c8fed203ed3043cba015a93ad1616f1f" - }, { id = "82e64a83756745bbbb1c9c2701bf816b" + }, { + id = "c8fed203ed3043cba015a93ad1616f1f" }] }] } diff --git a/internal/resources/api_token/v4_to_v5.go b/internal/resources/api_token/v4_to_v5.go index e8b27513..54c823f9 100644 --- a/internal/resources/api_token/v4_to_v5.go +++ b/internal/resources/api_token/v4_to_v5.go @@ -2,6 +2,7 @@ package api_token import ( "regexp" + "sort" "github.com/cloudflare/tf-migrate/internal" "github.com/cloudflare/tf-migrate/internal/transform" @@ -95,6 +96,7 @@ func (m *V4ToV5Migrator) transformPolicyBlocks(body *hclwrite.Body) { // transformPermissionGroups converts permission_groups from list of strings to list of objects // v4: permission_groups = ["id1", "id2"] // v5: permission_groups = [{ id = "id1" }, { id = "id2" }] +// The IDs are sorted alphabetically to match the v5 provider's canonical ordering. func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) { permGroupsAttr := body.GetAttribute("permission_groups") if permGroupsAttr == nil { @@ -104,13 +106,9 @@ func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) { // Parse the existing list expression to extract the permission IDs exprTokens := permGroupsAttr.Expr().BuildTokens(nil) - // Build a list of objects where each string ID becomes { id = "..." } - var permObjects []hclwrite.Tokens - - // We need to manually parse the tokens to extract string values - // For simplicity, we'll reconstruct the structure + // Collect all permission group IDs first + var permIDs []string inList := false - var currentID string for _, token := range exprTokens { switch token.Type { @@ -120,26 +118,33 @@ func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) { inList = false case hclsyntax.TokenQuotedLit: if inList { - // Extract the ID from the quoted literal (remove quotes) - currentID = string(token.Bytes) - // Create an object: { id = "currentID" } - objAttrs := []hclwrite.ObjectAttrTokens{ - { - Name: hclwrite.TokensForIdentifier("id"), - Value: hclwrite.TokensForValue(cty.StringVal(currentID)), - }, - } - permObjects = append(permObjects, hclwrite.TokensForObject(objAttrs)) + permIDs = append(permIDs, string(token.Bytes)) } } } - // If we found any permission IDs, replace the attribute with the new format - if len(permObjects) > 0 { - body.RemoveAttribute("permission_groups") - listTokens := hclwrite.TokensForTuple(permObjects) - body.SetAttributeRaw("permission_groups", listTokens) + if len(permIDs) == 0 { + return + } + + // Sort IDs alphabetically to match the v5 provider's canonical ordering + sort.Strings(permIDs) + + // Build a list of objects where each string ID becomes { id = "..." } + var permObjects []hclwrite.Tokens + for _, id := range permIDs { + objAttrs := []hclwrite.ObjectAttrTokens{ + { + Name: hclwrite.TokensForIdentifier("id"), + Value: hclwrite.TokensForValue(cty.StringVal(id)), + }, + } + permObjects = append(permObjects, hclwrite.TokensForObject(objAttrs)) } + + body.RemoveAttribute("permission_groups") + listTokens := hclwrite.TokensForTuple(permObjects) + body.SetAttributeRaw("permission_groups", listTokens) } // transformResources wraps the resources map with jsonencode() @@ -200,4 +205,3 @@ func (m *V4ToV5Migrator) transformConditionBlock(body *hclwrite.Body) { body.SetAttributeRaw("condition", conditionTokens) body.RemoveBlock(conditionBlock) } - diff --git a/internal/resources/api_token/v4_to_v5_test.go b/internal/resources/api_token/v4_to_v5_test.go index 9f6208d3..fa775622 100644 --- a/internal/resources/api_token/v4_to_v5_test.go +++ b/internal/resources/api_token/v4_to_v5_test.go @@ -59,9 +59,9 @@ resource "cloudflare_api_token" "example" { "com.cloudflare.api.account.*" = "*" }) permission_groups = [{ - id = "c8fed203ed3043cba015a93ad1616f1f" - }, { id = "82e64a83756745bbbb1c9c2701bf816b" + }, { + id = "c8fed203ed3043cba015a93ad1616f1f" }] }] }`, @@ -461,9 +461,9 @@ resource "cloudflare_api_token" "full_example" { "com.cloudflare.api.account.billing.*" = "read" }) permission_groups = [{ - id = "c8fed203ed3043cba015a93ad1616f1f" - }, { id = "82e64a83756745bbbb1c9c2701bf816b" + }, { + id = "c8fed203ed3043cba015a93ad1616f1f" }] }, { effect = "deny"