diff --git a/integration/v4_to_v5/testdata/managed_transforms/expected/managed_transforms.tf b/integration/v4_to_v5/testdata/managed_transforms/expected/managed_transforms.tf new file mode 100644 index 0000000..12c1396 --- /dev/null +++ b/integration/v4_to_v5/testdata/managed_transforms/expected/managed_transforms.tf @@ -0,0 +1,438 @@ +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string +} + +variable "cloudflare_zone_id" { + description = "Cloudflare zone ID" + type = string +} + +# ======================================== +# Test Case 1: Basic Configurations +# ======================================== + +# Test 1.1: Minimal configuration with no headers +resource "cloudflare_managed_transforms" "minimal" { + zone_id = var.cloudflare_zone_id + managed_request_headers = [] + managed_response_headers = [] +} + +# Test 1.2: Request headers only +resource "cloudflare_managed_transforms" "request_only" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }, { + id = "add_visitor_location_headers" + enabled = false + }] + managed_response_headers = [] +} + +# Test 1.3: Response headers only +resource "cloudflare_managed_transforms" "response_only" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }, { + id = "add_security_headers" + enabled = false + }] +} + +# Test 1.4: Both request and response headers +resource "cloudflare_managed_transforms" "both_headers" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_bot_protection_headers" + enabled = true + }] + managed_response_headers = [{ + id = "remove_server_header" + enabled = true + }] +} + +# Test 1.5: Multiple headers of each type +resource "cloudflare_managed_transforms" "multiple_headers" { + zone_id = var.cloudflare_zone_id + + + + + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }, { + id = "add_visitor_location_headers" + enabled = true + }, { + id = "add_bot_protection_headers" + enabled = false + }] + managed_response_headers = [{ + id = "add_security_headers" + enabled = true + }, { + id = "remove_x-powered-by_header" + enabled = true + }, { + id = "remove_server_header" + enabled = false + }] +} + +# ======================================== +# Test Case 2: Variable References +# ======================================== + +variable "enable_security_headers" { + type = bool + default = true +} + +variable "header_configs" { + type = map(object({ + request_headers = list(string) + response_headers = list(string) + })) + default = { + "production" = { + request_headers = ["add_true_client_ip_headers"] + response_headers = ["add_security_headers", "remove_x-powered-by_header"] + } + "staging" = { + request_headers = ["add_visitor_location_headers"] + response_headers = ["remove_server_header"] + } + } +} + +# Test 2.1: Conditional resource creation with count +resource "cloudflare_managed_transforms" "conditional_headers" { + count = var.enable_security_headers ? 1 : 0 + + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [{ + id = "add_security_headers" + enabled = true + }] +} + +# ======================================== +# Test Case 3: for_each with Maps +# ======================================== + +# Test 3.1: for_each over map of configurations +resource "cloudflare_managed_transforms" "env_specific" { + for_each = var.header_configs + + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = each.value.request_headers[0] + enabled = true + }] + managed_response_headers = [{ + id = each.value.response_headers[0] + enabled = true + }] +} + +# ======================================== +# Test Case 4: Locals and Expressions +# ======================================== + +locals { + zones = { + "zone1" = var.cloudflare_zone_id + "zone2" = var.cloudflare_zone_id + } + + header_enabled = true + common_headers = ["add_true_client_ip_headers", "add_visitor_location_headers"] +} + +# Test 4.1: Using locals +resource "cloudflare_managed_transforms" "with_locals" { + zone_id = local.zones["zone1"] + + + managed_request_headers = [{ + id = local.common_headers[0] + enabled = local.header_enabled + }, { + id = local.common_headers[1] + enabled = !local.header_enabled + }] + managed_response_headers = [] +} + +# Test 4.2: for_each over local values +resource "cloudflare_managed_transforms" "multi_zone" { + for_each = local.zones + + zone_id = each.value + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [] +} + +# ======================================== +# Test Case 5: Boolean Edge Cases +# ======================================== + +# Test 5.1: All headers enabled +resource "cloudflare_managed_transforms" "all_enabled" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_visitor_location_headers" + enabled = true + }] + managed_response_headers = [{ + id = "remove_server_header" + enabled = true + }] +} + +# Test 5.2: All headers disabled +resource "cloudflare_managed_transforms" "all_disabled" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_visitor_location_headers" + enabled = false + }] + managed_response_headers = [{ + id = "remove_server_header" + enabled = false + }] +} + +# Test 5.3: Mixed enabled/disabled states +resource "cloudflare_managed_transforms" "mixed_states" { + zone_id = var.cloudflare_zone_id + + + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }, { + id = "add_visitor_location_headers" + enabled = false + }] + managed_response_headers = [{ + id = "add_security_headers" + enabled = true + }, { + id = "remove_x-powered-by_header" + enabled = false + }] +} + +# ======================================== +# Test Case 6: Header ID Variations +# ======================================== + +# Test 6.1: Header IDs with underscores +resource "cloudflare_managed_transforms" "underscores" { + zone_id = var.cloudflare_zone_id + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }] +} + +# Test 6.2: Header IDs with dashes +resource "cloudflare_managed_transforms" "dashes" { + zone_id = var.cloudflare_zone_id + + managed_request_headers = [{ + id = "add-custom-header" + enabled = true + }] + managed_response_headers = [] +} + +# ======================================== +# Test Case 7: Lifecycle Meta-Arguments +# ======================================== + +# Test 7.1: Resource with lifecycle +resource "cloudflare_managed_transforms" "with_lifecycle" { + zone_id = var.cloudflare_zone_id + + + lifecycle { + create_before_destroy = true + } + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [] +} + +# Test 7.2: Resource with prevent_destroy +resource "cloudflare_managed_transforms" "prevent_destroy" { + zone_id = var.cloudflare_zone_id + + + lifecycle { + prevent_destroy = false + } + managed_request_headers = [] + managed_response_headers = [{ + id = "add_security_headers" + enabled = true + }] +} + +# ======================================== +# Test Case 8: Comments Preservation +# ======================================== + +# Test 8.1: Resource with inline comments +resource "cloudflare_managed_transforms" "with_comments" { + zone_id = var.cloudflare_zone_id # Primary zone + + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [{ + id = "add_security_headers" + enabled = true + }] +} + +# ======================================== +# Test Case 9: Count with Index +# ======================================== + +variable "header_count" { + type = number + default = 2 +} + +# Test 9.1: Resource with count +resource "cloudflare_managed_transforms" "with_count" { + count = var.header_count + + zone_id = var.cloudflare_zone_id + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = count.index == 0 + }] + managed_response_headers = [] +} + +# ======================================== +# Test Case 10: String Interpolation +# ======================================== + +variable "environment" { + type = string + default = "test" +} + +# Test 10.1: Using string interpolation (for resource naming only) +resource "cloudflare_managed_transforms" "interpolated" { + zone_id = var.cloudflare_zone_id + + managed_request_headers = [{ + id = "add_visitor_location_headers" + enabled = var.environment == "production" + }] + managed_response_headers = [] +} + +# ======================================== +# Test Case 11: Ternary Operators +# ======================================== + +# Test 11.1: Ternary in enabled field +resource "cloudflare_managed_transforms" "ternary" { + zone_id = var.cloudflare_zone_id + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = var.enable_security_headers ? true : false + }] + managed_response_headers = [] +} + +# ======================================== +# Test Case 12: for_each with toset +# ======================================== + +variable "zones_list" { + type = list(string) + default = ["zone_a", "zone_b"] +} + +# Test 12.1: for_each with toset conversion +resource "cloudflare_managed_transforms" "from_list" { + for_each = toset(var.zones_list) + + zone_id = var.cloudflare_zone_id + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [] +} + +# ======================================== +# Summary: 20+ resource instances total +# ======================================== +# - Basic configurations: 5 resources +# - Conditional (count): 1 resource (potentially 0-1 instances) +# - for_each with maps: 2 resources (2 instances each = 4 instances) +# - Locals: 3 resources (2 for multi_zone = 4 instances) +# - Boolean edge cases: 3 resources +# - Header ID variations: 2 resources +# - Lifecycle: 2 resources +# - Comments: 1 resource +# - Count: 1 resource (2 instances) +# - Interpolation: 1 resource +# - Ternary: 1 resource +# - toset: 1 resource (2 instances) +# Total: ~25+ instances covering all Terraform patterns diff --git a/integration/v4_to_v5/testdata/managed_transforms/expected/terraform.tfstate b/integration/v4_to_v5/testdata/managed_transforms/expected/terraform.tfstate new file mode 100644 index 0000000..82841b5 --- /dev/null +++ b/integration/v4_to_v5/testdata/managed_transforms/expected/terraform.tfstate @@ -0,0 +1,698 @@ +{ + "lineage": "test-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "minimal", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "request_only", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_x-powered-by_header" + }, + { + "enabled": false, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "response_only", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_bot_protection_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "both_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": true, + "id": "add_visitor_location_headers" + }, + { + "enabled": false, + "id": "add_bot_protection_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + }, + { + "enabled": true, + "id": "remove_x-powered-by_header" + }, + { + "enabled": false, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multiple_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "conditional_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "production", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "env_specific[\"production\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "staging", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "env_specific[\"staging\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_locals", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone1", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multi_zone[\"zone1\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone2", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multi_zone[\"zone2\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "all_enabled", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": false, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "all_disabled", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + }, + { + "enabled": false, + "id": "remove_x-powered-by_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "mixed_states", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_x-powered-by_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "underscores", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add-custom-header" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "dashes", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_lifecycle", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "prevent_destroy", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_comments", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": 0, + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_count[0]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": 1, + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_count[1]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "interpolated", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "ternary", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone_a", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "from_list[\"zone_a\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone_b", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "from_list[\"zone_b\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + } + ], + "serial": 1, + "terraform_version": "1.5.0", + "version": 4 +} \ No newline at end of file diff --git a/integration/v4_to_v5/testdata/managed_transforms/input/managed_transforms.tf b/integration/v4_to_v5/testdata/managed_transforms/input/managed_transforms.tf new file mode 100644 index 0000000..326a8e4 --- /dev/null +++ b/integration/v4_to_v5/testdata/managed_transforms/input/managed_transforms.tf @@ -0,0 +1,438 @@ +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string +} + +variable "cloudflare_zone_id" { + description = "Cloudflare zone ID" + type = string +} + +# ======================================== +# Test Case 1: Basic Configurations +# ======================================== + +# Test 1.1: Minimal configuration with no headers +resource "cloudflare_managed_headers" "minimal" { + zone_id = var.cloudflare_zone_id +} + +# Test 1.2: Request headers only +resource "cloudflare_managed_headers" "request_only" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } +} + +# Test 1.3: Response headers only +resource "cloudflare_managed_headers" "response_only" { + zone_id = var.cloudflare_zone_id + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } + + managed_response_headers { + id = "add_security_headers" + enabled = false + } +} + +# Test 1.4: Both request and response headers +resource "cloudflare_managed_headers" "both_headers" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_bot_protection_headers" + enabled = true + } + + managed_response_headers { + id = "remove_server_header" + enabled = true + } +} + +# Test 1.5: Multiple headers of each type +resource "cloudflare_managed_headers" "multiple_headers" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = true + } + + managed_request_headers { + id = "add_bot_protection_headers" + enabled = false + } + + managed_response_headers { + id = "add_security_headers" + enabled = true + } + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } + + managed_response_headers { + id = "remove_server_header" + enabled = false + } +} + +# ======================================== +# Test Case 2: Variable References +# ======================================== + +variable "enable_security_headers" { + type = bool + default = true +} + +variable "header_configs" { + type = map(object({ + request_headers = list(string) + response_headers = list(string) + })) + default = { + "production" = { + request_headers = ["add_true_client_ip_headers"] + response_headers = ["add_security_headers", "remove_x-powered-by_header"] + } + "staging" = { + request_headers = ["add_visitor_location_headers"] + response_headers = ["remove_server_header"] + } + } +} + +# Test 2.1: Conditional resource creation with count +resource "cloudflare_managed_headers" "conditional_headers" { + count = var.enable_security_headers ? 1 : 0 + + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_response_headers { + id = "add_security_headers" + enabled = true + } +} + +# ======================================== +# Test Case 3: for_each with Maps +# ======================================== + +# Test 3.1: for_each over map of configurations +resource "cloudflare_managed_headers" "env_specific" { + for_each = var.header_configs + + zone_id = var.cloudflare_zone_id + + # Note: In v4, we can't dynamically create blocks from variables + # This tests that static blocks work with for_each + managed_request_headers { + id = each.value.request_headers[0] + enabled = true + } + + managed_response_headers { + id = each.value.response_headers[0] + enabled = true + } +} + +# ======================================== +# Test Case 4: Locals and Expressions +# ======================================== + +locals { + zones = { + "zone1" = var.cloudflare_zone_id + "zone2" = var.cloudflare_zone_id + } + + header_enabled = true + common_headers = ["add_true_client_ip_headers", "add_visitor_location_headers"] +} + +# Test 4.1: Using locals +resource "cloudflare_managed_headers" "with_locals" { + zone_id = local.zones["zone1"] + + managed_request_headers { + id = local.common_headers[0] + enabled = local.header_enabled + } + + managed_request_headers { + id = local.common_headers[1] + enabled = !local.header_enabled + } +} + +# Test 4.2: for_each over local values +resource "cloudflare_managed_headers" "multi_zone" { + for_each = local.zones + + zone_id = each.value + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } +} + +# ======================================== +# Test Case 5: Boolean Edge Cases +# ======================================== + +# Test 5.1: All headers enabled +resource "cloudflare_managed_headers" "all_enabled" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = true + } + + managed_response_headers { + id = "remove_server_header" + enabled = true + } +} + +# Test 5.2: All headers disabled +resource "cloudflare_managed_headers" "all_disabled" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } + + managed_response_headers { + id = "remove_server_header" + enabled = false + } +} + +# Test 5.3: Mixed enabled/disabled states +resource "cloudflare_managed_headers" "mixed_states" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } + + managed_response_headers { + id = "add_security_headers" + enabled = true + } + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = false + } +} + +# ======================================== +# Test Case 6: Header ID Variations +# ======================================== + +# Test 6.1: Header IDs with underscores +resource "cloudflare_managed_headers" "underscores" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } +} + +# Test 6.2: Header IDs with dashes +resource "cloudflare_managed_headers" "dashes" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add-custom-header" + enabled = true + } +} + +# ======================================== +# Test Case 7: Lifecycle Meta-Arguments +# ======================================== + +# Test 7.1: Resource with lifecycle +resource "cloudflare_managed_headers" "with_lifecycle" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + lifecycle { + create_before_destroy = true + } +} + +# Test 7.2: Resource with prevent_destroy +resource "cloudflare_managed_headers" "prevent_destroy" { + zone_id = var.cloudflare_zone_id + + managed_response_headers { + id = "add_security_headers" + enabled = true + } + + lifecycle { + prevent_destroy = false + } +} + +# ======================================== +# Test Case 8: Comments Preservation +# ======================================== + +# Test 8.1: Resource with inline comments +resource "cloudflare_managed_headers" "with_comments" { + zone_id = var.cloudflare_zone_id # Primary zone + + # Enable client IP headers for security + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true # Always enabled + } + + # Security headers for response + managed_response_headers { + id = "add_security_headers" + enabled = true + } +} + +# ======================================== +# Test Case 9: Count with Index +# ======================================== + +variable "header_count" { + type = number + default = 2 +} + +# Test 9.1: Resource with count +resource "cloudflare_managed_headers" "with_count" { + count = var.header_count + + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = count.index == 0 + } +} + +# ======================================== +# Test Case 10: String Interpolation +# ======================================== + +variable "environment" { + type = string + default = "test" +} + +# Test 10.1: Using string interpolation (for resource naming only) +resource "cloudflare_managed_headers" "interpolated" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = var.environment == "production" + } +} + +# ======================================== +# Test Case 11: Ternary Operators +# ======================================== + +# Test 11.1: Ternary in enabled field +resource "cloudflare_managed_headers" "ternary" { + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = var.enable_security_headers ? true : false + } +} + +# ======================================== +# Test Case 12: for_each with toset +# ======================================== + +variable "zones_list" { + type = list(string) + default = ["zone_a", "zone_b"] +} + +# Test 12.1: for_each with toset conversion +resource "cloudflare_managed_headers" "from_list" { + for_each = toset(var.zones_list) + + zone_id = var.cloudflare_zone_id + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } +} + +# ======================================== +# Summary: 20+ resource instances total +# ======================================== +# - Basic configurations: 5 resources +# - Conditional (count): 1 resource (potentially 0-1 instances) +# - for_each with maps: 2 resources (2 instances each = 4 instances) +# - Locals: 3 resources (2 for multi_zone = 4 instances) +# - Boolean edge cases: 3 resources +# - Header ID variations: 2 resources +# - Lifecycle: 2 resources +# - Comments: 1 resource +# - Count: 1 resource (2 instances) +# - Interpolation: 1 resource +# - Ternary: 1 resource +# - toset: 1 resource (2 instances) +# Total: ~25+ instances covering all Terraform patterns diff --git a/integration/v4_to_v5/testdata/managed_transforms/input/terraform.tfstate b/integration/v4_to_v5/testdata/managed_transforms/input/terraform.tfstate new file mode 100644 index 0000000..82841b5 --- /dev/null +++ b/integration/v4_to_v5/testdata/managed_transforms/input/terraform.tfstate @@ -0,0 +1,698 @@ +{ + "lineage": "test-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "minimal", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "request_only", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_x-powered-by_header" + }, + { + "enabled": false, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "response_only", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_bot_protection_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "both_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": true, + "id": "add_visitor_location_headers" + }, + { + "enabled": false, + "id": "add_bot_protection_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + }, + { + "enabled": true, + "id": "remove_x-powered-by_header" + }, + { + "enabled": false, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multiple_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "conditional_headers", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "production", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "env_specific[\"production\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "staging", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "env_specific[\"staging\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_locals", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone1", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multi_zone[\"zone1\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone2", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "multi_zone[\"zone2\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "all_enabled", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": false, + "id": "remove_server_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "all_disabled", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + }, + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + }, + { + "enabled": false, + "id": "remove_x-powered-by_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "mixed_states", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "remove_x-powered-by_header" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "underscores", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add-custom-header" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "dashes", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_lifecycle", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "prevent_destroy", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [ + { + "enabled": true, + "id": "add_security_headers" + } + ], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_comments", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": 0, + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_count[0]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": 1, + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "with_count[1]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": false, + "id": "add_visitor_location_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "interpolated", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "ternary", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone_a", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "from_list[\"zone_a\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + }, + { + "index_key": "zone_b", + "instances": [ + { + "attributes": { + "id": "e2edd3d8885f9b2d75a3158dc9bd0e55", + "managed_request_headers": [ + { + "enabled": true, + "id": "add_true_client_ip_headers" + } + ], + "managed_response_headers": [], + "zone_id": "e2edd3d8885f9b2d75a3158dc9bd0e55" + }, + "private": "bnVsbA==", + "schema_version": 0, + "sensitive_attributes": [] + } + ], + "mode": "managed", + "name": "from_list[\"zone_b\"]", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_managed_transforms" + } + ], + "serial": 1, + "terraform_version": "1.5.0", + "version": 4 +} \ No newline at end of file diff --git a/internal/hcl/helpers.go b/internal/hcl/helpers.go index 9e1adf4..c5c2075 100644 --- a/internal/hcl/helpers.go +++ b/internal/hcl/helpers.go @@ -257,4 +257,18 @@ func TokensForSimpleValue(val interface{}) hclwrite.Tokens { default: return nil } +} + +// BuildArrayFromObjects creates array tokens from multiple object tokens +// Useful for converting multiple blocks to an array attribute +func BuildArrayFromObjects(objects []hclwrite.Tokens) hclwrite.Tokens { + if len(objects) == 0 { + return TokensForEmptyArray() + } + return hclwrite.TokensForTuple(objects) +} + +// TokensForEmptyArray creates tokens for an empty array [] +func TokensForEmptyArray() hclwrite.Tokens { + return hclwrite.TokensForTuple([]hclwrite.Tokens{}) } \ No newline at end of file diff --git a/internal/registry/registry.go b/internal/registry/registry.go index ced3d27..7b5891f 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -5,14 +5,15 @@ import ( "github.com/cloudflare/tf-migrate/internal/resources/api_token" "github.com/cloudflare/tf-migrate/internal/resources/dns_record" "github.com/cloudflare/tf-migrate/internal/resources/logpull_retention" + "github.com/cloudflare/tf-migrate/internal/resources/managed_transforms" "github.com/cloudflare/tf-migrate/internal/resources/notification_policy_webhooks" "github.com/cloudflare/tf-migrate/internal/resources/r2_bucket" "github.com/cloudflare/tf-migrate/internal/resources/workers_kv" "github.com/cloudflare/tf-migrate/internal/resources/workers_kv_namespace" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_access_service_token" + "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_device_posture_rule" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_dlp_custom_profile" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_gateway_policy" - "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_device_posture_rule" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_list" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_tunnel_cloudflared" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_tunnel_cloudflared_route" @@ -28,6 +29,7 @@ func RegisterAllMigrations() { dns_record.NewV4ToV5Migrator() zone_dnssec.NewV4ToV5Migrator() logpull_retention.NewV4ToV5Migrator() + managed_transforms.NewV4ToV5Migrator() notification_policy_webhooks.NewV4ToV5Migrator() r2_bucket.NewV4ToV5Migrator() workers_kv.NewV4ToV5Migrator() diff --git a/internal/resources/managed_transforms/v4_to_v5.go b/internal/resources/managed_transforms/v4_to_v5.go new file mode 100644 index 0000000..dd72a9f --- /dev/null +++ b/internal/resources/managed_transforms/v4_to_v5.go @@ -0,0 +1,104 @@ +package managed_transforms + +import ( + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/cloudflare/tf-migrate/internal" + "github.com/cloudflare/tf-migrate/internal/transform" + tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl" +) + +type V4ToV5Migrator struct { +} + +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + // Register the OLD (v4) resource name + internal.RegisterMigrator("cloudflare_managed_headers", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + // Return the NEW (v5) resource name + return "cloudflare_managed_transforms" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + // Check for the OLD (v4) resource name + return resourceType == "cloudflare_managed_headers" +} + +func (m *V4ToV5Migrator) Preprocess(content string) string { + // No preprocessing needed - block to attribute conversion handled in TransformConfig + return content +} + +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + // Rename resource type + tfhcl.RenameResourceType(block, "cloudflare_managed_headers", "cloudflare_managed_transforms") + + body := block.Body() + + // Convert managed_request_headers blocks to array attribute + // Set empty array if no blocks found (since v5 requires this field) + tfhcl.ConvertBlocksToArrayAttribute(body, "managed_request_headers", true) + + // Convert managed_response_headers blocks to array attribute + // Set empty array if no blocks found (since v5 requires this field) + tfhcl.ConvertBlocksToArrayAttribute(body, "managed_response_headers", true) + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +func (m *V4ToV5Migrator) TransformState(ctx *transform.Context, stateJSON gjson.Result, resourcePath string, resourceName string) (string, error) { + result := stateJSON.String() + + // Get the attributes + attrs := stateJSON.Get("attributes") + if !attrs.Exists() { + return result, nil + } + + // Handle managed_request_headers - ensure it's an array (even if empty) + // v5 requires this field, v4 allowed null + if !attrs.Get("managed_request_headers").Exists() || attrs.Get("managed_request_headers").Raw == "null" { + result, _ = sjson.Set(result, "attributes.managed_request_headers", []interface{}{}) + } + + // Handle managed_response_headers - ensure it's an array (even if empty) + // v5 requires this field, v4 allowed null + if !attrs.Get("managed_response_headers").Exists() || attrs.Get("managed_response_headers").Raw == "null" { + result, _ = sjson.Set(result, "attributes.managed_response_headers", []interface{}{}) + } + + // Set schema_version to 0 for v5 + result, _ = sjson.Set(result, "schema_version", 0) + + return result, nil +} + +// convertBlocksToArrayAttribute converts multiple blocks of a given type to an array attribute +// This handles the v4 block syntax -> v5 attribute syntax transformation +// +// v4 block syntax: +// managed_request_headers { +// id = "header_id" +// enabled = true +// } +// +// v5 attribute syntax: +// managed_request_headers = [ +// { +// id = "header_id" +// enabled = true +// } +// ] +func convertBlocksToArrayAttribute(body *hclwrite.Body, blockType string) { + // TODO: Implement in Subtask 4 + // This is a placeholder for now - will be implemented when we do config transformations +} diff --git a/internal/resources/managed_transforms/v4_to_v5_test.go b/internal/resources/managed_transforms/v4_to_v5_test.go new file mode 100644 index 0000000..ec5be9d --- /dev/null +++ b/internal/resources/managed_transforms/v4_to_v5_test.go @@ -0,0 +1,517 @@ +package managed_transforms + +import ( + "testing" + + "github.com/cloudflare/tf-migrate/internal/testhelpers" +) + +func TestV4ToV5Transformation(t *testing.T) { + t.Run("ConfigTransformation", func(t *testing.T) { + t.Run("BasicTransformations", testBasicConfig) + t.Run("MultipleBlocks", testMultipleBlocks) + t.Run("EdgeCases", testConfigEdgeCases) + t.Run("MultipleResources", testMultipleResources) + }) + + t.Run("StateTransformation", func(t *testing.T) { + t.Run("NullHandling", testStateNullHandling) + t.Run("DataPreservation", testStateDataPreservation) + t.Run("EdgeCases", testStateEdgeCases) + }) +} + +func testBasicConfig(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.ConfigTestCase{ + { + Name: "Minimal resource - no headers", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + managed_request_headers = [] + managed_response_headers = [] +}`, + }, + { + Name: "Single request header", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [] +}`, + }, + { + Name: "Single response header", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }] +}`, + }, + { + Name: "One of each header type", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_response_headers { + id = "add_security_headers" + enabled = false + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [{ + id = "add_security_headers" + enabled = false + }] +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) +} + +func testMultipleBlocks(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.ConfigTestCase{ + { + Name: "Multiple request headers", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } + + managed_request_headers { + id = "add_bot_protection_headers" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }, { + id = "add_visitor_location_headers" + enabled = false + }, { + id = "add_bot_protection_headers" + enabled = true + }] + managed_response_headers = [] +}`, + }, + { + Name: "Multiple response headers", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } + + managed_response_headers { + id = "add_security_headers" + enabled = false + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }, { + id = "add_security_headers" + enabled = false + }] +}`, + }, + { + Name: "Multiple headers of both types with mixed enabled states", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } + + managed_request_headers { + id = "add_visitor_location_headers" + enabled = false + } + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } + + managed_response_headers { + id = "add_security_headers" + enabled = false + } + + managed_response_headers { + id = "remove_server_header" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }, { + id = "add_visitor_location_headers" + enabled = false + }] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }, { + id = "add_security_headers" + enabled = false + }, { + id = "remove_server_header" + enabled = true + }] +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) +} + +func testConfigEdgeCases(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.ConfigTestCase{ + { + Name: "Boolean false values preserved", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "header_1" + enabled = false + } + + managed_request_headers { + id = "header_2" + enabled = false + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "header_1" + enabled = false + }, { + id = "header_2" + enabled = false + }] + managed_response_headers = [] +}`, + }, + { + Name: "Headers with special characters in IDs", + Input: `resource "cloudflare_managed_headers" "example" { + zone_id = "abc123" + + managed_request_headers { + id = "add_x-custom-header" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "example" { + zone_id = "abc123" + + managed_request_headers = [{ + id = "add_x-custom-header" + enabled = true + }] + managed_response_headers = [] +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) +} + +func testMultipleResources(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.ConfigTestCase{ + { + Name: "Two resources in same file", + Input: `resource "cloudflare_managed_headers" "zone1" { + zone_id = "zone-1" + + managed_request_headers { + id = "add_true_client_ip_headers" + enabled = true + } +} + +resource "cloudflare_managed_headers" "zone2" { + zone_id = "zone-2" + + managed_response_headers { + id = "remove_x-powered-by_header" + enabled = true + } +}`, + Expected: `resource "cloudflare_managed_transforms" "zone1" { + zone_id = "zone-1" + + managed_request_headers = [{ + id = "add_true_client_ip_headers" + enabled = true + }] + managed_response_headers = [] +} + +resource "cloudflare_managed_transforms" "zone2" { + zone_id = "zone-2" + + managed_request_headers = [] + managed_response_headers = [{ + id = "remove_x-powered-by_header" + enabled = true + }] +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) +} + +func testStateNullHandling(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.StateTestCase{ + { + Name: "Null headers become empty arrays", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": null, + "managed_response_headers": null + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [], + "managed_response_headers": [] + } +}`, + }, + { + Name: "One null, one with data", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "add_true_client_ip_headers", + "enabled": true + }], + "managed_response_headers": null + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "add_true_client_ip_headers", + "enabled": true + }], + "managed_response_headers": [] + } +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) +} + +func testStateDataPreservation(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.StateTestCase{ + { + Name: "Existing data preserved", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "add_true_client_ip_headers", + "enabled": true + }, { + "id": "add_visitor_location_headers", + "enabled": false + }], + "managed_response_headers": [{ + "id": "remove_x-powered-by_header", + "enabled": true + }] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "add_true_client_ip_headers", + "enabled": true + }, { + "id": "add_visitor_location_headers", + "enabled": false + }], + "managed_response_headers": [{ + "id": "remove_x-powered-by_header", + "enabled": true + }] + } +}`, + }, + { + Name: "Empty arrays preserved", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [], + "managed_response_headers": [] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [], + "managed_response_headers": [] + } +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) +} + +func testStateEdgeCases(t *testing.T) { + migrator := NewV4ToV5Migrator() + + tests := []testhelpers.StateTestCase{ + { + Name: "Boolean false values preserved in state", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "header_1", + "enabled": false + }, { + "id": "header_2", + "enabled": false + }], + "managed_response_headers": [] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [{ + "id": "header_1", + "enabled": false + }, { + "id": "header_2", + "enabled": false + }], + "managed_response_headers": [] + } +}`, + }, + { + Name: "Schema version set correctly", + Input: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [], + "managed_response_headers": [] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "zone_id": "abc123", + "managed_request_headers": [], + "managed_response_headers": [] + } +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) +} diff --git a/internal/transform/hcl/attributes.go b/internal/transform/hcl/attributes.go index 31e2e5d..c71ec27 100644 --- a/internal/transform/hcl/attributes.go +++ b/internal/transform/hcl/attributes.go @@ -139,6 +139,35 @@ func ExtractStringFromAttribute(attr *hclwrite.Attribute) string { return "" } +// ExtractBoolFromAttribute extracts a boolean value from an HCL attribute. +// Returns the boolean value and true if successful, or false and false if not found/invalid. +// +// Example usage: +// enabledAttr := body.GetAttribute("enabled") +// value, ok := ExtractBoolFromAttribute(enabledAttr) +// // Returns (true, true) from: enabled = true +// // Returns (false, true) from: enabled = false +// // Returns (false, false) from: enabled = null or missing +func ExtractBoolFromAttribute(attr *hclwrite.Attribute) (bool, bool) { + if attr == nil { + return false, false + } + + tokens := attr.Expr().BuildTokens(nil) + for _, token := range tokens { + if token.Type == hclsyntax.TokenIdent { + val := string(token.Bytes) + if val == "true" { + return true, true + } + if val == "false" { + return false, true + } + } + } + return false, false +} + // HasAttribute checks if an attribute exists in the body func HasAttribute(body *hclwrite.Body, attrName string) bool { return body.GetAttribute(attrName) != nil diff --git a/internal/transform/hcl/blocks.go b/internal/transform/hcl/blocks.go index e2adf7c..525e852 100644 --- a/internal/transform/hcl/blocks.go +++ b/internal/transform/hcl/blocks.go @@ -224,9 +224,57 @@ func ConvertSingleBlockToAttribute(body *hclwrite.Body, blockType, attrName stri if block == nil { return false } - + objTokens := hcl.BuildObjectFromBlock(block) body.SetAttributeRaw(attrName, objTokens) body.RemoveBlock(block) return true -} \ No newline at end of file +} + +// ConvertBlocksToArrayAttribute converts multiple blocks to an array attribute +// This is useful when migrating from v4 block syntax to v5 array attribute syntax +// +// Example - Converting managed headers: +// +// Before: +// managed_request_headers { +// id = "header_1" +// enabled = true +// } +// managed_request_headers { +// id = "header_2" +// enabled = false +// } +// +// After calling ConvertBlocksToArrayAttribute(body, "managed_request_headers"): +// managed_request_headers = [ +// { id = "header_1", enabled = true }, +// { id = "header_2", enabled = false } +// ] +// +// If no blocks are found and emptyIfNone is true, sets an empty array []. +func ConvertBlocksToArrayAttribute(body *hclwrite.Body, blockType string, emptyIfNone bool) { + blocks := FindBlocksByType(body, blockType) + + if len(blocks) == 0 { + if emptyIfNone { + body.SetAttributeRaw(blockType, hcl.TokensForEmptyArray()) + } + return + } + + // Convert each block to object tokens + var objectTokens []hclwrite.Tokens + for _, block := range blocks { + objTokens := hcl.BuildObjectFromBlock(block) + objectTokens = append(objectTokens, objTokens) + } + + // Build array tokens from the objects and set as attribute + arrayTokens := hcl.BuildArrayFromObjects(objectTokens) + body.SetAttributeRaw(blockType, arrayTokens) + + // Remove all original blocks + RemoveBlocksByType(body, blockType) +} +