Skip to content

Commit 09c366a

Browse files
authored
fix: sort api_token permission_groups to match v5 provider canonical ordering (#276)
The v5 provider returns permission_groups sorted alphabetically by ID, but the migrator preserved the original v4 ordering. This caused drift in E2E tests where the plan showed IDs swapped between what the config specified and what the provider expected.
1 parent 9c9f194 commit 09c366a

3 files changed

Lines changed: 34 additions & 30 deletions

File tree

integration/v4_to_v5/testdata/api_token/expected/api_token.tf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ resource "cloudflare_api_token" "basic_token" {
2727
"com.cloudflare.api.account.*" = "*"
2828
})
2929
permission_groups = [{
30-
id = "c8fed203ed3043cba015a93ad1616f1f"
31-
}, {
3230
id = "82e64a83756745bbbb1c9c2701bf816b"
31+
}, {
32+
id = "c8fed203ed3043cba015a93ad1616f1f"
3333
}]
3434
}]
3535
}
@@ -237,9 +237,9 @@ resource "cloudflare_api_token" "multi_perms_token" {
237237
"com.cloudflare.api.account.*" = "*"
238238
})
239239
permission_groups = [{
240-
id = "c8fed203ed3043cba015a93ad1616f1f"
241-
}, {
242240
id = "82e64a83756745bbbb1c9c2701bf816b"
241+
}, {
242+
id = "c8fed203ed3043cba015a93ad1616f1f"
243243
}]
244244
}]
245245
}

internal/resources/api_token/v4_to_v5.go

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api_token
22

33
import (
44
"regexp"
5+
"sort"
56

67
"github.com/cloudflare/tf-migrate/internal"
78
"github.com/cloudflare/tf-migrate/internal/transform"
@@ -95,6 +96,7 @@ func (m *V4ToV5Migrator) transformPolicyBlocks(body *hclwrite.Body) {
9596
// transformPermissionGroups converts permission_groups from list of strings to list of objects
9697
// v4: permission_groups = ["id1", "id2"]
9798
// v5: permission_groups = [{ id = "id1" }, { id = "id2" }]
99+
// The IDs are sorted alphabetically to match the v5 provider's canonical ordering.
98100
func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) {
99101
permGroupsAttr := body.GetAttribute("permission_groups")
100102
if permGroupsAttr == nil {
@@ -104,13 +106,9 @@ func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) {
104106
// Parse the existing list expression to extract the permission IDs
105107
exprTokens := permGroupsAttr.Expr().BuildTokens(nil)
106108

107-
// Build a list of objects where each string ID becomes { id = "..." }
108-
var permObjects []hclwrite.Tokens
109-
110-
// We need to manually parse the tokens to extract string values
111-
// For simplicity, we'll reconstruct the structure
109+
// Collect all permission group IDs first
110+
var permIDs []string
112111
inList := false
113-
var currentID string
114112

115113
for _, token := range exprTokens {
116114
switch token.Type {
@@ -120,26 +118,33 @@ func (m *V4ToV5Migrator) transformPermissionGroups(body *hclwrite.Body) {
120118
inList = false
121119
case hclsyntax.TokenQuotedLit:
122120
if inList {
123-
// Extract the ID from the quoted literal (remove quotes)
124-
currentID = string(token.Bytes)
125-
// Create an object: { id = "currentID" }
126-
objAttrs := []hclwrite.ObjectAttrTokens{
127-
{
128-
Name: hclwrite.TokensForIdentifier("id"),
129-
Value: hclwrite.TokensForValue(cty.StringVal(currentID)),
130-
},
131-
}
132-
permObjects = append(permObjects, hclwrite.TokensForObject(objAttrs))
121+
permIDs = append(permIDs, string(token.Bytes))
133122
}
134123
}
135124
}
136125

137-
// If we found any permission IDs, replace the attribute with the new format
138-
if len(permObjects) > 0 {
139-
body.RemoveAttribute("permission_groups")
140-
listTokens := hclwrite.TokensForTuple(permObjects)
141-
body.SetAttributeRaw("permission_groups", listTokens)
126+
if len(permIDs) == 0 {
127+
return
128+
}
129+
130+
// Sort IDs alphabetically to match the v5 provider's canonical ordering
131+
sort.Strings(permIDs)
132+
133+
// Build a list of objects where each string ID becomes { id = "..." }
134+
var permObjects []hclwrite.Tokens
135+
for _, id := range permIDs {
136+
objAttrs := []hclwrite.ObjectAttrTokens{
137+
{
138+
Name: hclwrite.TokensForIdentifier("id"),
139+
Value: hclwrite.TokensForValue(cty.StringVal(id)),
140+
},
141+
}
142+
permObjects = append(permObjects, hclwrite.TokensForObject(objAttrs))
142143
}
144+
145+
body.RemoveAttribute("permission_groups")
146+
listTokens := hclwrite.TokensForTuple(permObjects)
147+
body.SetAttributeRaw("permission_groups", listTokens)
143148
}
144149

145150
// transformResources wraps the resources map with jsonencode()
@@ -200,4 +205,3 @@ func (m *V4ToV5Migrator) transformConditionBlock(body *hclwrite.Body) {
200205
body.SetAttributeRaw("condition", conditionTokens)
201206
body.RemoveBlock(conditionBlock)
202207
}
203-

internal/resources/api_token/v4_to_v5_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ resource "cloudflare_api_token" "example" {
5959
"com.cloudflare.api.account.*" = "*"
6060
})
6161
permission_groups = [{
62-
id = "c8fed203ed3043cba015a93ad1616f1f"
63-
}, {
6462
id = "82e64a83756745bbbb1c9c2701bf816b"
63+
}, {
64+
id = "c8fed203ed3043cba015a93ad1616f1f"
6565
}]
6666
}]
6767
}`,
@@ -461,9 +461,9 @@ resource "cloudflare_api_token" "full_example" {
461461
"com.cloudflare.api.account.billing.*" = "read"
462462
})
463463
permission_groups = [{
464-
id = "c8fed203ed3043cba015a93ad1616f1f"
465-
}, {
466464
id = "82e64a83756745bbbb1c9c2701bf816b"
465+
}, {
466+
id = "c8fed203ed3043cba015a93ad1616f1f"
467467
}]
468468
}, {
469469
effect = "deny"

0 commit comments

Comments
 (0)