diff --git a/e2e/tf/v4/provider.tf b/e2e/tf/v4/provider.tf index 5a1ae467..e81eeadf 100644 --- a/e2e/tf/v4/provider.tf +++ b/e2e/tf/v4/provider.tf @@ -60,3 +60,29 @@ variable "crowdstrike_customer_id" { type = string default = "" } + +# BYO IP Prefix variables for testing +# Set via TF_VAR_byo_ip_* environment variables or directly in terraform.tfvars +variable "byo_ip_cidr" { + description = "BYO IP CIDR notation for the prefix" + type = string + default = "2606:54c2:2::/48" +} + +variable "byo_ip_asn" { + description = "BYO IP ASN number" + type = string + default = "13335" +} + +variable "byo_ip_loa_document_id" { + description = "BYO IP LOA document ID" + type = string + default = "a6061274b61449d3bb4521aee2e90c95" +} + +variable "byo_ip_prefix_id" { + description = "BYO IP prefix ID (for v4 provider or migration tests)" + type = string + default = "b543f1a825f4474b88e945f164624412" +} diff --git a/integration/v4_to_v5/testdata/byo_ip_prefix/expected/byo_ip_prefix.tf b/integration/v4_to_v5/testdata/byo_ip_prefix/expected/byo_ip_prefix.tf new file mode 100644 index 00000000..9624079d --- /dev/null +++ b/integration/v4_to_v5/testdata/byo_ip_prefix/expected/byo_ip_prefix.tf @@ -0,0 +1,217 @@ +# Comprehensive Integration Tests for byo_ip_prefix Migration (v4 → v5) +# This file covers all v4 schema attributes and Terraform patterns +# Target: 15-30+ resource instances + +# ============================================================================ +# PATTERN 1-2: Variables & Locals +# ============================================================================ + +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string + # No default - must be provided +} + +variable "cloudflare_zone_id" { + description = "Cloudflare zone ID" + type = string + # No default - must be provided +} + +variable "cloudflare_domain" { + description = "Cloudflare domain for testing" + type = string +} + +variable "prefix_base" { + description = "Base prefix ID for testing" + type = string + default = "cftftest" +} + +variable "enable_optional" { + description = "Whether to create optional resources" + type = bool + default = true +} + +variable "advertisement_status" { + description = "Advertisement status for prefixes" + type = string + default = "on" +} + +locals { + name_prefix = "cftftest" + environment = "integration-test" + description_template = "BYO IP Prefix for ${local.environment}" + + prefix_map = { + prod = "cftftest-prod-001" + staging = "cftftest-staging-001" + dev = "cftftest-dev-001" + test = "cftftest-test-001" + } + + prefix_list = [ + "cftftest-list-001", + "cftftest-list-002", + "cftftest-list-003" + ] +} + +# ============================================================================ +# PATTERN 1: Basic Resources (3 instances) +# ============================================================================ + +# Instance 1: Minimal resource (only required fields) +resource "cloudflare_byo_ip_prefix" "minimal" { + account_id = var.cloudflare_account_id +} + +# Instance 2: Full resource with all optional fields +resource "cloudflare_byo_ip_prefix" "full" { + account_id = var.cloudflare_account_id + description = "Full BYO IP prefix with all fields" +} + +# Instance 3: Resource with variables +resource "cloudflare_byo_ip_prefix" "with_variables" { + account_id = var.cloudflare_account_id + description = local.description_template +} + +# ============================================================================ +# PATTERN 3: for_each with Maps (4 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "from_map" { + for_each = local.prefix_map + + account_id = var.cloudflare_account_id + description = "Prefix for ${each.key} environment" +} + +# ============================================================================ +# PATTERN 4: for_each with Sets (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "from_set" { + for_each = toset(local.prefix_list) + + account_id = var.cloudflare_account_id + description = "Prefix from set: ${each.value}" +} + +# ============================================================================ +# PATTERN 5: count-based Resources (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_count" { + count = 3 + + account_id = var.cloudflare_account_id + description = "Prefix number ${count.index + 1}" +} + +# ============================================================================ +# PATTERN 6: Conditional Resource Creation (2 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "conditional" { + count = var.enable_optional ? 1 : 0 + + account_id = var.cloudflare_account_id + description = "Conditional prefix" +} + +resource "cloudflare_byo_ip_prefix" "conditional_ternary" { + count = var.enable_optional ? 2 : 0 + + account_id = var.cloudflare_account_id + description = var.enable_optional ? "Enabled prefix ${count.index}" : null +} + +# ============================================================================ +# PATTERN 7: Cross-resource References (1 instance) +# ============================================================================ + +# Note: byo_ip_prefix is a standalone resource, so cross-references are limited +# But we can reference other instances +resource "cloudflare_byo_ip_prefix" "reference" { + account_id = cloudflare_byo_ip_prefix.minimal.account_id + description = "References minimal: ${cloudflare_byo_ip_prefix.minimal.prefix_id}" +} + +# ============================================================================ +# PATTERN 8: Lifecycle Meta-arguments (2 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_lifecycle" { + account_id = var.cloudflare_account_id + description = "Prefix with lifecycle rules" + + lifecycle { + create_before_destroy = true + ignore_changes = [description] + } +} + +resource "cloudflare_byo_ip_prefix" "prevent_destroy" { + account_id = var.cloudflare_account_id + description = "Protected prefix" + + lifecycle { + prevent_destroy = true + } +} + +# ============================================================================ +# PATTERN 9: Terraform Functions (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_join" { + account_id = var.cloudflare_account_id + description = join(" - ", ["BYO IP", local.environment, "test"]) +} + +resource "cloudflare_byo_ip_prefix" "with_format" { + account_id = var.cloudflare_account_id + description = format("Prefix for %s environment", local.environment) +} + +resource "cloudflare_byo_ip_prefix" "with_interpolation" { + account_id = var.cloudflare_account_id + description = "Prefix managed by ${local.name_prefix} for ${local.environment}" +} + +# ============================================================================ +# EDGE CASES (2 instances) +# ============================================================================ + +# Edge case 1: Empty string description (vs null) +resource "cloudflare_byo_ip_prefix" "empty_description" { + account_id = var.cloudflare_account_id + description = "" +} + +# Edge case 2: Advertisement off +resource "cloudflare_byo_ip_prefix" "advertisement_off" { + account_id = var.cloudflare_account_id + description = "Prefix with advertisement disabled" +} + +# ============================================================================ +# INSTANCE COUNT SUMMARY +# ============================================================================ +# Basic: 3 +# for_each maps: 4 (prod, staging, dev, test) +# for_each sets: 3 +# count-based: 3 +# conditional: 3 (1 + 2 when enabled) +# cross-reference: 1 +# lifecycle: 2 +# functions: 3 +# edge cases: 2 +# TOTAL: 24 instances (exceeds 15-30 target) +# ============================================================================ diff --git a/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix.tf b/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix.tf new file mode 100644 index 00000000..19d0c111 --- /dev/null +++ b/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix.tf @@ -0,0 +1,241 @@ +# Comprehensive Integration Tests for byo_ip_prefix Migration (v4 → v5) +# This file covers all v4 schema attributes and Terraform patterns +# Target: 15-30+ resource instances + +# ============================================================================ +# PATTERN 1-2: Variables & Locals +# ============================================================================ + +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string + # No default - must be provided +} + +variable "cloudflare_zone_id" { + description = "Cloudflare zone ID" + type = string + # No default - must be provided +} + +variable "cloudflare_domain" { + description = "Cloudflare domain for testing" + type = string +} + +variable "prefix_base" { + description = "Base prefix ID for testing" + type = string + default = "cftftest" +} + +variable "enable_optional" { + description = "Whether to create optional resources" + type = bool + default = true +} + +variable "advertisement_status" { + description = "Advertisement status for prefixes" + type = string + default = "on" +} + +locals { + name_prefix = "cftftest" + environment = "integration-test" + description_template = "BYO IP Prefix for ${local.environment}" + + prefix_map = { + prod = "cftftest-prod-001" + staging = "cftftest-staging-001" + dev = "cftftest-dev-001" + test = "cftftest-test-001" + } + + prefix_list = [ + "cftftest-list-001", + "cftftest-list-002", + "cftftest-list-003" + ] +} + +# ============================================================================ +# PATTERN 1: Basic Resources (3 instances) +# ============================================================================ + +# Instance 1: Minimal resource (only required fields) +resource "cloudflare_byo_ip_prefix" "minimal" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-minimal-001" +} + +# Instance 2: Full resource with all optional fields +resource "cloudflare_byo_ip_prefix" "full" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-full-001" + description = "Full BYO IP prefix with all fields" + advertisement = "on" +} + +# Instance 3: Resource with variables +resource "cloudflare_byo_ip_prefix" "with_variables" { + account_id = var.cloudflare_account_id + prefix_id = var.prefix_base + description = local.description_template + advertisement = var.advertisement_status +} + +# ============================================================================ +# PATTERN 3: for_each with Maps (4 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "from_map" { + for_each = local.prefix_map + + account_id = var.cloudflare_account_id + prefix_id = each.value + description = "Prefix for ${each.key} environment" + advertisement = each.key == "prod" ? "on" : "off" +} + +# ============================================================================ +# PATTERN 4: for_each with Sets (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "from_set" { + for_each = toset(local.prefix_list) + + account_id = var.cloudflare_account_id + prefix_id = each.value + description = "Prefix from set: ${each.value}" +} + +# ============================================================================ +# PATTERN 5: count-based Resources (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_count" { + count = 3 + + account_id = var.cloudflare_account_id + prefix_id = "cftftest-count-${count.index}" + description = "Prefix number ${count.index + 1}" + advertisement = count.index % 2 == 0 ? "on" : "off" +} + +# ============================================================================ +# PATTERN 6: Conditional Resource Creation (2 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "conditional" { + count = var.enable_optional ? 1 : 0 + + account_id = var.cloudflare_account_id + prefix_id = "cftftest-conditional-001" + description = "Conditional prefix" + advertisement = "off" +} + +resource "cloudflare_byo_ip_prefix" "conditional_ternary" { + count = var.enable_optional ? 2 : 0 + + account_id = var.cloudflare_account_id + prefix_id = "cftftest-conditional-${count.index + 1}" + description = var.enable_optional ? "Enabled prefix ${count.index}" : null +} + +# ============================================================================ +# PATTERN 7: Cross-resource References (1 instance) +# ============================================================================ + +# Note: byo_ip_prefix is a standalone resource, so cross-references are limited +# But we can reference other instances +resource "cloudflare_byo_ip_prefix" "reference" { + account_id = cloudflare_byo_ip_prefix.minimal.account_id + prefix_id = "cftftest-reference-001" + description = "References minimal: ${cloudflare_byo_ip_prefix.minimal.prefix_id}" +} + +# ============================================================================ +# PATTERN 8: Lifecycle Meta-arguments (2 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_lifecycle" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-lifecycle-001" + description = "Prefix with lifecycle rules" + advertisement = "on" + + lifecycle { + create_before_destroy = true + ignore_changes = [description] + } +} + +resource "cloudflare_byo_ip_prefix" "prevent_destroy" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-prevent-001" + description = "Protected prefix" + + lifecycle { + prevent_destroy = true + } +} + +# ============================================================================ +# PATTERN 9: Terraform Functions (3 instances) +# ============================================================================ + +resource "cloudflare_byo_ip_prefix" "with_join" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-join-001" + description = join(" - ", ["BYO IP", local.environment, "test"]) +} + +resource "cloudflare_byo_ip_prefix" "with_format" { + account_id = var.cloudflare_account_id + prefix_id = "cftftest-format-001" + description = format("Prefix for %s environment", local.environment) + advertisement = "on" +} + +resource "cloudflare_byo_ip_prefix" "with_interpolation" { + account_id = var.cloudflare_account_id + prefix_id = "${local.name_prefix}-prefix-001" + description = "Prefix managed by ${local.name_prefix} for ${local.environment}" +} + +# ============================================================================ +# EDGE CASES (2 instances) +# ============================================================================ + +# Edge case 1: Empty string description (vs null) +resource "cloudflare_byo_ip_prefix" "empty_description" { + account_id = var.cloudflare_account_id + prefix_id = "${local.name_prefix}-empty-desc-001" + description = "" +} + +# Edge case 2: Advertisement off +resource "cloudflare_byo_ip_prefix" "advertisement_off" { + account_id = var.cloudflare_account_id + prefix_id = "${local.name_prefix}-adv-off-001" + description = "Prefix with advertisement disabled" + advertisement = "off" +} + +# ============================================================================ +# INSTANCE COUNT SUMMARY +# ============================================================================ +# Basic: 3 +# for_each maps: 4 (prod, staging, dev, test) +# for_each sets: 3 +# count-based: 3 +# conditional: 3 (1 + 2 when enabled) +# cross-reference: 1 +# lifecycle: 2 +# functions: 3 +# edge cases: 2 +# TOTAL: 24 instances (exceeds 15-30 target) +# ============================================================================ diff --git a/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix_e2e.tf b/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix_e2e.tf new file mode 100644 index 00000000..e4bd8fcf --- /dev/null +++ b/integration/v4_to_v5/testdata/byo_ip_prefix/input/byo_ip_prefix_e2e.tf @@ -0,0 +1,42 @@ +# Terraform v4 to v5 Migration E2E Test - BYO IP Prefix +# +# Note: The v4 provider's Create function does not call the API to create a +# prefix. It sets the resource ID from prefix_id and reads the existing prefix. +# A real prefix ID must be provided via CLOUDFLARE_BYO_IP_PREFIX_ID. +# +# See: https://github.com/cloudflare/terraform-provider-cloudflare/blob/main/docs/resources/byo_ip_prefix.md + +# Variables passed from root module +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string +} + +variable "byo_ip_cidr" { + description = "BYO IP CIDR (e.g., 2606:54c2:3::/48)" + type = string +} + +variable "byo_ip_asn" { + description = "BYO IP ASN (e.g., 13335)" + type = string +} + +variable "byo_ip_loa_document_id" { + description = "LOA Document ID" + type = string +} + +variable "byo_ip_prefix_id" { + description = "Pre-existing BYO IP prefix ID" + type = string +} + +# Test instance: BYO IP prefix +# This resource will be imported, not created +resource "cloudflare_byo_ip_prefix" "test" { + account_id = var.cloudflare_account_id + prefix_id = var.byo_ip_prefix_id + description = "E2E migration test prefix" + advertisement = "on" +} diff --git a/internal/e2e-runner/env.go b/internal/e2e-runner/env.go index d7bf6ce8..cea07af4 100644 --- a/internal/e2e-runner/env.go +++ b/internal/e2e-runner/env.go @@ -24,10 +24,22 @@ type E2EEnv struct { CrowdstrikeClientSecret string CrowdstrikeAPIURL string CrowdstrikeCustomerID string + BYOIPCidr string + BYOIPASN string + BYOIPLOADocumentID string + BYOIPPrefixID string } // LoadEnv loads environment variables and validates required ones func LoadEnv(required []string) (*E2EEnv, error) { + // Helper function to get environment variable with default value + getEnvWithDefault := func(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue + } + env := &E2EEnv{ AccountID: os.Getenv("CLOUDFLARE_ACCOUNT_ID"), ZoneID: os.Getenv("CLOUDFLARE_ZONE_ID"), @@ -40,6 +52,10 @@ func LoadEnv(required []string) (*E2EEnv, error) { CrowdstrikeClientSecret: os.Getenv("CLOUDFLARE_CROWDSTRIKE_CLIENT_SECRET"), CrowdstrikeAPIURL: os.Getenv("CLOUDFLARE_CROWDSTRIKE_API_URL"), CrowdstrikeCustomerID: os.Getenv("CLOUDFLARE_CROWDSTRIKE_CUSTOMER_ID"), + BYOIPCidr: getEnvWithDefault("CLOUDFLARE_BYO_IP_CIDR", "2606:54c2:2::/48"), + BYOIPASN: getEnvWithDefault("CLOUDFLARE_BYO_IP_ASN", "13335"), + BYOIPLOADocumentID: getEnvWithDefault("CLOUDFLARE_BYO_IP_LOA_DOCUMENT_ID", "a6061274b61449d3bb4521aee2e90c95"), + BYOIPPrefixID: getEnvWithDefault("CLOUDFLARE_BYO_IP_PREFIX_ID", "b543f1a825f4474b88e945f164624412"), } // Validate required variables @@ -68,6 +84,14 @@ func LoadEnv(required []string) (*E2EEnv, error) { value = env.CrowdstrikeAPIURL case "CLOUDFLARE_CROWDSTRIKE_CUSTOMER_ID": value = env.CrowdstrikeCustomerID + case "CLOUDFLARE_BYO_IP_CIDR": + value = env.BYOIPCidr + case "CLOUDFLARE_BYO_IP_ASN": + value = env.BYOIPASN + case "CLOUDFLARE_BYO_IP_LOA_DOCUMENT_ID": + value = env.BYOIPLOADocumentID + case "CLOUDFLARE_BYO_IP_PREFIX_ID": + value = env.BYOIPPrefixID default: return nil, fmt.Errorf("unknown environment variable: %s", varName) } diff --git a/internal/e2e-runner/import.go b/internal/e2e-runner/import.go index c03ab27e..09afa7c1 100644 --- a/internal/e2e-runner/import.go +++ b/internal/e2e-runner/import.go @@ -1,4 +1,5 @@ // import.go handles parsing import annotations and generating import blocks. +// import.go handles parsing import annotations and generating import blocks. // // This file provides functionality to: // - Parse import annotations from Terraform configuration files @@ -6,16 +7,18 @@ // - Support variable interpolation in import addresses // // Import annotations use the format: -// # tf-migrate:import-address=
-// resource "type" "name" { ... } +// +// # tf-migrate:import-address=
+// resource "type" "name" { ... } // // Where
can include variable references like ${var.cloudflare_account_id} // // The generated import blocks are placed in the root module's main.tf: -// import { -// to = module... -// id = -// } +// +// import { +// to = module... +// id = +// } package e2e import ( diff --git a/internal/e2e-runner/import_test.go b/internal/e2e-runner/import_test.go index 7080dc1c..05af6279 100644 --- a/internal/e2e-runner/import_test.go +++ b/internal/e2e-runner/import_test.go @@ -9,11 +9,11 @@ import ( func TestParseImportAnnotations(t *testing.T) { tests := []struct { - name string - fileContent string - moduleName string - expectedSpecs int - expectedFirst *ImportSpec + name string + fileContent string + moduleName string + expectedSpecs int + expectedFirst *ImportSpec }{ { name: "single import annotation", diff --git a/internal/e2e-runner/init.go b/internal/e2e-runner/init.go index 433784c4..cf9f8cd1 100644 --- a/internal/e2e-runner/init.go +++ b/internal/e2e-runner/init.go @@ -185,6 +185,49 @@ variable "cloudflare_domain" { printGreen(" ✓ Added cloudflare_domain variable to provider.tf") } + // Check if provider.tf has BYO IP variables and add them if missing + providerContent, err = os.ReadFile(providerTfPath) + if err == nil && !strings.Contains(string(providerContent), "variable \"byo_ip_cidr\"") { + // Append BYO IP variables to provider.tf + byoIPVarsContent := ` +# BYO IP Prefix variables for testing +# Set via TF_VAR_byo_ip_* environment variables or directly in terraform.tfvars +variable "byo_ip_cidr" { + description = "BYO IP CIDR notation for the prefix" + type = string + default = "2606:54c2:2::/48" +} + +variable "byo_ip_asn" { + description = "BYO IP ASN number" + type = string + default = "13335" +} + +variable "byo_ip_loa_document_id" { + description = "BYO IP LOA document ID" + type = string + default = "c8af01b0dd8f4779980824d9a8c84136" +} + +variable "byo_ip_prefix_id" { + description = "BYO IP prefix ID (for v4 provider or migration tests)" + type = string + default = "b543f1a825f4474b88e945f164624412" +} +` + f, err := os.OpenFile(providerTfPath, os.O_APPEND|os.O_WRONLY, permFile) + if err != nil { + return fmt.Errorf("failed to open provider.tf at %s: %w", providerTfPath, err) + } + defer f.Close() + + if _, err := f.WriteString(byoIPVarsContent); err != nil { + return fmt.Errorf("failed to append BYO IP variables to provider.tf: %w", err) + } + printGreen(" ✓ Added BYO IP variables to provider.tf") + } + tfvarsContent := fmt.Sprintf(`# Auto-generated by init script # Edit and re-run ./scripts/init to update these values @@ -195,7 +238,11 @@ crowdstrike_client_id = "%s" crowdstrike_client_secret = "%s" crowdstrike_api_url = "%s" crowdstrike_customer_id = "%s" -`, env.AccountID, env.ZoneID, env.Domain, env.CrowdstrikeClientID, env.CrowdstrikeClientSecret, env.CrowdstrikeAPIURL, env.CrowdstrikeCustomerID) +byo_ip_cidr = "%s" +byo_ip_asn = "%s" +byo_ip_loa_document_id = "%s" +byo_ip_prefix_id = "%s" +`, env.AccountID, env.ZoneID, env.Domain, env.CrowdstrikeClientID, env.CrowdstrikeClientSecret, env.CrowdstrikeAPIURL, env.CrowdstrikeCustomerID, env.BYOIPCidr, env.BYOIPASN, env.BYOIPLOADocumentID, env.BYOIPPrefixID) tfvarsPath := filepath.Join(v4Dir, "terraform.tfvars") if err := os.WriteFile(tfvarsPath, []byte(tfvarsContent), permFile); err != nil { @@ -274,6 +321,18 @@ crowdstrike_customer_id = "%s" if moduleVars["crowdstrike_customer_id"] { mainTfContent += "\n crowdstrike_customer_id = var.crowdstrike_customer_id" } + if moduleVars["byo_ip_cidr"] { + mainTfContent += "\n byo_ip_cidr = var.byo_ip_cidr" + } + if moduleVars["byo_ip_asn"] { + mainTfContent += "\n byo_ip_asn = var.byo_ip_asn" + } + if moduleVars["byo_ip_loa_document_id"] { + mainTfContent += "\n byo_ip_loa_document_id = var.byo_ip_loa_document_id" + } + if moduleVars["byo_ip_prefix_id"] { + mainTfContent += "\n byo_ip_prefix_id = var.byo_ip_prefix_id" + } mainTfContent += "\n}\n" } @@ -422,4 +481,3 @@ func discoverModuleVariables(moduleDir string) (map[string]bool, error) { return variables, nil } - diff --git a/internal/e2e-runner/runner.go b/internal/e2e-runner/runner.go index 3ae93181..8a728111 100644 --- a/internal/e2e-runner/runner.go +++ b/internal/e2e-runner/runner.go @@ -33,7 +33,6 @@ const ( permSecretFile = 0600 // rw------- - sensitive files (state, secrets) ) - // RunConfig holds configuration for e2e test run type RunConfig struct { SkipV4Test bool @@ -639,10 +638,10 @@ func RunE2ETests(cfg *RunConfig) error { // driftCheckResult holds the result of checking and displaying drift type driftCheckResult struct { - hasDrift bool - driftLines []string - exemptedCount int - exemptedLines []string + hasDrift bool + driftLines []string + exemptedCount int + exemptedLines []string } // checkAndDisplayDrift checks for drift in plan output and displays results @@ -882,7 +881,6 @@ func runV4Tests(ctx *testContext) error { } printSuccess("Terraform init successful (remote state loaded from R2)") - // Run terraform plan printYellow("Running terraform plan in v4/...") planArgs := append([]string{"plan", "-no-color", "-parallelism=2", "-out=" + filepath.Join(ctx.tmpDir, "v4.tfplan"), "-input=false"}, ctx.targetArgs...) @@ -1005,8 +1003,8 @@ func displayGroupedDrift(driftLines []string) { pattern string } - patternCounts := make(map[string]int) // pattern -> count - patternResources := make(map[string][]string) // pattern -> list of resources + patternCounts := make(map[string]int) // pattern -> count + patternResources := make(map[string][]string) // pattern -> list of resources for _, line := range driftLines { // Try to extract resource name and pattern diff --git a/internal/registry/registry.go b/internal/registry/registry.go index a1ccbc53..8aeb034a 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -14,6 +14,7 @@ import ( "github.com/cloudflare/tf-migrate/internal/resources/authenticated_origin_pulls" "github.com/cloudflare/tf-migrate/internal/resources/authenticated_origin_pulls_certificate" "github.com/cloudflare/tf-migrate/internal/resources/bot_management" + "github.com/cloudflare/tf-migrate/internal/resources/byo_ip_prefix" "github.com/cloudflare/tf-migrate/internal/resources/certificate_pack" "github.com/cloudflare/tf-migrate/internal/resources/custom_hostname_fallback_origin" "github.com/cloudflare/tf-migrate/internal/resources/custom_pages" @@ -101,6 +102,7 @@ func RegisterAllMigrations() { authenticated_origin_pulls.NewV4ToV5Migrator() authenticated_origin_pulls_certificate.NewV4ToV5Migrator() bot_management.NewV4ToV5Migrator() + byo_ip_prefix.NewV4ToV5Migrator() certificate_pack.NewV4ToV5Migrator() custom_hostname_fallback_origin.NewV4ToV5Migrator() custom_pages.NewV4ToV5Migrator() diff --git a/internal/resources/byo_ip_prefix/v4_to_v5.go b/internal/resources/byo_ip_prefix/v4_to_v5.go new file mode 100644 index 00000000..0396cd93 --- /dev/null +++ b/internal/resources/byo_ip_prefix/v4_to_v5.go @@ -0,0 +1,58 @@ +package byo_ip_prefix + +import ( + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/tidwall/gjson" + + "github.com/cloudflare/tf-migrate/internal" + "github.com/cloudflare/tf-migrate/internal/transform" + tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl" +) + +// V4ToV5Migrator handles migration of BYO IP prefix resources from v4 to v5 +type V4ToV5Migrator struct{} + +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + internal.RegisterMigrator("cloudflare_byo_ip_prefix", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + // Resource name stays the same in v5 + return "cloudflare_byo_ip_prefix" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + return resourceType == "cloudflare_byo_ip_prefix" +} + +func (m *V4ToV5Migrator) Preprocess(content string) string { + // No preprocessing needed for BYO IP prefix + return content +} + +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + body := block.Body() + + // Remove v4-only fields that don't exist in v5 + // prefix_id - becomes 'id' in v5 (computed, not in config) + // advertisement - replaced by 'advertised' in v5 (computed) + tfhcl.RemoveAttributes(body, "prefix_id", "advertisement") + + // Note: We do NOT add asn and cidr here + // These are required in v5 but don't exist in v4 + // User must manually add them after migration + // v5 provider will fetch all computed fields on first refresh + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +func (m *V4ToV5Migrator) TransformState(_ *transform.Context, instance gjson.Result, _, _ string) (string, error) { + // State transformation is handled by the v5 provider's UpgradeState (schema_version 0→1). + // tf-migrate only handles HCL config transformation; pass state through unchanged. + return instance.String(), nil +} diff --git a/internal/resources/byo_ip_prefix/v4_to_v5_test.go b/internal/resources/byo_ip_prefix/v4_to_v5_test.go new file mode 100644 index 00000000..0e905c73 --- /dev/null +++ b/internal/resources/byo_ip_prefix/v4_to_v5_test.go @@ -0,0 +1,84 @@ +package byo_ip_prefix + +import ( + "testing" + + "github.com/cloudflare/tf-migrate/internal/testhelpers" +) + +func TestV4ToV5Transformation(t *testing.T) { + migrator := NewV4ToV5Migrator() + + // Test configuration transformations + t.Run("ConfigTransformation", func(t *testing.T) { + tests := []testhelpers.ConfigTestCase{ + { + Name: "basic BYO IP prefix - removes prefix_id and advertisement", + Input: ` +resource "cloudflare_byo_ip_prefix" "example" { + account_id = "f037e56e89293a057740de681ac9abbe" + prefix_id = "prefix-abc123" + description = "My BYO IP prefix" + advertisement = "on" +}`, + Expected: `resource "cloudflare_byo_ip_prefix" "example" { + account_id = "f037e56e89293a057740de681ac9abbe" + description = "My BYO IP prefix" +}`, + }, + { + Name: "minimal BYO IP prefix - only required fields", + Input: ` +resource "cloudflare_byo_ip_prefix" "minimal" { + account_id = var.cloudflare_account_id + prefix_id = "prefix-xyz789" +}`, + Expected: `resource "cloudflare_byo_ip_prefix" "minimal" { + account_id = var.cloudflare_account_id +}`, + }, + { + Name: "BYO IP prefix with description only", + Input: ` +resource "cloudflare_byo_ip_prefix" "test" { + account_id = "test-account-id" + prefix_id = "prefix-test" + description = "Test prefix" +}`, + Expected: `resource "cloudflare_byo_ip_prefix" "test" { + account_id = "test-account-id" + description = "Test prefix" +}`, + }, + { + Name: "multiple BYO IP prefixes in one file", + Input: ` +resource "cloudflare_byo_ip_prefix" "first" { + account_id = var.account_id + prefix_id = "prefix-first" + description = "First prefix" + advertisement = "on" +} + +resource "cloudflare_byo_ip_prefix" "second" { + account_id = var.account_id + prefix_id = "prefix-second" + description = "Second prefix" + advertisement = "off" +}`, + Expected: `resource "cloudflare_byo_ip_prefix" "first" { + account_id = var.account_id + description = "First prefix" +} + +resource "cloudflare_byo_ip_prefix" "second" { + account_id = var.account_id + description = "Second prefix" +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) + }) + +} diff --git a/internal/resources/zero_trust_organization/v4_to_v5.go b/internal/resources/zero_trust_organization/v4_to_v5.go index 6a2421f7..b37e03b4 100644 --- a/internal/resources/zero_trust_organization/v4_to_v5.go +++ b/internal/resources/zero_trust_organization/v4_to_v5.go @@ -7,8 +7,8 @@ import ( "github.com/cloudflare/tf-migrate/internal" "github.com/cloudflare/tf-migrate/internal/transform" - "github.com/cloudflare/tf-migrate/internal/transform/state" tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl" + "github.com/cloudflare/tf-migrate/internal/transform/state" ) type V4ToV5Migrator struct {