diff --git a/integration/v4_to_v5/testdata/zero_trust_access_group/expected/zero_trust_access_group.tf b/integration/v4_to_v5/testdata/zero_trust_access_group/expected/zero_trust_access_group.tf index fdff33b5..7b810340 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_group/expected/zero_trust_access_group.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_group/expected/zero_trust_access_group.tf @@ -35,6 +35,9 @@ locals { + + + @@ -662,3 +665,63 @@ moved { from = cloudflare_access_group.auth_context to = cloudflare_zero_trust_access_group.auth_context } + +# Pattern 25: Azure block with list id (APIX-1110) +resource "cloudflare_zero_trust_access_group" "azure_list_id" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Azure List ID Group" + + include = [ + { + azure_ad = { + identity_provider_id = "idp-123" + id = "group-id-1" + } + }, + ] +} + +moved { + from = cloudflare_access_group.azure_list_id + to = cloudflare_zero_trust_access_group.azure_list_id +} + +# Pattern 26: GSuite block with list email +resource "cloudflare_zero_trust_access_group" "gsuite_list_email" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} GSuite List Email Group" + + include = [ + { + gsuite = { + email = "group@example.com" + identity_provider_id = "idp-123" + } + }, + ] +} + +moved { + from = cloudflare_access_group.gsuite_list_email + to = cloudflare_zero_trust_access_group.gsuite_list_email +} + +# Pattern 27: Okta block with list name +resource "cloudflare_zero_trust_access_group" "okta_list_name" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Okta List Name Group" + + include = [ + { + okta = { + name = "okta-group-1" + identity_provider_id = "idp-123" + } + }, + ] +} + +moved { + from = cloudflare_access_group.okta_list_name + to = cloudflare_zero_trust_access_group.okta_list_name +} diff --git a/integration/v4_to_v5/testdata/zero_trust_access_group/input/zero_trust_access_group.tf b/integration/v4_to_v5/testdata/zero_trust_access_group/input/zero_trust_access_group.tf index afca460f..823e583f 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_group/input/zero_trust_access_group.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_group/input/zero_trust_access_group.tf @@ -297,6 +297,45 @@ resource "cloudflare_access_group" "auth_context" { } } +# Pattern 25: Azure block with list id (APIX-1110) +resource "cloudflare_access_group" "azure_list_id" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Azure List ID Group" + + include { + azure { + identity_provider_id = "idp-123" + id = ["group-id-1"] + } + } +} + +# Pattern 26: GSuite block with list email +resource "cloudflare_access_group" "gsuite_list_email" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} GSuite List Email Group" + + include { + gsuite { + email = ["group@example.com"] + identity_provider_id = "idp-123" + } + } +} + +# Pattern 27: Okta block with list name +resource "cloudflare_access_group" "okta_list_name" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Okta List Name Group" + + include { + okta { + name = ["okta-group-1"] + identity_provider_id = "idp-123" + } + } +} + # Pattern 23: Already-renamed v4 resource (exercises UpgradeState path, not MoveState) # When the v4 config already uses cloudflare_zero_trust_access_group (the newer v4 name), # tf-migrate does NOT generate a moved {} block. During v5 apply, Terraform triggers diff --git a/internal/resources/zero_trust_access_group/v4_to_v5.go b/internal/resources/zero_trust_access_group/v4_to_v5.go index 4b58e594..4512d445 100644 --- a/internal/resources/zero_trust_access_group/v4_to_v5.go +++ b/internal/resources/zero_trust_access_group/v4_to_v5.go @@ -1612,8 +1612,9 @@ func (m *V4ToV5Migrator) buildGithubOrgItems(nameExpr, teamExpr, identityProvide } // expandGsuite handles gsuite blocks -// gsuite = [{email = "group@example.com", identity_provider_id = "id"}] +// gsuite = [{email = ["group@example.com"], identity_provider_id = "id"}] // -> {gsuite = {email = "group@example.com", identity_provider_id = "id"}} +// Note: in v4, gsuite.email is a list; in v5, gsuite.email is a string. func (m *V4ToV5Migrator) expandGsuite(item hclsyntax.ObjectConsItem) []hclsyntax.Expression { tup, ok := item.ValueExpr.(*hclsyntax.TupleConsExpr) if !ok { @@ -1628,6 +1629,10 @@ func (m *V4ToV5Migrator) expandGsuite(item hclsyntax.ObjectConsItem) []hclsyntax continue } + // Unwrap email from list to scalar: email = [value] -> email = value + // In v4, gsuite.email is a list of strings; in v5, gsuite.email is a string + gsuiteObj = m.unwrapSingleElementList(gsuiteObj, "email") + newObj := &hclsyntax.ObjectConsExpr{ Items: []hclsyntax.ObjectConsItem{ { @@ -1643,8 +1648,9 @@ func (m *V4ToV5Migrator) expandGsuite(item hclsyntax.ObjectConsItem) []hclsyntax } // expandOkta handles okta blocks -// okta = [{name = "group", identity_provider_id = "id"}] +// okta = [{name = ["group"], identity_provider_id = "id"}] // -> {okta = {name = "group", identity_provider_id = "id"}} +// Note: in v4, okta.name is a list; in v5, okta.name is a string. func (m *V4ToV5Migrator) expandOkta(item hclsyntax.ObjectConsItem) []hclsyntax.Expression { tup, ok := item.ValueExpr.(*hclsyntax.TupleConsExpr) if !ok { @@ -1659,6 +1665,10 @@ func (m *V4ToV5Migrator) expandOkta(item hclsyntax.ObjectConsItem) []hclsyntax.E continue } + // Unwrap name from list to scalar: name = [value] -> name = value + // In v4, okta.name is a list of strings; in v5, okta.name is a string + oktaObj = m.unwrapSingleElementList(oktaObj, "name") + newObj := &hclsyntax.ObjectConsExpr{ Items: []hclsyntax.ObjectConsItem{ { @@ -1767,8 +1777,9 @@ func (m *V4ToV5Migrator) expandAuthContext(item hclsyntax.ObjectConsItem) []hcls } // expandAzure handles azure blocks -// azure = [{id = "group-id", identity_provider_id = "id"}] +// azure = [{id = ["group-id"], identity_provider_id = "id"}] // -> {azure_ad = {id = "group-id", identity_provider_id = "id"}} +// Note: in v4, azure.id is a list; in v5, azure_ad.id is a string. func (m *V4ToV5Migrator) expandAzure(item hclsyntax.ObjectConsItem) []hclsyntax.Expression { tup, ok := item.ValueExpr.(*hclsyntax.TupleConsExpr) if !ok { @@ -1783,6 +1794,10 @@ func (m *V4ToV5Migrator) expandAzure(item hclsyntax.ObjectConsItem) []hclsyntax. continue } + // Unwrap id from list to scalar: id = [value] -> id = value + // In v4, azure.id is a list of strings; in v5, azure_ad.id is a string + azureObj = m.unwrapSingleElementList(azureObj, "id") + // Rename azure to azure_ad for v5 newObj := &hclsyntax.ObjectConsExpr{ Items: []hclsyntax.ObjectConsItem{ @@ -1800,6 +1815,32 @@ func (m *V4ToV5Migrator) expandAzure(item hclsyntax.ObjectConsItem) []hclsyntax. // Helper functions +// unwrapSingleElementList unwraps a single-element list attribute to a scalar value. +// For example: id = [value] -> id = value +// This is needed for azure.id, gsuite.email, okta.name which are lists in v4 but +// strings in v5. +func (m *V4ToV5Migrator) unwrapSingleElementList(obj *hclsyntax.ObjectConsExpr, attrName string) *hclsyntax.ObjectConsExpr { + var newItems []hclsyntax.ObjectConsItem + for _, item := range obj.Items { + key := m.getKeyString(item.KeyExpr) + if key == attrName { + if tup, ok := item.ValueExpr.(*hclsyntax.TupleConsExpr); ok && len(tup.Exprs) == 1 { + // Unwrap single-element list to scalar + newItems = append(newItems, hclsyntax.ObjectConsItem{ + KeyExpr: item.KeyExpr, + ValueExpr: tup.Exprs[0], + }) + continue + } + } + newItems = append(newItems, item) + } + return &hclsyntax.ObjectConsExpr{ + Items: newItems, + SrcRange: obj.SrcRange, + } +} + // getKeyString extracts the string value from a key expression func (m *V4ToV5Migrator) getKeyString(keyExpr hclsyntax.Expression) string { switch k := keyExpr.(type) { diff --git a/internal/resources/zero_trust_access_group/v4_to_v5_test.go b/internal/resources/zero_trust_access_group/v4_to_v5_test.go index d399c016..993cff95 100644 --- a/internal/resources/zero_trust_access_group/v4_to_v5_test.go +++ b/internal/resources/zero_trust_access_group/v4_to_v5_test.go @@ -619,9 +619,226 @@ moved { } `, }, - // Note: gsuite, azure, okta, saml complex nested tests are skipped - // as the v4 schema uses these as array fields, not nested blocks - // Testing these would require actual v4 state format which has arrays + // ============================================================================ + // azure, gsuite, okta block tests (list-to-scalar unwrapping) + // In v4, azure.id, gsuite.email, okta.name are lists. + // In v5, azure_ad.id, gsuite.email, okta.name are strings. + // The migration must unwrap single-element lists to scalars. + // ============================================================================ + { + Name: "azure block with list id literal", + Input: ` +resource "cloudflare_access_group" "test" { + account_id = "abc123" + name = "Azure Group" + + include { + azure { + identity_provider_id = "idp-123" + id = ["group-id-1"] + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "test" { + account_id = "abc123" + name = "Azure Group" + + include = [ + { + azure_ad = { + identity_provider_id = "idp-123" + id = "group-id-1" + } + }, + ] +} +moved { + from = cloudflare_access_group.test + to = cloudflare_zero_trust_access_group.test +} +`, + }, + { + Name: "azure block with list id variable reference", + Input: ` +resource "cloudflare_access_group" "test" { + for_each = var.idp.group + + account_id = "abc123" + name = each.key + + include { + azure { + identity_provider_id = cloudflare_zero_trust_access_identity_provider.azure.id + id = [each.value] + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "test" { + for_each = var.idp.group + + account_id = "abc123" + name = each.key + + include = [ + { + azure_ad = { + identity_provider_id = cloudflare_zero_trust_access_identity_provider.azure.id + id = each.value + } + }, + ] +} +moved { + from = cloudflare_access_group.test + to = cloudflare_zero_trust_access_group.test +} +`, + }, + { + Name: "azure block with list id - already renamed resource type (exact user report APIX-1110)", + Input: ` +resource "cloudflare_zero_trust_access_group" "appl" { + for_each = var.idp.group + + account_id = "abc123" + name = each.key + + include { + azure { + identity_provider_id = cloudflare_zero_trust_access_identity_provider.azure.id + id = [each.value] + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "appl" { + for_each = var.idp.group + + account_id = "abc123" + name = each.key + + include = [ + { + azure_ad = { + identity_provider_id = cloudflare_zero_trust_access_identity_provider.azure.id + id = each.value + } + }, + ] +} +`, + }, + { + Name: "azure block with scalar id (already unwrapped)", + Input: ` +resource "cloudflare_access_group" "test" { + account_id = "abc123" + name = "Azure Group" + + include { + azure { + identity_provider_id = "idp-123" + id = "group-id-1" + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "test" { + account_id = "abc123" + name = "Azure Group" + + include = [ + { + azure_ad = { + identity_provider_id = "idp-123" + id = "group-id-1" + } + }, + ] +} +moved { + from = cloudflare_access_group.test + to = cloudflare_zero_trust_access_group.test +} +`, + }, + { + Name: "gsuite block with list email", + Input: ` +resource "cloudflare_access_group" "test" { + account_id = "abc123" + name = "GSuite Group" + + include { + gsuite { + email = ["group@example.com"] + identity_provider_id = "idp-123" + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "test" { + account_id = "abc123" + name = "GSuite Group" + + include = [ + { + gsuite = { + email = "group@example.com" + identity_provider_id = "idp-123" + } + }, + ] +} +moved { + from = cloudflare_access_group.test + to = cloudflare_zero_trust_access_group.test +} +`, + }, + { + Name: "okta block with list name", + Input: ` +resource "cloudflare_access_group" "test" { + account_id = "abc123" + name = "Okta Group" + + include { + okta { + name = ["okta-group-1"] + identity_provider_id = "idp-123" + } + } +} +`, + Expected: ` +resource "cloudflare_zero_trust_access_group" "test" { + account_id = "abc123" + name = "Okta Group" + + include = [ + { + okta = { + name = "okta-group-1" + identity_provider_id = "idp-123" + } + }, + ] +} +moved { + from = cloudflare_access_group.test + to = cloudflare_zero_trust_access_group.test +} +`, + }, { Name: "complex multi-selector", Input: `