diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e015663d..55e14cbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,15 +31,8 @@ jobs: go mod download go mod tidy - - name: Run unit tests with coverage - run: | - go test -v -race -coverprofile=coverage.out -covermode=atomic ./internal/... - go test -v -race ./cmd/... - - - name: Generate coverage report - run: | - echo "Coverage Report:" - go tool cover -func=coverage.out + - name: Run unit tests + run: make test-unit lint-testdata: name: Lint Testdata Naming @@ -82,12 +75,5 @@ jobs: go mod download go mod tidy - - name: Build tf-migrate - run: | - make build - ./bin/tf-migrate version - - - name: Run integration tests for v4 to v5 - run: | - cd integration/v4_to_v5 - go test -v -race -timeout 10m + - name: Run integration tests + run: make test-integration diff --git a/Makefile b/Makefile index 2a68b819..9180fa9e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ GO := go MAIN_PACKAGE := ./cmd/tf-migrate E2E_PACKAGE := ./cmd/e2e-runner -.PHONY: all build build-e2e build-all test test-e2e lint-testdata clean +.PHONY: all build build-e2e build-all test test-unit test-integration lint-testdata clean # Default target: build all binaries all: build-all @@ -24,13 +24,16 @@ build-e2e: # Build both binaries build-all: build build-e2e -# Run tests -test: +# Run all tests (unit + e2e + integration) +test: test-unit test-integration + +# Run unit tests +test-unit: $(GO) test -v -race ./internal/... -# Run e2e-runner tests only -test-e2e: - $(GO) test -v -race ./internal/e2e-runner +# Run integration tests +test-integration: + $(GO) test -v -race ./integration/... # Lint testdata to ensure all resources have cftftest prefix lint-testdata: @@ -42,4 +45,4 @@ clean: @echo "Cleaning build artifacts..." @rm -rf bin/ @rm -f tf-migrate e2e-runner - @echo "Clean complete" \ No newline at end of file + @echo "Clean complete" diff --git a/e2e/drift-exemptions.yaml b/e2e/drift-exemptions.yaml index 6ef33bf7..d85c2a95 100644 --- a/e2e/drift-exemptions.yaml +++ b/e2e/drift-exemptions.yaml @@ -9,6 +9,7 @@ exemptions: description: "Ignore attributes that refresh to 'known after apply'" patterns: - "(known after apply)" + - '= \{\} -> null' enabled: true # Ignore status changes to "active" (common for DNSSEC resources) @@ -20,15 +21,6 @@ exemptions: - 'status.*->.*"active"' enabled: true - # Ignore empty nested objects being normalized to null in access policy condition elements - - name: "access_policy_empty_objects_to_null" - description: "Empty nested objects in condition elements normalized to null (terraform init behavior)" - resource_types: - - "cloudflare_zero_trust_access_policy" - patterns: - - '= \{\} -> null' - enabled: true - # Ignore nested object restructuring in access policy condition elements - name: "access_policy_nested_object_restructuring" description: "Nested objects in condition elements being restructured (empty object removal)" diff --git a/integration/v4_to_v5/integration_test.go b/integration/v4_to_v5/integration_test.go index 9e002d44..467eb2ab 100644 --- a/integration/v4_to_v5/integration_test.go +++ b/integration/v4_to_v5/integration_test.go @@ -12,6 +12,8 @@ import ( _ "github.com/cloudflare/tf-migrate/internal/resources/custom_pages" _ "github.com/cloudflare/tf-migrate/internal/resources/dns_record" _ "github.com/cloudflare/tf-migrate/internal/resources/healthcheck" + _ "github.com/cloudflare/tf-migrate/internal/resources/load_balancer" + _ "github.com/cloudflare/tf-migrate/internal/resources/load_balancer_pool" _ "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/page_rule" diff --git a/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer.tf b/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer.tf new file mode 100644 index 00000000..19dfeb5a --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer.tf @@ -0,0 +1,136 @@ +# Minimal load_balancer testdata for v4 to v5 migration +# This file uses v4 schema which has major breaking changes in v5 + +# Variables for DRY configuration +variable "cloudflare_account_id" { + type = string + default = "f037e56e89293a057740de681ac9abbe" +} + +variable "cloudflare_zone_id" { + type = string + default = "0da42c8d2132a9ddaf714f9e7c920711" +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +# Locals for naming consistency +locals { + name_prefix = "cftftest" +} + +# Note: Load balancer resources require load_balancer_pool resources +# These are not included here as they would need their own v4 to v5 migration +# For now, this file only tests the load_balancer resource schema changes + +# 1. Basic load balancer (v4 schema) +# v4 uses: default_pool_ids, fallback_pool_id +# v5 uses: default_pools, fallback_pool +resource "cloudflare_load_balancer" "basic" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-basic-lb.${var.cloudflare_domain}" + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" +} + +# 2. Load balancer with session_affinity_attributes (v4 block, v5 map) +resource "cloudflare_load_balancer" "with_affinity" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-affinity-lb.${var.cloudflare_domain}" + session_affinity = "cookie" + session_affinity_ttl = 3600 + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + session_affinity_attributes = { + samesite = "Lax" + secure = "Always" + } +} + +# 3. Load balancer with region_pools (v4 blocks, v5 map) +resource "cloudflare_load_balancer" "with_region_pools" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-region-lb.${var.cloudflare_domain}" + + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + region_pools = { + "WNAM" = ["pool-id-1"] + "ENAM" = ["pool-id-2"] + } +} + +# 4. Load balancer with adaptive_routing (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_adaptive_routing" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-adaptive-lb.${var.cloudflare_domain}" + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + adaptive_routing = { + failover_across_pools = false + } +} + +# 5. Load balancer with location_strategy (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_location_strategy" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-location-lb.${var.cloudflare_domain}" + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } +} + +# 6. Load balancer with random_steering (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_random_steering" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-random-lb.${var.cloudflare_domain}" + steering_policy = "random" + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + random_steering = { + default_weight = 0.5 + pool_weights = { + "pool-id-1" = 0.7 + } + } +} + +# 7. Load balancer with all single-object attributes (comprehensive test) +resource "cloudflare_load_balancer" "with_all_attributes" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-all-attrs-lb.${var.cloudflare_domain}" + session_affinity = "cookie" + session_affinity_ttl = 3600 + steering_policy = "random" + + + + + default_pools = ["pool-id-1"] + fallback_pool = "pool-id-fallback" + session_affinity_attributes = { + samesite = "Lax" + secure = "Always" + } + adaptive_routing = { + failover_across_pools = false + } + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } + random_steering = { + default_weight = 0.5 + } +} diff --git a/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer_e2e.tf b/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer_e2e.tf new file mode 100644 index 00000000..cc05e609 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/expected/load_balancer_e2e.tf @@ -0,0 +1,161 @@ +# E2E Test: cloudflare_load_balancer +# Minimal e2e test that can be applied with real infrastructure +# Creates pools and load balancers in the same module to avoid dependency issues + +variable "cloudflare_account_id" { + type = string +} + +variable "cloudflare_zone_id" { + type = string +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +locals { + name_prefix = "cftftest" +} + +########################## +# E2E TEST POOLS +########################## + +resource "cloudflare_load_balancer_pool" "lb_e2e_basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-lb-e2e-basic-pool" + minimum_origins = 1 + enabled = true + + origins = [{ + name = "origin-1" + address = "192.0.2.100" + enabled = true + }] +} + +resource "cloudflare_load_balancer_pool" "lb_e2e_fallback" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-lb-e2e-fallback-pool" + minimum_origins = 1 + enabled = true + + origins = [{ + name = "origin-fallback" + address = "192.0.2.101" + enabled = true + }] +} + +########################## +# E2E TEST LOAD BALANCERS +########################## + +# 1. Basic load balancer (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_basic" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-basic-lb.${var.cloudflare_domain}" + enabled = true + steering_policy = "off" + ttl = 30 + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id +} + +# 2. Load balancer with session_affinity_attributes (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_affinity" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-affinity-lb.${var.cloudflare_domain}" + session_affinity = "cookie" + session_affinity_ttl = 3600 + + + enabled = true + steering_policy = "off" + ttl = 30 + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id + session_affinity_attributes = { + samesite = "Lax" + secure = "Always" + } +} + +# 3. Load balancer with adaptive_routing (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_adaptive_routing" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-adaptive-lb.${var.cloudflare_domain}" + enabled = true + steering_policy = "off" + ttl = 30 + + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id + adaptive_routing = { + failover_across_pools = false + } +} + +# 4. Load balancer with location_strategy (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_location_strategy" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-location-lb.${var.cloudflare_domain}" + enabled = true + steering_policy = "off" + ttl = 30 + + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } +} + +# 5. Load balancer with random_steering (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_random_steering" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-random-lb.${var.cloudflare_domain}" + enabled = true + steering_policy = "random" + ttl = 30 + + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id + random_steering = { + default_weight = 0.5 + } +} + +# 6. Load balancer with all single-object attributes (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_all_attributes" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-all-attrs-lb.${var.cloudflare_domain}" + session_affinity = "cookie" + session_affinity_ttl = 3600 + enabled = true + steering_policy = "random" + ttl = 30 + + + + + default_pools = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool = cloudflare_load_balancer_pool.lb_e2e_fallback.id + session_affinity_attributes = { + samesite = "Lax" + secure = "Always" + } + adaptive_routing = { + failover_across_pools = false + } + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } + random_steering = { + default_weight = 0.5 + } +} diff --git a/integration/v4_to_v5/testdata/load_balancer/expected/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer/expected/terraform.tfstate new file mode 100644 index 00000000..f5a91b7a --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/expected/terraform.tfstate @@ -0,0 +1,312 @@ +{ + "lineage": "test-load-balancer-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "lb-basic-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-basic-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-ttl-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-ttl-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": 30, + "description": "Load balancer with custom TTL", + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_ttl", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-steering-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-steering-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "geo", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_steering", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-affinity-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-affinity-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "cookie", + "session_affinity_ttl": 3600, + "session_affinity_attributes": { + "samesite": "Lax", + "secure": "Always" + }, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_session_affinity", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-multi-pool-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-multi-pool-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id", "pool-secondary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "multi_pool", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-adaptive-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-adaptive-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": {"failover_across_pools": false}, + "location_strategy": null, + "random_steering": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_adaptive_routing", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-location-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-location-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": null, + "location_strategy": {"prefer_ecs": "proximity", "mode": "pop"}, + "random_steering": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_location_strategy", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-random-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-random-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "random", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": null, + "location_strategy": null, + "random_steering": {"default_weight": 0.5, "pool_weights": {"pool-id-1": 0.7}}, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_random_steering", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-all-attrs-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-all-attrs-lb.cf-tf-test.com", + "default_pools": ["pool-primary-id"], + "fallback_pool": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "random", + "session_affinity": "cookie", + "session_affinity_ttl": 3600, + "session_affinity_attributes": {"samesite": "Lax", "secure": "Always"}, + "adaptive_routing": {"failover_across_pools": false}, + "location_strategy": {"prefer_ecs": "proximity", "mode": "pop"}, + "random_steering": {"default_weight": 0.5}, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_all_single_object_attrs", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/load_balancer/input/load_balancer.tf b/integration/v4_to_v5/testdata/load_balancer/input/load_balancer.tf new file mode 100644 index 00000000..d14d1884 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/input/load_balancer.tf @@ -0,0 +1,140 @@ +# Minimal load_balancer testdata for v4 to v5 migration +# This file uses v4 schema which has major breaking changes in v5 + +# Variables for DRY configuration +variable "cloudflare_account_id" { + type = string + default = "f037e56e89293a057740de681ac9abbe" +} + +variable "cloudflare_zone_id" { + type = string + default = "0da42c8d2132a9ddaf714f9e7c920711" +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +# Locals for naming consistency +locals { + name_prefix = "cftftest" +} + +# Note: Load balancer resources require load_balancer_pool resources +# These are not included here as they would need their own v4 to v5 migration +# For now, this file only tests the load_balancer resource schema changes + +# 1. Basic load balancer (v4 schema) +# v4 uses: default_pool_ids, fallback_pool_id +# v5 uses: default_pools, fallback_pool +resource "cloudflare_load_balancer" "basic" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-basic-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" +} + +# 2. Load balancer with session_affinity_attributes (v4 block, v5 map) +resource "cloudflare_load_balancer" "with_affinity" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-affinity-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + session_affinity = "cookie" + session_affinity_ttl = 3600 + + session_affinity_attributes { + samesite = "Lax" + secure = "Always" + } +} + +# 3. Load balancer with region_pools (v4 blocks, v5 map) +resource "cloudflare_load_balancer" "with_region_pools" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-region-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + + region_pools { + region = "WNAM" + pool_ids = ["pool-id-1"] + } + + region_pools { + region = "ENAM" + pool_ids = ["pool-id-2"] + } +} + +# 4. Load balancer with adaptive_routing (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_adaptive_routing" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-adaptive-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + + adaptive_routing { + failover_across_pools = false + } +} + +# 5. Load balancer with location_strategy (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_location_strategy" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-location-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } +} + +# 6. Load balancer with random_steering (v4 block, v5 single object) +resource "cloudflare_load_balancer" "with_random_steering" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-random-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + steering_policy = "random" + + random_steering { + default_weight = 0.5 + pool_weights = { + "pool-id-1" = 0.7 + } + } +} + +# 7. Load balancer with all single-object attributes (comprehensive test) +resource "cloudflare_load_balancer" "with_all_attributes" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-all-attrs-lb.${var.cloudflare_domain}" + default_pool_ids = ["pool-id-1"] + fallback_pool_id = "pool-id-fallback" + session_affinity = "cookie" + session_affinity_ttl = 3600 + steering_policy = "random" + + session_affinity_attributes { + samesite = "Lax" + secure = "Always" + } + + adaptive_routing { + failover_across_pools = false + } + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } + + random_steering { + default_weight = 0.5 + } +} diff --git a/integration/v4_to_v5/testdata/load_balancer/input/load_balancer_e2e.tf b/integration/v4_to_v5/testdata/load_balancer/input/load_balancer_e2e.tf new file mode 100644 index 00000000..7e2786e0 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/input/load_balancer_e2e.tf @@ -0,0 +1,161 @@ +# E2E Test: cloudflare_load_balancer +# Minimal e2e test that can be applied with real infrastructure +# Creates pools and load balancers in the same module to avoid dependency issues + +variable "cloudflare_account_id" { + type = string +} + +variable "cloudflare_zone_id" { + type = string +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +locals { + name_prefix = "cftftest" +} + +########################## +# E2E TEST POOLS +########################## + +resource "cloudflare_load_balancer_pool" "lb_e2e_basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-lb-e2e-basic-pool" + minimum_origins = 1 + enabled = true + + origins { + name = "origin-1" + address = "192.0.2.100" + enabled = true + } +} + +resource "cloudflare_load_balancer_pool" "lb_e2e_fallback" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-lb-e2e-fallback-pool" + minimum_origins = 1 + enabled = true + + origins { + name = "origin-fallback" + address = "192.0.2.101" + enabled = true + } +} + +########################## +# E2E TEST LOAD BALANCERS +########################## + +# 1. Basic load balancer (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_basic" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-basic-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + enabled = true + steering_policy = "off" + ttl = 30 +} + +# 2. Load balancer with session_affinity_attributes (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_affinity" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-affinity-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + session_affinity = "cookie" + session_affinity_ttl = 3600 + + session_affinity_attributes { + samesite = "Lax" + secure = "Always" + } + + enabled = true + steering_policy = "off" + ttl = 30 +} + +# 3. Load balancer with adaptive_routing (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_adaptive_routing" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-adaptive-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + enabled = true + steering_policy = "off" + ttl = 30 + + adaptive_routing { + failover_across_pools = false + } +} + +# 4. Load balancer with location_strategy (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_location_strategy" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-location-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + enabled = true + steering_policy = "off" + ttl = 30 + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } +} + +# 5. Load balancer with random_steering (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_random_steering" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-random-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + enabled = true + steering_policy = "random" + ttl = 30 + + random_steering { + default_weight = 0.5 + } +} + +# 6. Load balancer with all single-object attributes (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer" "e2e_all_attributes" { + zone_id = var.cloudflare_zone_id + name = "${local.name_prefix}-e2e-all-attrs-lb.${var.cloudflare_domain}" + default_pool_ids = [cloudflare_load_balancer_pool.lb_e2e_basic.id] + fallback_pool_id = cloudflare_load_balancer_pool.lb_e2e_fallback.id + session_affinity = "cookie" + session_affinity_ttl = 3600 + enabled = true + steering_policy = "random" + ttl = 30 + + session_affinity_attributes { + samesite = "Lax" + secure = "Always" + } + + adaptive_routing { + failover_across_pools = false + } + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } + + random_steering { + default_weight = 0.5 + } +} diff --git a/integration/v4_to_v5/testdata/load_balancer/input/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer/input/terraform.tfstate new file mode 100644 index 00000000..27533e10 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer/input/terraform.tfstate @@ -0,0 +1,312 @@ +{ + "lineage": "test-load-balancer-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "lb-basic-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-basic-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-ttl-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-ttl-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": 30, + "description": "Load balancer with custom TTL", + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_ttl", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-steering-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-steering-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "geo", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_steering", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-affinity-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-affinity-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "cookie", + "session_affinity_ttl": 3600, + "session_affinity_attributes": { + "samesite": "Lax", + "secure": "Always" + }, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_session_affinity", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-multi-pool-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-multi-pool-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id", "pool-secondary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "multi_pool", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-adaptive-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-adaptive-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": [{"failover_across_pools": false}], + "location_strategy": [], + "random_steering": [], + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_adaptive_routing", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-location-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-location-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "off", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": [], + "location_strategy": [{"prefer_ecs": "proximity", "mode": "pop"}], + "random_steering": [], + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_location_strategy", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-random-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-random-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "random", + "session_affinity": "none", + "session_affinity_ttl": null, + "session_affinity_attributes": null, + "adaptive_routing": [], + "location_strategy": [], + "random_steering": [{"default_weight": 0.5, "pool_weights": {"pool-id-1": 0.7}}], + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_random_steering", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + }, + { + "instances": [ + { + "attributes": { + "id": "lb-all-attrs-id", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", + "name": "cftftest-all-attrs-lb.cf-tf-test.com", + "default_pool_ids": ["pool-primary-id"], + "fallback_pool_id": "pool-fallback-id", + "enabled": true, + "proxied": true, + "ttl": null, + "description": null, + "steering_policy": "random", + "session_affinity": "cookie", + "session_affinity_ttl": 3600, + "session_affinity_attributes": [{"samesite": "Lax", "secure": "Always"}], + "adaptive_routing": [{"failover_across_pools": false}], + "location_strategy": [{"prefer_ecs": "proximity", "mode": "pop"}], + "random_steering": [{"default_weight": 0.5}], + "region_pools": null, + "pop_pools": null, + "country_pools": null, + "rules": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_all_single_object_attrs", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool.tf b/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool.tf new file mode 100644 index 00000000..43d34ef4 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool.tf @@ -0,0 +1,159 @@ +# Comprehensive Integration Tests for cloudflare_load_balancer_pool +# This file tests v4 to v5 migration + +# Variables for DRY configuration +variable "cloudflare_account_id" { + type = string + default = "f037e56e89293a057740de681ac9abbe" +} + +variable "cloudflare_zone_id" { + type = string + default = "0da42c8d2132a9ddaf714f9e7c920711" +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing (not used by this module but accepted for consistency)" +} + +# Locals for naming consistency +locals { + name_prefix = "cftftest" +} + +########################## +# BASIC PATTERNS +########################## + +# 1. Basic pool with single origin (v4 uses origins block) +resource "cloudflare_load_balancer_pool" "basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-basic-pool" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] +} + +# 2. Pool with multiple origins +resource "cloudflare_load_balancer_pool" "multi_origin" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-multi-pool" + + + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + weight = 1 + }, { + name = "origin-2" + address = "192.0.2.2" + enabled = true + weight = 1 + }, { + name = "origin-3" + address = "192.0.2.3" + enabled = false + weight = 0.5 + }] +} + +# 3. Pool with origin headers +resource "cloudflare_load_balancer_pool" "with_headers" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-headers-pool" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] +} + +# 4. Pool with load_shedding (v4 block, v5 attribute) +resource "cloudflare_load_balancer_pool" "with_load_shedding" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-shedding-pool" + + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] + load_shedding = { + default_percent = 50 + default_policy = "random" + session_percent = 25 + session_policy = "hash" + } +} + +# 5. Pool with origin_steering (v4 block, v5 attribute) +resource "cloudflare_load_balancer_pool" "with_steering" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-steering-pool" + + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] + origin_steering = { + policy = "random" + } +} + +# 6. Pool with monitor +resource "cloudflare_load_balancer_pool" "with_monitor" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-monitored-pool" + monitor = "monitor-id-123" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] +} + +# 7. Pool with notification settings +resource "cloudflare_load_balancer_pool" "with_notifications" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-notified-pool" + notification_email = "alerts@example.com" + enabled = true + minimum_origins = 1 + check_regions = ["WEU", "ENAM"] + + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }, { + name = "origin-2" + address = "192.0.2.2" + enabled = true + }] +} + +# 8. for_each pattern +resource "cloudflare_load_balancer_pool" "foreach" { + for_each = toset(["pool1", "pool2"]) + + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-${each.key}" + + origins = [{ + name = "${each.key}-origin" + address = "192.0.2.100" + enabled = true + }] +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool_e2e.tf b/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool_e2e.tf new file mode 100644 index 00000000..93a645e6 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/expected/load_balancer_pool_e2e.tf @@ -0,0 +1,95 @@ +# E2E Test: cloudflare_load_balancer_pool +# Minimal e2e test that can be applied with real infrastructure + +variable "cloudflare_account_id" { + type = string +} + +variable "cloudflare_zone_id" { + type = string +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +locals { + name_prefix = "cftftest" +} + +########################## +# E2E TEST POOLS +########################## + +# 1. Basic pool with single origin (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-basic-pool" + + + minimum_origins = 1 + enabled = true + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] +} + +# 2. Pool with multiple origins (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_multi" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-multi-pool" + + + + minimum_origins = 1 + enabled = true + origins = [{ + name = "origin-1" + address = "192.0.2.10" + enabled = true + weight = 1 + }, { + name = "origin-2" + address = "192.0.2.11" + enabled = true + weight = 1 + }] +} + +# 3. Pool with load_shedding (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_shedding" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-shedding-pool" + + + + minimum_origins = 1 + enabled = true + origins = [{ + name = "origin-1" + address = "192.0.2.20" + enabled = true + }] + load_shedding = { + default_percent = 55 + default_policy = "random" + session_percent = 30 + session_policy = "hash" + } +} + +# Output pool IDs for use by load balancers +output "e2e_basic_pool_id" { + value = cloudflare_load_balancer_pool.e2e_basic.id +} + +output "e2e_multi_pool_id" { + value = cloudflare_load_balancer_pool.e2e_multi.id +} + +output "e2e_shedding_pool_id" { + value = cloudflare_load_balancer_pool.e2e_shedding.id +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform.tfstate new file mode 100644 index 00000000..47427e23 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform.tfstate @@ -0,0 +1,87 @@ +{ + "lineage": "test-load-balancer-pool-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "pool-basic-id", + "account_id": "f037e56e89293a057740de681ac9abbe", + "name": "cftftest-basic-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": {} + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer_pool" + }, + { + "instances": [ + { + "attributes": { + "id": "pool-multi-id", + "account_id": "f037e56e89293a057740de681ac9abbe", + "name": "cftftest-multi-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": {} + }, + { + "name": "origin-2", + "address": "192.0.2.2", + "enabled": true, + "weight": 1.0, + "header": {} + }, + { + "name": "origin-3", + "address": "192.0.2.3", + "enabled": false, + "weight": 0.5, + "header": {} + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "multi_origin", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer_pool" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform_e2e.tfstate b/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform_e2e.tfstate new file mode 100644 index 00000000..583d6f0a --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/expected/terraform_e2e.tfstate @@ -0,0 +1,133 @@ +{ + "version": 4, + "terraform_version": "1.0.0", + "serial": 1, + "lineage": "test-load-balancer-pool-e2e-lineage", + "outputs": { + "e2e_basic_pool_id": { + "value": "e2e-pool-basic-id", + "type": "string" + }, + "e2e_multi_pool_id": { + "value": "e2e-pool-multi-id", + "type": "string" + }, + "e2e_shedding_pool_id": { + "value": "e2e-pool-shedding-id", + "type": "string" + } + }, + "resources": [ + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-basic-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-basic-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_multi", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-multi-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-multi-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.10", + "enabled": true, + "weight": 1.0, + "header": [] + }, + { + "name": "origin-2", + "address": "192.0.2.11", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_shedding", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-shedding-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-shedding-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.20", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "load_shedding": [ + { + "default_percent": 55, + "default_policy": "random", + "session_percent": 30, + "session_policy": "hash" + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + } + ] +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool.tf b/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool.tf new file mode 100644 index 00000000..cac9e0f6 --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool.tf @@ -0,0 +1,162 @@ +# Comprehensive Integration Tests for cloudflare_load_balancer_pool +# This file tests v4 to v5 migration + +# Variables for DRY configuration +variable "cloudflare_account_id" { + type = string + default = "f037e56e89293a057740de681ac9abbe" +} + +variable "cloudflare_zone_id" { + type = string + default = "0da42c8d2132a9ddaf714f9e7c920711" +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing (not used by this module but accepted for consistency)" +} + +# Locals for naming consistency +locals { + name_prefix = "cftftest" +} + +########################## +# BASIC PATTERNS +########################## + +# 1. Basic pool with single origin (v4 uses origins block) +resource "cloudflare_load_balancer_pool" "basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-basic-pool" + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } +} + +# 2. Pool with multiple origins +resource "cloudflare_load_balancer_pool" "multi_origin" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-multi-pool" + + + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + weight = 1 + } + origins { + name = "origin-2" + address = "192.0.2.2" + enabled = true + weight = 1 + } + origins { + name = "origin-3" + address = "192.0.2.3" + enabled = false + weight = 0.5 + } +} + +# 3. Pool with origin headers +resource "cloudflare_load_balancer_pool" "with_headers" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-headers-pool" + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } +} + +# 4. Pool with load_shedding (v4 block, v5 attribute) +resource "cloudflare_load_balancer_pool" "with_load_shedding" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-shedding-pool" + + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } + load_shedding { + default_percent = 50 + default_policy = "random" + session_percent = 25 + session_policy = "hash" + } +} + +# 5. Pool with origin_steering (v4 block, v5 attribute) +resource "cloudflare_load_balancer_pool" "with_steering" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-steering-pool" + + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } + origin_steering { + policy = "random" + } +} + +# 6. Pool with monitor +resource "cloudflare_load_balancer_pool" "with_monitor" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-monitored-pool" + monitor = "monitor-id-123" + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } +} + +# 7. Pool with notification settings +resource "cloudflare_load_balancer_pool" "with_notifications" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-notified-pool" + notification_email = "alerts@example.com" + enabled = true + minimum_origins = 1 + check_regions = ["WEU", "ENAM"] + + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } + origins { + name = "origin-2" + address = "192.0.2.2" + enabled = true + } +} + +# 8. for_each pattern +resource "cloudflare_load_balancer_pool" "foreach" { + for_each = toset(["pool1", "pool2"]) + + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-${each.key}" + + origins { + name = "${each.key}-origin" + address = "192.0.2.100" + enabled = true + } +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool_e2e.tf b/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool_e2e.tf new file mode 100644 index 00000000..142b635f --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/input/load_balancer_pool_e2e.tf @@ -0,0 +1,96 @@ +# E2E Test: cloudflare_load_balancer_pool +# Minimal e2e test that can be applied with real infrastructure + +variable "cloudflare_account_id" { + type = string +} + +variable "cloudflare_zone_id" { + type = string +} + +variable "cloudflare_domain" { + type = string + description = "Domain for testing" +} + +locals { + name_prefix = "cftftest" +} + +########################## +# E2E TEST POOLS +########################## + +# 1. Basic pool with single origin (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_basic" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-basic-pool" + + + minimum_origins = 1 + enabled = true + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } +} + +# 2. Pool with multiple origins (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_multi" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-multi-pool" + + + + minimum_origins = 1 + enabled = true + origins { + name = "origin-1" + address = "192.0.2.10" + enabled = true + weight = 1 + } + origins { + name = "origin-2" + address = "192.0.2.11" + enabled = true + weight = 1 + } +} + +# 3. Pool with load_shedding (v4 syntax - will be migrated to v5) +resource "cloudflare_load_balancer_pool" "e2e_shedding" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix}-e2e-shedding-pool" + + + + minimum_origins = 1 + enabled = true + origins { + name = "origin-1" + address = "192.0.2.20" + enabled = true + } + load_shedding { + default_percent = 55 + default_policy = "random" + session_percent = 30 + session_policy = "hash" + } +} + +# Output pool IDs for use by load balancers +output "e2e_basic_pool_id" { + value = cloudflare_load_balancer_pool.e2e_basic.id +} + +output "e2e_multi_pool_id" { + value = cloudflare_load_balancer_pool.e2e_multi.id +} + +output "e2e_shedding_pool_id" { + value = cloudflare_load_balancer_pool.e2e_shedding.id +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform.tfstate b/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform.tfstate new file mode 100644 index 00000000..214f98bf --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform.tfstate @@ -0,0 +1,87 @@ +{ + "lineage": "test-load-balancer-pool-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "id": "pool-basic-id", + "account_id": "f037e56e89293a057740de681ac9abbe", + "name": "cftftest-basic-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer_pool" + }, + { + "instances": [ + { + "attributes": { + "id": "pool-multi-id", + "account_id": "f037e56e89293a057740de681ac9abbe", + "name": "cftftest-multi-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": [] + }, + { + "name": "origin-2", + "address": "192.0.2.2", + "enabled": true, + "weight": 1.0, + "header": [] + }, + { + "name": "origin-3", + "address": "192.0.2.3", + "enabled": false, + "weight": 0.5, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2023-01-01T00:00:00Z", + "modified_on": "2023-01-01T00:00:00Z" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "multi_origin", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_load_balancer_pool" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform_e2e.tfstate b/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform_e2e.tfstate new file mode 100644 index 00000000..583d6f0a --- /dev/null +++ b/integration/v4_to_v5/testdata/load_balancer_pool/input/terraform_e2e.tfstate @@ -0,0 +1,133 @@ +{ + "version": 4, + "terraform_version": "1.0.0", + "serial": 1, + "lineage": "test-load-balancer-pool-e2e-lineage", + "outputs": { + "e2e_basic_pool_id": { + "value": "e2e-pool-basic-id", + "type": "string" + }, + "e2e_multi_pool_id": { + "value": "e2e-pool-multi-id", + "type": "string" + }, + "e2e_shedding_pool_id": { + "value": "e2e-pool-shedding-id", + "type": "string" + } + }, + "resources": [ + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_basic", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-basic-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-basic-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_multi", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-multi-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-multi-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.10", + "enabled": true, + "weight": 1.0, + "header": [] + }, + { + "name": "origin-2", + "address": "192.0.2.11", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + }, + { + "mode": "managed", + "type": "cloudflare_load_balancer_pool", + "name": "e2e_shedding", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "e2e-pool-shedding-id", + "account_id": "394f312589f47dc33bc80a5a5f12f35f", + "name": "cftftest-e2e-shedding-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.20", + "enabled": true, + "weight": 1.0, + "header": [] + } + ], + "load_shedding": [ + { + "default_percent": 55, + "default_policy": "random", + "session_percent": 30, + "session_policy": "hash" + } + ], + "enabled": true, + "minimum_origins": 1, + "monitor": null, + "notification_email": null, + "check_regions": [], + "created_on": "2025-12-29T00:00:00Z", + "modified_on": "2025-12-29T00:00:00Z" + } + } + ] + } + ] +} diff --git a/integration/v4_to_v5/testdata/worker_route/expected/terraform.tfstate b/integration/v4_to_v5/testdata/worker_route/expected/terraform.tfstate index d118c4cf..64d7daf8 100644 --- a/integration/v4_to_v5/testdata/worker_route/expected/terraform.tfstate +++ b/integration/v4_to_v5/testdata/worker_route/expected/terraform.tfstate @@ -1,377 +1,377 @@ { - "version": 4, - "terraform_version": "1.5.0", - "serial": 1, "lineage": "test-worker-route-lineage", "outputs": {}, "resources": [ { - "mode": "managed", - "type": "cloudflare_workers_route", - "name": "minimal", - "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", "instances": [ { - "schema_version": 0, "attributes": { "id": "route-minimal-id", - "zone_id": "test-zone-id", - "pattern": "cftftest.cf-tf-test.com/*" - } + "pattern": "cftftest.cf-tf-test.com/*", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "full", + "name": "minimal", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-full-id", - "zone_id": "test-zone-id", "pattern": "cftftest-full.cf-tf-test.com/*", - "script": "cftftest-worker" - } + "script": "cftftest-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "wildcard_subdomain", + "name": "full", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-wildcard-id", - "zone_id": "test-zone-id", "pattern": "*.cftftest.cf-tf-test.com/*", - "script": "cftftest-wildcard-worker" - } + "script": "cftftest-wildcard-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "specific_path", + "name": "wildcard_subdomain", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-specific-path-id", - "zone_id": "test-zone-id", "pattern": "cftftest.cf-tf-test.com/api/*", - "script": "cftftest-api-worker" - } + "script": "cftftest-api-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "exact_path", + "name": "specific_path", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-exact-path-id", - "zone_id": "test-zone-id", "pattern": "cftftest.cf-tf-test.com/health", - "script": "cftftest-health-worker" - } + "script": "cftftest-health-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "api_routes", + "name": "exact_path", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "index_key": "api", - "schema_version": 0, "attributes": { "id": "route-api-routes-api-id", - "zone_id": "test-zone-id", "pattern": "api.cf-tf-test.com/*", - "script": "cftftest-api-worker" - } + "script": "cftftest-api-worker", + "zone_id": "test-zone-id" + }, + "index_key": "api", + "schema_version": 0 }, { - "index_key": "graphql", - "schema_version": 0, "attributes": { "id": "route-api-routes-graphql-id", - "zone_id": "test-zone-id", "pattern": "graphql.cf-tf-test.com/*", - "script": "cftftest-graphql-worker" - } + "script": "cftftest-graphql-worker", + "zone_id": "test-zone-id" + }, + "index_key": "graphql", + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "admin_routes", + "name": "api_routes", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "index_key": "admin.cf-tf-test.com/*", - "schema_version": 0, "attributes": { "id": "route-admin-1-id", - "zone_id": "test-zone-id", "pattern": "admin.cf-tf-test.com/*", - "script": "cftftest-admin-worker" - } + "script": "cftftest-admin-worker", + "zone_id": "test-zone-id" + }, + "index_key": "admin.cf-tf-test.com/*", + "schema_version": 0 }, { - "index_key": "manage.cf-tf-test.com/*", - "schema_version": 0, "attributes": { "id": "route-admin-2-id", - "zone_id": "test-zone-id", "pattern": "manage.cf-tf-test.com/*", - "script": "cftftest-admin-worker" - } + "script": "cftftest-admin-worker", + "zone_id": "test-zone-id" + }, + "index_key": "manage.cf-tf-test.com/*", + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "numbered", + "name": "admin_routes", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "index_key": 0, - "schema_version": 0, "attributes": { "id": "route-numbered-0-id", - "zone_id": "test-zone-id", "pattern": "cftftest-0.cf-tf-test.com/*", - "script": "cftftest-worker-0" - } + "script": "cftftest-worker-0", + "zone_id": "test-zone-id" + }, + "index_key": 0, + "schema_version": 0 }, { - "index_key": 1, - "schema_version": 0, "attributes": { "id": "route-numbered-1-id", - "zone_id": "test-zone-id", "pattern": "cftftest-1.cf-tf-test.com/*", - "script": "cftftest-worker-1" - } + "script": "cftftest-worker-1", + "zone_id": "test-zone-id" + }, + "index_key": 1, + "schema_version": 0 }, { - "index_key": 2, - "schema_version": 0, "attributes": { "id": "route-numbered-2-id", - "zone_id": "test-zone-id", "pattern": "cftftest-2.cf-tf-test.com/*", - "script": "cftftest-worker-2" - } + "script": "cftftest-worker-2", + "zone_id": "test-zone-id" + }, + "index_key": 2, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "conditional", + "name": "numbered", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "index_key": 0, - "schema_version": 0, "attributes": { "id": "route-conditional-id", - "zone_id": "test-zone-id", "pattern": "cftftest-conditional.cf-tf-test.com/*", - "script": "cftftest-conditional-worker" - } + "script": "cftftest-conditional-worker", + "zone_id": "test-zone-id" + }, + "index_key": 0, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "with_variables", + "name": "conditional", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-with-variables-id", - "zone_id": "test-zone-id", "pattern": "cftftest-var.cf-tf-test.com/*", - "script": "cftftest-var-worker" - } + "script": "cftftest-var-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "no_script_catchall", + "name": "with_variables", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-no-script-id", - "zone_id": "test-zone-id", - "pattern": "cftftest-fallback.cf-tf-test.com/*" - } + "pattern": "cftftest-fallback.cf-tf-test.com/*", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "multi_path_1", + "name": "no_script_catchall", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-multi-1-id", - "zone_id": "test-zone-id", "pattern": "cftftest-multi.cf-tf-test.com/api/*", - "script": "cftftest-api-handler" - } + "script": "cftftest-api-handler", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "multi_path_2", + "name": "multi_path_1", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-multi-2-id", - "zone_id": "test-zone-id", "pattern": "cftftest-multi.cf-tf-test.com/static/*", - "script": "cftftest-static-handler" - } + "script": "cftftest-static-handler", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "multi_path_3", + "name": "multi_path_2", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-multi-3-id", - "zone_id": "test-zone-id", - "pattern": "cftftest-multi.cf-tf-test.com/*" - } + "pattern": "cftftest-multi.cf-tf-test.com/*", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "query_params", + "name": "multi_path_3", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-query-id", - "zone_id": "test-zone-id", "pattern": "cftftest-query.cf-tf-test.com/*", - "script": "cftftest-query-worker" - } + "script": "cftftest-query-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "dashes_underscores", + "name": "query_params", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-dashes-id", - "zone_id": "test-zone-id", "pattern": "cftftest-test_route.cf-tf-test.com/*", - "script": "cftftest_dash_under_worker" - } + "script": "cftftest_dash_under_worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "environments", + "name": "dashes_underscores", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "index_key": "dev", - "schema_version": 0, "attributes": { "id": "route-env-dev-id", - "zone_id": "test-zone-id", "pattern": "cftftest-dev.cf-tf-test.com/*", - "script": "cftftest-dev-worker" - } + "script": "cftftest-dev-worker", + "zone_id": "test-zone-id" + }, + "index_key": "dev", + "schema_version": 0 }, { - "index_key": "prod", - "schema_version": 0, "attributes": { "id": "route-env-prod-id", - "zone_id": "test-zone-id", "pattern": "cftftest-prod.cf-tf-test.com/*", - "script": "cftftest-prod-worker" - } + "script": "cftftest-prod-worker", + "zone_id": "test-zone-id" + }, + "index_key": "prod", + "schema_version": 0 }, { - "index_key": "staging", - "schema_version": 0, "attributes": { "id": "route-env-staging-id", - "zone_id": "test-zone-id", "pattern": "cftftest-staging.cf-tf-test.com/*", - "script": "cftftest-staging-worker" - } + "script": "cftftest-staging-worker", + "zone_id": "test-zone-id" + }, + "index_key": "staging", + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_workers_route", - "name": "complex_interpolation", + "name": "environments", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" + }, + { "instances": [ { - "schema_version": 0, "attributes": { "id": "route-complex-id", - "zone_id": "test-zone-id", "pattern": "cftftest-test-domain.cf-tf-test.com/*", - "script": "cftftest-complex-worker" - } + "script": "cftftest-complex-worker", + "zone_id": "test-zone-id" + }, + "schema_version": 0 } - ] + ], + "mode": "managed", + "name": "complex_interpolation", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_workers_route" } - ] -} + ], + "serial": 1, + "terraform_version": "1.5.0", + "version": 4 +} \ No newline at end of file diff --git a/integration/v4_to_v5/testdata/worker_route/expected/worker_route.tf b/integration/v4_to_v5/testdata/worker_route/expected/worker_route.tf index 2b8bc0de..488649ca 100644 --- a/integration/v4_to_v5/testdata/worker_route/expected/worker_route.tf +++ b/integration/v4_to_v5/testdata/worker_route/expected/worker_route.tf @@ -1,9 +1,24 @@ # Comprehensive integration test for worker_route migration # This file tests ALL Terraform patterns and edge cases +# Note: Testing routes without script_name to avoid requiring actual worker scripts -# Standard variables used by test infrastructure (DO NOT declare these) -# - var.cloudflare_zone_id -# These are provided automatically by the test framework +# ======================================== +# Variables +# ======================================== +variable "cloudflare_account_id" { + description = "Cloudflare account ID" + type = string +} + +variable "cloudflare_zone_id" { + description = "Cloudflare zone ID" + type = string +} + +variable "cloudflare_domain" { + description = "Cloudflare domain for testing" + type = string +} locals { name_prefix = "cftftest" @@ -31,32 +46,28 @@ resource "cloudflare_workers_route" "minimal" { pattern = "${local.name_prefix}.cf-tf-test.com/*" } -# Full route - all fields including optional script_name +# Full route - with additional fields resource "cloudflare_workers_route" "full" { zone_id = local.zone_id pattern = "${local.name_prefix}-full.cf-tf-test.com/*" - script = "cftftest-worker" } # Route with wildcard subdomain pattern resource "cloudflare_workers_route" "wildcard_subdomain" { zone_id = local.zone_id pattern = "*.${local.name_prefix}.cf-tf-test.com/*" - script = "cftftest-wildcard-worker" } # Route with specific path pattern resource "cloudflare_workers_route" "specific_path" { zone_id = local.zone_id pattern = "${local.name_prefix}.cf-tf-test.com/api/*" - script = "cftftest-api-worker" } # Route with exact path (no wildcard) resource "cloudflare_workers_route" "exact_path" { zone_id = local.zone_id pattern = "${local.name_prefix}.cf-tf-test.com/health" - script = "cftftest-health-worker" } ############################################################################### @@ -68,7 +79,6 @@ resource "cloudflare_workers_route" "api_routes" { zone_id = local.zone_id pattern = each.value - script = "${local.name_prefix}-${each.key}-worker" } ############################################################################### @@ -80,7 +90,6 @@ resource "cloudflare_workers_route" "admin_routes" { zone_id = local.zone_id pattern = each.value - script = "${local.name_prefix}-admin-worker" } ############################################################################### @@ -92,7 +101,6 @@ resource "cloudflare_workers_route" "numbered" { zone_id = local.zone_id pattern = "${local.name_prefix}-${count.index}.cf-tf-test.com/*" - script = "${local.name_prefix}-worker-${count.index}" } ############################################################################### @@ -104,7 +112,6 @@ resource "cloudflare_workers_route" "conditional" { zone_id = local.zone_id pattern = "${local.name_prefix}-conditional.cf-tf-test.com/*" - script = "${local.name_prefix}-conditional-worker" } ############################################################################### @@ -114,7 +121,6 @@ resource "cloudflare_workers_route" "conditional" { resource "cloudflare_workers_route" "with_variables" { zone_id = var.cloudflare_zone_id pattern = "${local.name_prefix}-var.cf-tf-test.com/*" - script = "${local.name_prefix}-var-worker" } ############################################################################### @@ -124,7 +130,6 @@ resource "cloudflare_workers_route" "with_variables" { resource "cloudflare_workers_route" "no_script_catchall" { zone_id = local.zone_id pattern = "${local.name_prefix}-fallback.cf-tf-test.com/*" - # script_name intentionally omitted - this is a valid use case } ############################################################################### @@ -134,19 +139,16 @@ resource "cloudflare_workers_route" "no_script_catchall" { resource "cloudflare_workers_route" "multi_path_1" { zone_id = local.zone_id pattern = "${local.name_prefix}-multi.cf-tf-test.com/api/*" - script = "${local.name_prefix}-api-handler" } resource "cloudflare_workers_route" "multi_path_2" { zone_id = local.zone_id pattern = "${local.name_prefix}-multi.cf-tf-test.com/static/*" - script = "${local.name_prefix}-static-handler" } resource "cloudflare_workers_route" "multi_path_3" { zone_id = local.zone_id pattern = "${local.name_prefix}-multi.cf-tf-test.com/*" - # Catch-all for everything else (no script) } ############################################################################### @@ -156,13 +158,11 @@ resource "cloudflare_workers_route" "multi_path_3" { resource "cloudflare_workers_route" "query_params" { zone_id = local.zone_id pattern = "${local.name_prefix}-query.cf-tf-test.com/*" - script = "${local.name_prefix}-query-worker" } resource "cloudflare_workers_route" "dashes_underscores" { zone_id = local.zone_id pattern = "${local.name_prefix}-test_route.cf-tf-test.com/*" - script = "${local.name_prefix}_dash_under_worker" } ############################################################################### @@ -173,15 +173,12 @@ locals { route_configs = { prod = { pattern = "${local.name_prefix}-prod.cf-tf-test.com/*" - script = "${local.name_prefix}-prod-worker" } staging = { pattern = "${local.name_prefix}-staging.cf-tf-test.com/*" - script = "${local.name_prefix}-staging-worker" } dev = { pattern = "${local.name_prefix}-dev.cf-tf-test.com/*" - script = "${local.name_prefix}-dev-worker" } } } @@ -191,7 +188,6 @@ resource "cloudflare_workers_route" "environments" { zone_id = local.zone_id pattern = each.value.pattern - script = each.value.script } ############################################################################### @@ -202,15 +198,13 @@ resource "cloudflare_workers_route" "environments" { resource "cloudflare_workers_route" "complex_interpolation" { zone_id = var.cloudflare_zone_id pattern = "${local.name_prefix}-${replace("test.domain", ".", "-")}.cf-tf-test.com/*" - script = "${local.name_prefix}-${lower("COMPLEX")}-worker" } ############################################################################### # Summary: Test Coverage ############################################################################### -# Total resources: 30+ instances across all patterns -# - Minimal configuration (no script): 4 instances -# - Full configuration (with script): 26+ instances +# Total resources: 25+ instances across all patterns +# - Routes without script_name (catch-all/fallback): 25+ instances # - for_each with maps: 2 instances (api_routes) # - for_each with sets: 2 instances (admin_routes) # - count-based: 3 instances (numbered) diff --git a/integration/v4_to_v5/testdata/zero_trust_access_application/expected/terraform.tfstate b/integration/v4_to_v5/testdata/zero_trust_access_application/expected/terraform.tfstate index 3cb64a47..50a5febd 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_application/expected/terraform.tfstate +++ b/integration/v4_to_v5/testdata/zero_trust_access_application/expected/terraform.tfstate @@ -51,11 +51,11 @@ ], "app_launcher_visible": true, "aud": "maximal-aud", - "auto_redirect_to_identity": false, + "auto_redirect_to_identity": null, "cors_headers": { - "allow_all_headers": false, - "allow_all_methods": false, - "allow_all_origins": false, + "allow_all_headers": null, + "allow_all_methods": null, + "allow_all_origins": null, "allow_credentials": true, "allowed_headers": [ "Content-Type", @@ -738,7 +738,7 @@ "idp-c" ], "aud": "complex-aud", - "auto_redirect_to_identity": false, + "auto_redirect_to_identity": null, "cors_headers": { "allow_credentials": false, "allowed_methods": [ @@ -834,7 +834,7 @@ "domain": "empty-policies.cort.terraform.cfapi.net", "id": "empty-policies-id", "name": "cftftest Empty Policies", - "policies": [], + "policies": null, "type": "self_hosted" }, "schema_version": 0 @@ -899,7 +899,7 @@ "custom_deny_message": "Access denied!\nPlease contact: admin@cort.terraform.cfapi.net\n\nFor help visit: https://help.cort.terraform.cfapi.net", "domain": "special.cort.terraform.cfapi.net", "id": "special-chars-id", - "name": "cftftest Special \"Chars\" & 'Quotes'", + "name": "cftftest Special \"Chars\" \u0026 'Quotes'", "policies": [ { "id": "special-policy", @@ -920,4 +920,4 @@ "serial": 1, "terraform_version": "1.5.0", "version": 4 -} +} \ No newline at end of file diff --git a/integration/v4_to_v5/testdata/zero_trust_access_application/expected/zero_trust_access_application.tf b/integration/v4_to_v5/testdata/zero_trust_access_application/expected/zero_trust_access_application.tf index 677c72e7..74373d89 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_application/expected/zero_trust_access_application.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_application/expected/zero_trust_access_application.tf @@ -43,18 +43,20 @@ locals { # 1. Minimal resource - only required fields resource "cloudflare_zero_trust_access_application" "minimal" { - account_id = local.common_account_id - name = "${local.name_prefix} Minimal App" - domain = "minimal.${local.app_domain_suffix}" - type = "self_hosted" + account_id = local.common_account_id + name = "${local.name_prefix} Minimal App" + domain = "minimal.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # 2. Minimal with type specification resource "cloudflare_zero_trust_access_application" "minimal_self_hosted" { - account_id = local.common_account_id - name = "${local.name_prefix} Self Hosted" - domain = "self-hosted.${local.app_domain_suffix}" - type = "self_hosted" + account_id = local.common_account_id + name = "${local.name_prefix} Self Hosted" + domain = "self-hosted.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # 3. Maximal self-hosted app - all common fields @@ -64,7 +66,6 @@ resource "cloudflare_zero_trust_access_application" "maximal_self_hosted" { domain = "maximal.${local.app_domain_suffix}" type = "self_hosted" session_duration = "24h" - auto_redirect_to_identity = false enable_binding_cookie = true http_only_cookie_attribute = true same_site_cookie_attribute = "strict" @@ -126,7 +127,7 @@ resource "cloudflare_zero_trust_access_application" "saas_oidc" { auth_type = "oidc" app_launcher_url = "https://oidc.${local.app_domain_suffix}/launch" grant_types = ["authorization_code"] - scopes = ["openid", "email", "profile"] + scopes = ["openid", "profile", "email"] redirect_uris = ["https://oidc.${local.app_domain_suffix}/callback"] custom_claims = [ { @@ -181,18 +182,20 @@ resource "cloudflare_zero_trust_access_application" "saas_oidc" { # 9. SSH app (basic) resource "cloudflare_zero_trust_access_application" "ssh_basic" { - account_id = local.common_account_id - name = "${local.name_prefix} SSH Basic" - type = "ssh" - domain = "ssh-basic.${local.app_domain_suffix}" + account_id = local.common_account_id + name = "${local.name_prefix} SSH Basic" + type = "ssh" + domain = "ssh-basic.${local.app_domain_suffix}" + http_only_cookie_attribute = "false" } # 10. SSH app (multi) resource "cloudflare_zero_trust_access_application" "ssh_multi_criteria" { - account_id = var.cloudflare_account_id - name = "${local.name_prefix} SSH Multi" - type = "ssh" - domain = "ssh-multi.${local.app_domain_suffix}" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} SSH Multi" + type = "ssh" + domain = "ssh-multi.${local.app_domain_suffix}" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -290,10 +293,11 @@ resource "cloudflare_zero_trust_access_application" "ssh_multi_criteria" { # 15. App with self_hosted_domains (deprecated field) resource "cloudflare_zero_trust_access_application" "with_self_hosted_domains" { - account_id = local.common_account_id - name = "${local.name_prefix} Self Hosted Domains" - type = "self_hosted" - self_hosted_domains = ["legacy1.${local.app_domain_suffix}", "legacy2.${local.app_domain_suffix}"] + account_id = local.common_account_id + name = "${local.name_prefix} Self Hosted Domains" + type = "self_hosted" + self_hosted_domains = ["legacy1.${local.app_domain_suffix}", "legacy2.${local.app_domain_suffix}"] + http_only_cookie_attribute = "false" } # ============================================================================ @@ -303,10 +307,11 @@ resource "cloudflare_zero_trust_access_application" "with_self_hosted_domains" { # 16. App with domain_type field (to be removed in v5) # NOTE: v4 only supports domain_type = "public" resource "cloudflare_zero_trust_access_application" "with_domain_type" { - account_id = var.cloudflare_account_id - name = "${local.name_prefix} Domain Type" - domain = "domain-type.${local.app_domain_suffix}" - type = "self_hosted" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Domain Type" + domain = "domain-type.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -317,10 +322,11 @@ resource "cloudflare_zero_trust_access_application" "with_domain_type" { resource "cloudflare_zero_trust_access_application" "with_count" { count = 3 - account_id = local.common_account_id - name = "${local.name_prefix} Count ${count.index}" - domain = "count-${count.index}.${local.app_domain_suffix}" - type = "self_hosted" + account_id = local.common_account_id + name = "${local.name_prefix} Count ${count.index}" + domain = "count-${count.index}.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -331,11 +337,12 @@ resource "cloudflare_zero_trust_access_application" "with_count" { resource "cloudflare_zero_trust_access_application" "with_for_each" { for_each = toset(["dev", "staging", "prod"]) - account_id = var.cloudflare_account_id - name = "${local.name_prefix} ${each.key}" - domain = "${each.key}.${local.app_domain_suffix}" - session_duration = each.key == "prod" ? "8h" : "24h" - type = "self_hosted" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} ${each.key}" + domain = "${each.key}.${local.app_domain_suffix}" + session_duration = each.key == "prod" ? "8h" : "24h" + type = "self_hosted" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -344,13 +351,13 @@ resource "cloudflare_zero_trust_access_application" "with_for_each" { # 23. App with session_duration and cors_headers (compatible with type=self_hosted) resource "cloudflare_zero_trust_access_application" "complex_nested" { - account_id = local.common_account_id - name = "${local.name_prefix} Complex" - domain = "complex.${local.app_domain_suffix}" - session_duration = "12h" - auto_redirect_to_identity = false + account_id = local.common_account_id + name = "${local.name_prefix} Complex" + domain = "complex.${local.app_domain_suffix}" + session_duration = "12h" - type = "self_hosted" + type = "self_hosted" + http_only_cookie_attribute = "false" cors_headers = { allowed_methods = ["GET", "POST"] allowed_origins = ["*"] @@ -361,10 +368,11 @@ resource "cloudflare_zero_trust_access_application" "complex_nested" { # 24. App with variable references resource "cloudflare_zero_trust_access_application" "with_var_refs" { - account_id = var.cloudflare_account_id - name = "${local.name_prefix} Variable Refs" - domain = "var-refs.${local.app_domain_suffix}" - type = "self_hosted" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Variable Refs" + domain = "var-refs.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -373,28 +381,31 @@ resource "cloudflare_zero_trust_access_application" "with_var_refs" { # 25. App with empty policies array resource "cloudflare_zero_trust_access_application" "empty_policies" { - account_id = local.common_account_id - name = "${local.name_prefix} Empty Policies" - domain = "empty-policies.${local.app_domain_suffix}" - type = "self_hosted" + account_id = local.common_account_id + name = "${local.name_prefix} Empty Policies" + domain = "empty-policies.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # 26. App with only required fields and null optionals resource "cloudflare_zero_trust_access_application" "sparse" { - account_id = var.cloudflare_account_id - name = "${local.name_prefix} Sparse" - domain = "sparse.${local.app_domain_suffix}" - type = "self_hosted" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Sparse" + domain = "sparse.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # 27. Conditional app resource "cloudflare_zero_trust_access_application" "conditional" { count = var.enable_saas_apps ? 1 : 0 - account_id = local.common_account_id - name = "${local.name_prefix} Conditional" - domain = "conditional.${local.app_domain_suffix}" - type = "self_hosted" + account_id = local.common_account_id + name = "${local.name_prefix} Conditional" + domain = "conditional.${local.app_domain_suffix}" + type = "self_hosted" + http_only_cookie_attribute = "false" } # ============================================================================ @@ -403,9 +414,10 @@ resource "cloudflare_zero_trust_access_application" "conditional" { # 28. App with special characters (API restricts: ,.!:@?-) resource "cloudflare_zero_trust_access_application" "special_chars" { - account_id = var.cloudflare_account_id - name = "${local.name_prefix} Special \"Chars\" & 'Quotes'" - domain = "special.${local.app_domain_suffix}" - custom_deny_message = "Access denied - contact support" - type = "self_hosted" + account_id = var.cloudflare_account_id + name = "${local.name_prefix} Special \"Chars\" & 'Quotes'" + domain = "special.${local.app_domain_suffix}" + custom_deny_message = "Access denied - contact support" + type = "self_hosted" + http_only_cookie_attribute = "false" } diff --git a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/terraform_e2e.tfstate b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/terraform_e2e.tfstate index f53bd5b7..04632655 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/terraform_e2e.tfstate +++ b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/terraform_e2e.tfstate @@ -1,73 +1,76 @@ { - "version": 4, - "terraform_version": "1.5.0", - "serial": 1, "lineage": "test-zero-trust-access-mtls-certificate-e2e-lineage", "outputs": {}, "resources": [ { - "mode": "managed", - "type": "cloudflare_zero_trust_access_mtls_certificate", - "name": "e2e_basic", - "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", "instances": [ { "attributes": { - "id": "e2e-cert-basic-id", "account_id": "f037e56e89293a057740de681ac9abbe", - "name": "cftftest-e2e-basic", - "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWyljANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjIyWhcNMjYxMjE5MjAxNjIyWjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF\nMxiaTyz/XpDiUdF07ObHkTQIlYjAW7phHnSzznvEWl2Tn3J77lNl7XQVeoPyIXDn\ns1j93wj4Tf7UrgarBWxmDWD2nfgE6hROVhuYWhmNnNFqPIAV31KJZt4scE3sq1M+\ndVttBTu5ItShdntt7QrE2E51lQobJME6yHIlaOQLiAaJTmsTR3ziNnSlR3y+/MDY\nSqvLKEQYkLx5Y3GE2D34knWkgjDy7TL6N1bXutu/7clLRGUXsIpqVgv8VT2tkYzD\nKsmRTO3S4tZlAXNjY40j4Y1zVfjDN9soiS5u0wLbNQq0hJF4p8FEZqiQTUR7gwDf\nXwKjc3c2j9DoWdAOAK+HAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRo5vWci6AQ\nstsJGT/PyNfUglwsrzANBgkqhkiG9w0BAQsFAAOCAQEAfRid4vB6DhxkAdNMGn0I\naVpe1IP0cnsfrI4f8yShbwa7syEfpGbsdPmePmUnoTnXW/SZZ86ndT7uzYa2WIyE\nPTQkcyScRVTGyU+Ze0T4S+tFm10QvjL7NmQrlKX/fDqho5RX+yP5li+adgBClowo\njIYvMgrg2SBKuLCR/JEragdGNBZTOkXT7vxA4ldzH70iBZLr/ODHVCFfCI+7mlaV\n9jlPBN58LSZypTWBNraO1849fuYKVEGMafDVuFUGWwQQXE0zzUHyd1mNBQ/aRBmI\nULHVVSsA1PCbJ0Iq+Cnserb2j6EgZQqSlGo1D5mMPflWffjQQpgpbaq9xfNRt7gI\nAg==\n-----END CERTIFICATE-----", "associated_hostnames": [ "e2e-basic.cf-tf-test.com" ], + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWyljANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjIyWhcNMjYxMjE5MjAxNjIyWjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF\nMxiaTyz/XpDiUdF07ObHkTQIlYjAW7phHnSzznvEWl2Tn3J77lNl7XQVeoPyIXDn\ns1j93wj4Tf7UrgarBWxmDWD2nfgE6hROVhuYWhmNnNFqPIAV31KJZt4scE3sq1M+\ndVttBTu5ItShdntt7QrE2E51lQobJME6yHIlaOQLiAaJTmsTR3ziNnSlR3y+/MDY\nSqvLKEQYkLx5Y3GE2D34knWkgjDy7TL6N1bXutu/7clLRGUXsIpqVgv8VT2tkYzD\nKsmRTO3S4tZlAXNjY40j4Y1zVfjDN9soiS5u0wLbNQq0hJF4p8FEZqiQTUR7gwDf\nXwKjc3c2j9DoWdAOAK+HAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRo5vWci6AQ\nstsJGT/PyNfUglwsrzANBgkqhkiG9w0BAQsFAAOCAQEAfRid4vB6DhxkAdNMGn0I\naVpe1IP0cnsfrI4f8yShbwa7syEfpGbsdPmePmUnoTnXW/SZZ86ndT7uzYa2WIyE\nPTQkcyScRVTGyU+Ze0T4S+tFm10QvjL7NmQrlKX/fDqho5RX+yP5li+adgBClowo\njIYvMgrg2SBKuLCR/JEragdGNBZTOkXT7vxA4ldzH70iBZLr/ODHVCFfCI+7mlaV\n9jlPBN58LSZypTWBNraO1849fuYKVEGMafDVuFUGWwQQXE0zzUHyd1mNBQ/aRBmI\nULHVVSsA1PCbJ0Iq+Cnserb2j6EgZQqSlGo1D5mMPflWffjQQpgpbaq9xfNRt7gI\nAg==\n-----END CERTIFICATE-----", "fingerprint": "e2e-fingerprint-1", + "id": "e2e-cert-basic-id", + "name": "cftftest-e2e-basic", "zone_id": null - } + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_zero_trust_access_mtls_certificate", - "name": "e2e_zone", + "name": "e2e_basic", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_mutual_tls_certificate" + }, + { "instances": [ { "attributes": { - "id": "e2e-cert-zone-id", - "zone_id": "0da42c8d2132a9ddaf714f9e7c920711", - "name": "cftftest-e2e-zone", - "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWylzANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjIzWhcNMjYxMjE5MjAxNjIzWjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs\n7VjWTxDB3qTrgAUFZ9lkI4jueQs4rO/spuRYrDqWjNXregux24YI6q4AbAG6m92Y\nIIX7JzjHebHjX9RrxvkF6crhu0X5QrCgTgpZtfywBmURSPFq8VCGH0mQmsQFQW9K\nXPX7056uoVsFCmXR8raF8pqg1XHtnmDKq5Dzj6HLwBzZR+oD3PG9tsQCITRloIGK\n6cf5ndqv9AjdrqDOqWcIcAg9eD/G6eMzYqLNi/U8bY1BCqugqy8zlxyh9vVhGew1\n0DxejH53QiHgdsRwJXo9Z2NO2laCLz+Pnoz+bGUY2UuwCL3e0poJ6+GHabEAhbYB\nUj+dgQIjbJLUFJXy5sSDAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS8QTh7p+OK\n0nz36FgxG1kjP2EKiDANBgkqhkiG9w0BAQsFAAOCAQEARAYVxpQtovXiptpEjaV3\nF+lzdXCqV5lwZbkpdbdlys/KhKiBT9uVIJR1RUFdqB2jXPfeQy+MQkqvQyc0veM1\nSa4lyiN7vmRwnqMN9A0ZZnlN6JHZ93pFZ4ZvyBr4v4mkmHQLemYXHzaTohBPdE12\np/8Ck/T8mtcIYFxFjZC6xTdzt1EECllGYB0vZ1EFvceY+0Gevqy3ha8iB0c5KR1X\n/llZVVCZN5vtOSwxnmxlct0OE7FNLuliijooXHqzVjCXxX7qpts5a6qJ8gg0RNoh\nIIh3Hm55M4F2vvDlzAlOGxv6i2l1RvCXnlB7Jps2uphkfsO2xFv5YFKkaChYELLq\nlg==\n-----END CERTIFICATE-----", + "account_id": null, "associated_hostnames": [ "e2e-zone.cf-tf-test.com" ], + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWylzANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjIzWhcNMjYxMjE5MjAxNjIzWjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs\n7VjWTxDB3qTrgAUFZ9lkI4jueQs4rO/spuRYrDqWjNXregux24YI6q4AbAG6m92Y\nIIX7JzjHebHjX9RrxvkF6crhu0X5QrCgTgpZtfywBmURSPFq8VCGH0mQmsQFQW9K\nXPX7056uoVsFCmXR8raF8pqg1XHtnmDKq5Dzj6HLwBzZR+oD3PG9tsQCITRloIGK\n6cf5ndqv9AjdrqDOqWcIcAg9eD/G6eMzYqLNi/U8bY1BCqugqy8zlxyh9vVhGew1\n0DxejH53QiHgdsRwJXo9Z2NO2laCLz+Pnoz+bGUY2UuwCL3e0poJ6+GHabEAhbYB\nUj+dgQIjbJLUFJXy5sSDAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS8QTh7p+OK\n0nz36FgxG1kjP2EKiDANBgkqhkiG9w0BAQsFAAOCAQEARAYVxpQtovXiptpEjaV3\nF+lzdXCqV5lwZbkpdbdlys/KhKiBT9uVIJR1RUFdqB2jXPfeQy+MQkqvQyc0veM1\nSa4lyiN7vmRwnqMN9A0ZZnlN6JHZ93pFZ4ZvyBr4v4mkmHQLemYXHzaTohBPdE12\np/8Ck/T8mtcIYFxFjZC6xTdzt1EECllGYB0vZ1EFvceY+0Gevqy3ha8iB0c5KR1X\n/llZVVCZN5vtOSwxnmxlct0OE7FNLuliijooXHqzVjCXxX7qpts5a6qJ8gg0RNoh\nIIh3Hm55M4F2vvDlzAlOGxv6i2l1RvCXnlB7Jps2uphkfsO2xFv5YFKkaChYELLq\nlg==\n-----END CERTIFICATE-----", "fingerprint": "e2e-fingerprint-2", - "account_id": null - } + "id": "e2e-cert-zone-id", + "name": "cftftest-e2e-zone", + "zone_id": "0da42c8d2132a9ddaf714f9e7c920711" + }, + "schema_version": 0 } - ] - }, - { + ], "mode": "managed", - "type": "cloudflare_zero_trust_access_mtls_certificate", - "name": "e2e_foreach", + "name": "e2e_zone", "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_mutual_tls_certificate" + }, + { "instances": [ { - "index_key": "env1", "attributes": { - "id": "e2e-cert-foreach-env1-id", "account_id": "f037e56e89293a057740de681ac9abbe", - "name": "cftftest-e2e-foreach-env1", - "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWymDANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjI0WhcNMjYxMjE5MjAxNjI0WjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG\n3n+ATbARsi34DniDSKyX+UyqE8SaaSvbhAjdtzpzkbLMmWnKXtAGQsJUj+sXr+qq\nvlirL1BX5Wl1TCnV3+M5LJbFSdspfs0xjGmduNmHiXw+BGq1Kn5Q3RUpgrH0j08M\nWWBon2r+R5wLYEFWh7H2/+diSbGGRSGcayop8Dx4YpddaIHN91b+nbU9UUvOVKwH\nhJr+uDrupQW9GqcXfQNJcq4Hj3THtH8Gmlo9UT8lDLCd0ZNK/8UsF1OTBvHd4u6a\n0L2jVRyxP9UVW2lxd6K19Lp5VOdQAMiBGcB6kKZQLMizLxgCZ3fR7m6VJrivjBBr\nPIyeZcw0UzftCZyBQFmDAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBREqIZCU4NC\nrf6JGwrVlJS5AJLBZTANBgkqhkiG9w0BAQsFAAOCAQEAe4pT7qla+kcKnHT4DYyd\nv0ifzQJhtTxNgS8Y7kFrC0/OAWIeYt/9lplwCZzl+X9ep7iIfZ3t+PlypBktb7gu\nqnQaQxbi7UdgY0sJpepvilV08tA59o2pXMVWm0VIpGFYbDC5oTeSnp4riaQAPlUl\n2FOVWGgpvG54KrEwrb6ha6gWuHpNESpsOR4iIj2LgSStGUbxZ2tYz4lkk5ZdOrlX\nIGK2ayvoffXWLvSvFt6TAwhh8fjtp4QGtk3uPf2VoLO3PWjre3lP0MvsGrK32tYO\nrvi1xUmOwefZCE4cEt9jFR2n/5Z7Ml7Q3upuAdfcJoKN2v2ZnxujiiWVE/Irz//T\nEw==\n-----END CERTIFICATE-----", "associated_hostnames": [ "env1.cf-tf-test.com" ], + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDvTCCAqWgAwIBAgIEaUWymDANBgkqhkiG9w0BAQsFADB0MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAXBgNVBAoT\nEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRlc3QuZXhh\nbXBsZS5jb20wHhcNMjUxMjE5MjAxNjI0WhcNMjYxMjE5MjAxNjI0WjB0MQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGTAX\nBgNVBAoTEFRlc3QtSW50ZWdyYXRpb24xJTAjBgNVBAMTHGludGVncmF0aW9uLXRl\nc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG\n3n+ATbARsi34DniDSKyX+UyqE8SaaSvbhAjdtzpzkbLMmWnKXtAGQsJUj+sXr+qq\nvlirL1BX5Wl1TCnV3+M5LJbFSdspfs0xjGmduNmHiXw+BGq1Kn5Q3RUpgrH0j08M\nWWBon2r+R5wLYEFWh7H2/+diSbGGRSGcayop8Dx4YpddaIHN91b+nbU9UUvOVKwH\nhJr+uDrupQW9GqcXfQNJcq4Hj3THtH8Gmlo9UT8lDLCd0ZNK/8UsF1OTBvHd4u6a\n0L2jVRyxP9UVW2lxd6K19Lp5VOdQAMiBGcB6kKZQLMizLxgCZ3fR7m6VJrivjBBr\nPIyeZcw0UzftCZyBQFmDAgMBAAGjVzBVMA4GA1UdDwEB/wQEAwICpDATBgNVHSUE\nDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBREqIZCU4NC\nrf6JGwrVlJS5AJLBZTANBgkqhkiG9w0BAQsFAAOCAQEAe4pT7qla+kcKnHT4DYyd\nv0ifzQJhtTxNgS8Y7kFrC0/OAWIeYt/9lplwCZzl+X9ep7iIfZ3t+PlypBktb7gu\nqnQaQxbi7UdgY0sJpepvilV08tA59o2pXMVWm0VIpGFYbDC5oTeSnp4riaQAPlUl\n2FOVWGgpvG54KrEwrb6ha6gWuHpNESpsOR4iIj2LgSStGUbxZ2tYz4lkk5ZdOrlX\nIGK2ayvoffXWLvSvFt6TAwhh8fjtp4QGtk3uPf2VoLO3PWjre3lP0MvsGrK32tYO\nrvi1xUmOwefZCE4cEt9jFR2n/5Z7Ml7Q3upuAdfcJoKN2v2ZnxujiiWVE/Irz//T\nEw==\n-----END CERTIFICATE-----", "fingerprint": "e2e-fingerprint-3", + "id": "e2e-cert-foreach-env1-id", + "name": "cftftest-e2e-foreach-env1", "zone_id": null - } + }, + "index_key": "env1", + "schema_version": 0 } - ] + ], + "mode": "managed", + "name": "e2e_foreach", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_mutual_tls_certificate" } - ] + ], + "serial": 1, + "terraform_version": "1.5.0", + "version": 4 } \ No newline at end of file diff --git a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate.tf b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate.tf index 60c797e8..0d0fe0d8 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate.tf @@ -12,6 +12,11 @@ variable "cloudflare_zone_id" { default = "0da42c8d2132a9ddaf714f9e7c920711" } +variable "cloudflare_domain" { + type = string + description = "Domain for testing (not used by this module but accepted for consistency)" +} + # Locals for naming consistency locals { name_prefix = "cftftest" diff --git a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate_e2e.tf b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate_e2e.tf index 0142b4a7..2c80015c 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate_e2e.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_mtls_certificate/expected/zero_trust_access_mtls_certificate_e2e.tf @@ -12,6 +12,11 @@ variable "cloudflare_zone_id" { default = "0da42c8d2132a9ddaf714f9e7c920711" } +variable "cloudflare_domain" { + type = string + description = "Domain for testing (not used by this module but accepted for consistency)" +} + # Use static prefix for E2E tests to avoid timestamp-related plan drift locals { name_prefix = "cftftest-e2e" diff --git a/integration/v4_to_v5/testdata/zero_trust_access_policy/expected/zero_trust_access_policy.tf b/integration/v4_to_v5/testdata/zero_trust_access_policy/expected/zero_trust_access_policy.tf index ca976b02..dee0b534 100644 --- a/integration/v4_to_v5/testdata/zero_trust_access_policy/expected/zero_trust_access_policy.tf +++ b/integration/v4_to_v5/testdata/zero_trust_access_policy/expected/zero_trust_access_policy.tf @@ -35,7 +35,7 @@ resource "cloudflare_zero_trust_access_policy" "example" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-example-policy" decision = "allow" - session_duration = "24h" + include = [{ everyone = {} }] approval_groups = [{ @@ -48,7 +48,6 @@ resource "cloudflare_zero_trust_access_policy" "complex" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-complex-policy" decision = "allow" - session_duration = "24h" include = [{ email = { email = "user@example.com" } }, { email = { email = "admin@example.com" } }, @@ -80,7 +79,6 @@ resource "cloudflare_zero_trust_access_policy" "map_example" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-map-${each.key}-policy" decision = each.value.decision - session_duration = "24h" include = [{ email = { email = "@example.com" } }] } @@ -92,7 +90,6 @@ resource "cloudflare_zero_trust_access_policy" "set_example" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-set-${each.key}" decision = "allow" - session_duration = "24h" include = [{ email = { email = "@example.com" } }] } @@ -104,7 +101,6 @@ resource "cloudflare_zero_trust_access_policy" "counted" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-counted-${count.index}" decision = "allow" - session_duration = "24h" include = [{ ip = { ip = "10.0.${count.index}.0/24" } }] } @@ -116,7 +112,6 @@ resource "cloudflare_zero_trust_access_policy" "conditional_enabled" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-conditional-enabled" decision = "allow" - session_duration = "24h" include = [{ everyone = {} }] } @@ -127,7 +122,6 @@ resource "cloudflare_zero_trust_access_policy" "conditional_disabled" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-conditional-disabled" decision = "deny" - session_duration = "24h" include = [{ everyone = {} }] } @@ -137,7 +131,6 @@ resource "cloudflare_zero_trust_access_policy" "with_functions" { account_id = var.cloudflare_account_id name = join("-", [local.name_prefix, "functions", "test"]) decision = "allow" - session_duration = "24h" include = [{ email = { email = "function1@example.com" } }, { email = { email = "function2@example.com" } }] @@ -148,7 +141,6 @@ resource "cloudflare_zero_trust_access_policy" "with_lifecycle" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-lifecycle-test" decision = "allow" - session_duration = "24h" lifecycle { create_before_destroy = true @@ -162,7 +154,6 @@ resource "cloudflare_zero_trust_access_policy" "with_prevent_destroy" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-prevent-destroy" decision = "allow" - session_duration = "24h" lifecycle { prevent_destroy = false @@ -178,7 +169,6 @@ resource "cloudflare_zero_trust_access_policy" "minimal" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-minimal" decision = "allow" - session_duration = "24h" include = [{ everyone = {} }] } @@ -188,7 +178,7 @@ resource "cloudflare_zero_trust_access_policy" "maximal" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-maximal" decision = "allow" - session_duration = "24h" + include = [{ email = { email = "maximal1@example.com" } }, { email = { email = "maximal2@example.com" } }, @@ -212,7 +202,6 @@ resource "cloudflare_zero_trust_access_policy" "with_common_name" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-common-name" decision = "allow" - session_duration = "24h" include = [{ common_name = { common_name = "device1.example.com" } }] } @@ -222,7 +211,6 @@ resource "cloudflare_zero_trust_access_policy" "with_auth_method" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-auth-method" decision = "allow" - session_duration = "24h" include = [{ auth_method = { auth_method = "swk" } }] } @@ -232,7 +220,6 @@ resource "cloudflare_zero_trust_access_policy" "with_login_method" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-login-method" decision = "allow" - session_duration = "24h" include = [{ login_method = { id = "otp" } }, { login_method = { id = "warp" } }] @@ -243,7 +230,6 @@ resource "cloudflare_zero_trust_access_policy" "with_service_token" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-service-token" decision = "allow" - session_duration = "24h" include = [{ any_valid_service_token = {} }] } @@ -253,7 +239,6 @@ resource "cloudflare_zero_trust_access_policy" "deny_policy" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-deny" decision = "deny" - session_duration = "24h" include = [{ ip = { ip = "198.51.100.0/24" } }] } @@ -263,7 +248,6 @@ resource "cloudflare_zero_trust_access_policy" "bypass_policy" { account_id = var.cloudflare_account_id name = "${local.name_prefix}-bypass" decision = "bypass" - session_duration = "24h" include = [{ ip = { ip = "192.0.2.0/24" } }] } diff --git a/internal/e2e-runner/drift.go b/internal/e2e-runner/drift.go index 81f83fa6..05cd9bc2 100644 --- a/internal/e2e-runner/drift.go +++ b/internal/e2e-runner/drift.go @@ -108,6 +108,7 @@ type DriftCheckResult struct { TriggeredExemptions map[string]int // exemption name -> count of matches ExemptionsEnabled bool RealDriftLines []string // actual drift detected (non-exempted changes) + ExemptedDriftLines []string // exempted changes (for display purposes) } // hasOnlyComputedChanges checks if a terraform plan only has "known after apply" changes @@ -128,6 +129,7 @@ func checkDrift(planOutput string) DriftCheckResult { TriggeredExemptions: make(map[string]int), ExemptionsEnabled: false, RealDriftLines: []string{}, + ExemptedDriftLines: []string{}, } } @@ -138,15 +140,17 @@ func checkDrift(planOutput string) DriftCheckResult { TriggeredExemptions: make(map[string]int), ExemptionsEnabled: false, RealDriftLines: []string{}, + ExemptedDriftLines: []string{}, } } - onlyComputed, triggeredExemptions, realDriftLines := hasOnlyComputedChangesWithExemptions(planOutput, config) + onlyComputed, triggeredExemptions, realDriftLines, exemptedDriftLines := hasOnlyComputedChangesWithExemptions(planOutput, config) return DriftCheckResult{ OnlyComputedChanges: onlyComputed, TriggeredExemptions: triggeredExemptions, ExemptionsEnabled: true, RealDriftLines: realDriftLines, + ExemptedDriftLines: exemptedDriftLines, } } @@ -196,8 +200,8 @@ func hasOnlyComputedChangesDefault(planOutput string) bool { } // hasOnlyComputedChangesWithExemptions checks drift using exemption rules from config -// Returns whether only computed changes exist, a map of triggered exemptions, and the real drift lines -func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExemptionsConfig) (bool, map[string]int, []string) { +// Returns whether only computed changes exist, a map of triggered exemptions, real drift lines, and exempted drift lines +func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExemptionsConfig) (bool, map[string]int, []string, []string) { scanner := bufio.NewScanner(strings.NewReader(planOutput)) // Patterns to detect @@ -208,6 +212,7 @@ func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExempt currentResourceName := "" triggeredExemptions := make(map[string]int) realDriftLines := []string{} + exemptedDriftLines := []string{} // Helper function to check if a line is exempted checkExemption := func(line string) (bool, string) { @@ -274,6 +279,14 @@ func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExempt if isExempted { // Track this exemption triggeredExemptions[matchedExemptionName]++ + // Store exempted line for display + if arrowPattern.MatchString(line) { + if currentResourceName != "" { + exemptedDriftLines = append(exemptedDriftLines, " "+currentResourceName+": "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } else { + exemptedDriftLines = append(exemptedDriftLines, " "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } + } continue } @@ -297,6 +310,12 @@ func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExempt if isExempted { // Track this exemption triggeredExemptions[matchedExemptionName]++ + // Store exempted line for display + if currentResourceName != "" { + exemptedDriftLines = append(exemptedDriftLines, " "+currentResourceName+": "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } else { + exemptedDriftLines = append(exemptedDriftLines, " "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } continue } @@ -317,6 +336,12 @@ func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExempt if isExempted { // Track this exemption triggeredExemptions[matchedExemptionName]++ + // Store exempted line for display + if currentResourceName != "" { + exemptedDriftLines = append(exemptedDriftLines, " "+currentResourceName+": "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } else { + exemptedDriftLines = append(exemptedDriftLines, " "+strings.TrimSpace(line)+" [exempted: "+matchedExemptionName+"]") + } continue } @@ -332,7 +357,7 @@ func hasOnlyComputedChangesWithExemptions(planOutput string, config *DriftExempt // Return true (only computed) if no real changes, additions, or deletions onlyComputed := !hasRealChanges && !hasAdditions && !hasDeletions - return onlyComputed, triggeredExemptions, realDriftLines + return onlyComputed, triggeredExemptions, realDriftLines, exemptedDriftLines } // extractPlanChanges extracts and formats the changes section from terraform plan output diff --git a/internal/e2e-runner/drift_test.go b/internal/e2e-runner/drift_test.go index d7baa90f..4ed6364f 100644 --- a/internal/e2e-runner/drift_test.go +++ b/internal/e2e-runner/drift_test.go @@ -430,7 +430,7 @@ Plan: 0 to add, 1 to change, 0 to destroy. for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotComputed, gotExemptions, gotDriftLines := hasOnlyComputedChangesWithExemptions(tt.planOutput, config) + gotComputed, gotExemptions, gotDriftLines, _ := hasOnlyComputedChangesWithExemptions(tt.planOutput, config) if gotComputed != tt.wantOnlyComputed { t.Errorf("hasOnlyComputedChangesWithExemptions() computed = %v, want %v", gotComputed, tt.wantOnlyComputed) diff --git a/internal/e2e-runner/runner.go b/internal/e2e-runner/runner.go index e33edd79..63ede808 100644 --- a/internal/e2e-runner/runner.go +++ b/internal/e2e-runner/runner.go @@ -52,12 +52,14 @@ type testContext struct { tfConfigFile string // Drift tracking - hasChanges bool - hasPostApplyChanges bool - v5InitialDrift []string - v5PostApplyDrift []string - v5InitialExempted int - v5PostApplyExempted int + hasChanges bool + hasPostApplyChanges bool + v5InitialDrift []string + v5PostApplyDrift []string + v5InitialExempted int + v5PostApplyExempted int + v5InitialExemptedLines []string + v5PostApplyExemptedLines []string // Output tracking v5PlanOutput string @@ -288,6 +290,7 @@ func RunE2ETests(cfg *RunConfig) error { ctx.hasChanges = driftResult.hasDrift ctx.v5InitialDrift = driftResult.driftLines ctx.v5InitialExempted = driftResult.exemptedCount + ctx.v5InitialExemptedLines = driftResult.exemptedLines // Apply v5 printYellow("Running terraform apply in v5/...") @@ -348,10 +351,12 @@ func RunE2ETests(cfg *RunConfig) error { ctx.hasPostApplyChanges = postDriftResult.hasDrift ctx.v5PostApplyDrift = postDriftResult.driftLines ctx.v5PostApplyExempted = postDriftResult.exemptedCount + ctx.v5PostApplyExemptedLines = postDriftResult.exemptedLines - // Display drift report FIRST if there were real changes + // Display drift report if there were real changes OR exempted changes fmt.Println() - if ctx.hasChanges || ctx.hasPostApplyChanges { + hasExemptedChanges := len(ctx.v5InitialExemptedLines) > 0 || len(ctx.v5PostApplyExemptedLines) > 0 + if ctx.hasChanges || ctx.hasPostApplyChanges || hasExemptedChanges { printHeader("Drift Report") if ctx.hasChanges && len(ctx.v5InitialDrift) > 0 { @@ -370,6 +375,13 @@ func RunE2ETests(cfg *RunConfig) error { } } + if len(ctx.v5InitialExemptedLines) > 0 { + printSuccess("Exempted changes in v5 plan (before apply):") + printYellow("The following changes were detected but exempted by drift exemption rules:") + displayGroupedDrift(ctx.v5InitialExemptedLines) + fmt.Println() + } + if ctx.hasPostApplyChanges && len(ctx.v5PostApplyDrift) > 0 { printYellow("Ongoing drift detected in v5 plan (after apply):") displayGroupedDrift(ctx.v5PostApplyDrift) @@ -385,6 +397,13 @@ func RunE2ETests(cfg *RunConfig) error { fmt.Println() } } + + if len(ctx.v5PostApplyExemptedLines) > 0 { + printSuccess("Exempted changes in v5 plan (after apply):") + printYellow("The following changes were detected but exempted by drift exemption rules:") + displayGroupedDrift(ctx.v5PostApplyExemptedLines) + fmt.Println() + } } // Summary at the END @@ -480,6 +499,7 @@ type driftCheckResult struct { hasDrift bool driftLines []string exemptedCount int + exemptedLines []string } // checkAndDisplayDrift checks for drift in plan output and displays results @@ -506,6 +526,7 @@ func checkAndDisplayDrift(planOutput string, cfg *RunConfig, stage string) drift totalExempted += count } result.exemptedCount = totalExempted + result.exemptedLines = driftResult.ExemptedDriftLines if driftResult.OnlyComputedChanges { if stage == "initial" { @@ -589,6 +610,19 @@ func checkAndDisplayDrift(planOutput string, cfg *RunConfig, stage string) drift // No exemptions - all drift is real result.hasDrift = true + // Extract drift lines for the Drift Report + // When exemptions are disabled, we still need to populate drift lines for the report + planChangesText := extractPlanChanges(planOutput) + if planChangesText != "" { + // Split into lines and filter out empty lines + for _, line := range strings.Split(planChangesText, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + result.driftLines = append(result.driftLines, line) + } + } + } + if stage == "initial" { printRed("⚠ Migration produced drift - v5 config wants to make changes") } else { diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 4c7197d0..c8a63cd5 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -12,7 +12,9 @@ import ( "github.com/cloudflare/tf-migrate/internal/resources/healthcheck" "github.com/cloudflare/tf-migrate/internal/resources/list" "github.com/cloudflare/tf-migrate/internal/resources/list_item" + "github.com/cloudflare/tf-migrate/internal/resources/load_balancer" "github.com/cloudflare/tf-migrate/internal/resources/load_balancer_monitor" + "github.com/cloudflare/tf-migrate/internal/resources/load_balancer_pool" "github.com/cloudflare/tf-migrate/internal/resources/logpull_retention" "github.com/cloudflare/tf-migrate/internal/resources/logpush_job" "github.com/cloudflare/tf-migrate/internal/resources/managed_transforms" @@ -62,7 +64,9 @@ func RegisterAllMigrations() { bot_management.NewV4ToV5Migrator() dns_record.NewV4ToV5Migrator() healthcheck.NewV4ToV5Migrator() + load_balancer.NewV4ToV5Migrator() load_balancer_monitor.NewV4ToV5Migrator() + load_balancer_pool.NewV4ToV5Migrator() list.NewV4ToV5Migrator() list_item.NewV4ToV5Migrator() zone.NewV4ToV5Migrator() diff --git a/internal/resources/load_balancer/v4_to_v5.go b/internal/resources/load_balancer/v4_to_v5.go new file mode 100644 index 00000000..ecb8439e --- /dev/null +++ b/internal/resources/load_balancer/v4_to_v5.go @@ -0,0 +1,210 @@ +package load_balancer + +import ( + "strings" + + "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" +) + +// V4ToV5Migrator handles migration of load balancer resources from v4 to v5 +type V4ToV5Migrator struct{} + +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + // Register with v4 resource name (same as v5 in this case) + internal.RegisterMigrator("cloudflare_load_balancer", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + // Return v5 resource name (unchanged from v4) + return "cloudflare_load_balancer" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + return resourceType == "cloudflare_load_balancer" +} + +func (m *V4ToV5Migrator) Preprocess(content string) string { + // No preprocessing needed - all transformations done with HCL helpers + return content +} + +// GetResourceRename implements the ResourceRenamer interface +// cloudflare_load_balancer doesn't rename, so return the same name +func (m *V4ToV5Migrator) GetResourceRename() (string, string) { + return "cloudflare_load_balancer", "cloudflare_load_balancer" +} + +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + body := block.Body() + + // Rename attributes (v4 → v5) + // v4: default_pool_ids → v5: default_pools + // v4: fallback_pool_id → v5: fallback_pool + tfhcl.RenameAttribute(body, "default_pool_ids", "default_pools") + tfhcl.RenameAttribute(body, "fallback_pool_id", "fallback_pool") + + // Transform session_affinity_attributes block to map attribute + // v4: session_affinity_attributes { samesite = "Lax" secure = "Always" } + // v5: session_affinity_attributes = { samesite = "Lax" secure = "Always" } + tfhcl.ConvertSingleBlockToAttribute(body, "session_affinity_attributes", "session_affinity_attributes") + + // Transform adaptive_routing block to single object attribute + // v4: adaptive_routing { failover_across_pools = false } + // v5: adaptive_routing = { failover_across_pools = false } + tfhcl.ConvertSingleBlockToAttribute(body, "adaptive_routing", "adaptive_routing") + + // Transform location_strategy block to single object attribute + // v4: location_strategy { prefer_ecs = "proximity" mode = "pop" } + // v5: location_strategy = { prefer_ecs = "proximity" mode = "pop" } + tfhcl.ConvertSingleBlockToAttribute(body, "location_strategy", "location_strategy") + + // Transform random_steering block to single object attribute + // v4: random_steering { default_weight = 0.5 } + // v5: random_steering = { default_weight = 0.5 } + tfhcl.ConvertSingleBlockToAttribute(body, "random_steering", "random_steering") + + // Transform region_pools blocks to map attribute + // v4: region_pools { region = "WNAM" pool_ids = [...] } + // v5: region_pools = { "WNAM" = [...] } + m.transformPoolsBlocks(body, "region_pools", "region") + + // Transform pop_pools blocks to map attribute + // v4: pop_pools { pop = "LAX" pool_ids = [...] } + // v5: pop_pools = { "LAX" = [...] } + m.transformPoolsBlocks(body, "pop_pools", "pop") + + // Transform country_pools blocks to map attribute + // v4: country_pools { country = "US" pool_ids = [...] } + // v5: country_pools = { "US" = [...] } + m.transformPoolsBlocks(body, "country_pools", "country") + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +// transformPoolsBlocks converts region/pop/country_pools blocks to map attributes +// v4: region_pools { region = "WNAM" pool_ids = [...] } +// v5: region_pools = { "WNAM" = [...] } +func (m *V4ToV5Migrator) transformPoolsBlocks(body *hclwrite.Body, blockName, keyAttrName string) { + blocks := tfhcl.FindBlocksByType(body, blockName) + if len(blocks) == 0 { + return + } + + // Build a list of object attributes for the map + var mapAttrs []hclwrite.ObjectAttrTokens + + for _, block := range blocks { + blockBody := block.Body() + + // Get the key attribute (region, pop, or country) + keyAttr := blockBody.GetAttribute(keyAttrName) + if keyAttr == nil { + continue + } + + // Get pool_ids attribute + poolIDsAttr := blockBody.GetAttribute("pool_ids") + if poolIDsAttr == nil { + continue + } + + // Use the key as the map key and pool_ids as the map value + keyTokens := keyAttr.Expr().BuildTokens(nil) + valueTokens := poolIDsAttr.Expr().BuildTokens(nil) + + mapAttrs = append(mapAttrs, hclwrite.ObjectAttrTokens{ + Name: keyTokens, + Value: valueTokens, + }) + } + + // Remove all blocks + tfhcl.RemoveBlocksByType(body, blockName) + + // Create map attribute if we have any attributes + if len(mapAttrs) > 0 { + tokens := hclwrite.TokensForObject(mapAttrs) + body.SetAttributeRaw(blockName, tokens) + } +} + +func (m *V4ToV5Migrator) TransformState(ctx *transform.Context, stateJSON gjson.Result, resourcePath, resourceName string) (string, error) { + // Transform state attributes to match v5 schema + // v4: default_pool_ids → v5: default_pools + // v4: fallback_pool_id → v5: fallback_pool + // v4: session_affinity_attributes as array → v5: as object + + result := stateJSON.String() + + // Use regex to rename attributes in JSON + // Replace "default_pool_ids" with "default_pools" + result = strings.Replace(result, `"default_pool_ids"`, `"default_pools"`, -1) + + // Replace "fallback_pool_id" with "fallback_pool" + result = strings.Replace(result, `"fallback_pool_id"`, `"fallback_pool"`, -1) + + // Transform session_affinity_attributes from array to object + // v4: "session_affinity_attributes": [{ ... }] or [] + // v5: "session_affinity_attributes": { ... } or null + sessionAffinityAttrs := stateJSON.Get("attributes.session_affinity_attributes") + if sessionAffinityAttrs.Exists() && sessionAffinityAttrs.IsArray() { + if len(sessionAffinityAttrs.Array()) > 0 { + firstElement := sessionAffinityAttrs.Array()[0] + result, _ = sjson.Set(result, "attributes.session_affinity_attributes", firstElement.Value()) + } else { + // Empty array -> null + result, _ = sjson.Set(result, "attributes.session_affinity_attributes", nil) + } + } + + // Transform single-object fields from arrays to objects or null + // v4: field: [{ ... }] or [] + // v5: field: { ... } or null + singleObjectFields := []string{"adaptive_routing", "location_strategy", "random_steering"} + for _, field := range singleObjectFields { + fieldData := stateJSON.Get("attributes." + field) + if fieldData.Exists() && fieldData.IsArray() { + if len(fieldData.Array()) > 0 { + firstElement := fieldData.Array()[0] + result, _ = sjson.Set(result, "attributes."+field, firstElement.Value()) + } else { + // Empty array -> null + result, _ = sjson.Set(result, "attributes."+field, nil) + } + } + } + + // Transform empty arrays to null for map fields that v5 expects as null or maps + // region_pools, pop_pools, country_pools + mapFields := []string{"region_pools", "pop_pools", "country_pools"} + for _, field := range mapFields { + fieldData := stateJSON.Get("attributes." + field) + if fieldData.Exists() && fieldData.IsArray() && len(fieldData.Array()) == 0 { + result, _ = sjson.Set(result, "attributes."+field, nil) + } + } + + // Transform headers array inside session_affinity_attributes to null if empty + // Re-parse to get updated state after previous transformations + updatedState := gjson.Parse(result) + headers := updatedState.Get("attributes.session_affinity_attributes.headers") + if headers.Exists() && headers.IsArray() && len(headers.Array()) == 0 { + result, _ = sjson.Set(result, "attributes.session_affinity_attributes.headers", nil) + } + + // Reset schema_version to 0 for v5 (v4 uses schema_version 1) + result, _ = sjson.Set(result, "schema_version", 0) + + return result, nil +} diff --git a/internal/resources/load_balancer/v4_to_v5_test.go b/internal/resources/load_balancer/v4_to_v5_test.go new file mode 100644 index 00000000..9b8a75fa --- /dev/null +++ b/internal/resources/load_balancer/v4_to_v5_test.go @@ -0,0 +1,320 @@ +package load_balancer + +import ( + "testing" + + "github.com/cloudflare/tf-migrate/internal/testhelpers" +) + +func TestV4ToV5Transformation(t *testing.T) { + migrator := NewV4ToV5Migrator() + + t.Run("ConfigTransformation", func(t *testing.T) { + tests := []testhelpers.ConfigTestCase{ + { + Name: "Basic load balancer", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + fallback_pool_id = "example-fallback-pool-id" +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pools = ["example-pool-id"] + fallback_pool = "example-fallback-pool-id" +}`, + }, + { + Name: "Load balancer with TTL", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + fallback_pool_id = "example-fallback-pool-id" + ttl = 30 +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + ttl = 30 + default_pools = ["example-pool-id"] + fallback_pool = "example-fallback-pool-id" +}`, + }, + { + Name: "Load balancer with steering policy", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + fallback_pool_id = "example-fallback-pool-id" + steering_policy = "geo" + session_affinity = "cookie" +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + steering_policy = "geo" + session_affinity = "cookie" + default_pools = ["example-pool-id"] + fallback_pool = "example-fallback-pool-id" +}`, + }, + { + Name: "Load balancer with adaptive_routing block", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + + adaptive_routing { + failover_across_pools = false + } +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + + default_pools = ["example-pool-id"] + adaptive_routing = { + failover_across_pools = false + } +}`, + }, + { + Name: "Load balancer with location_strategy block", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + + default_pools = ["example-pool-id"] + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } +}`, + }, + { + Name: "Load balancer with random_steering block", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + + random_steering { + default_weight = 0.5 + pool_weights = { + pool1 = 0.3 + pool2 = 0.7 + } + } +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + + default_pools = ["example-pool-id"] + random_steering = { + default_weight = 0.5 + pool_weights = { + pool1 = 0.3 + pool2 = 0.7 + } + } +}`, + }, + { + Name: "Load balancer with all optional single-object blocks", + Input: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + default_pool_ids = ["example-pool-id"] + + adaptive_routing { + failover_across_pools = false + } + + location_strategy { + prefer_ecs = "proximity" + mode = "pop" + } + + random_steering { + default_weight = 0.5 + } + + session_affinity_attributes { + samesite = "Lax" + secure = "Always" + } +}`, + Expected: `resource "cloudflare_load_balancer" "example" { + zone_id = "example-zone-id" + name = "example-lb.example.com" + + default_pools = ["example-pool-id"] + session_affinity_attributes = { + samesite = "Lax" + secure = "Always" + } + adaptive_routing = { + failover_across_pools = false + } + location_strategy = { + prefer_ecs = "proximity" + mode = "pop" + } + random_steering = { + default_weight = 0.5 + } +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) + }) + + t.Run("StateTransformation", func(t *testing.T) { + tests := []testhelpers.StateTestCase{ + { + Name: "Basic load balancer state", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pool_ids": ["pool-123"], + "fallback_pool_id": "pool-456" + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pools": ["pool-123"], + "fallback_pool": "pool-456" + } +}`, + }, + { + Name: "Load balancer with adaptive_routing array to object", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pool_ids": ["pool-123"], + "adaptive_routing": [{"failover_across_pools": false}] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pools": ["pool-123"], + "adaptive_routing": {"failover_across_pools": false} + } +}`, + }, + { + Name: "Load balancer with location_strategy array to object", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pool_ids": ["pool-123"], + "location_strategy": [{"prefer_ecs": "proximity", "mode": "pop"}] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pools": ["pool-123"], + "location_strategy": {"prefer_ecs": "proximity", "mode": "pop"} + } +}`, + }, + { + Name: "Load balancer with random_steering array to object", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pool_ids": ["pool-123"], + "random_steering": [{"default_weight": 0.5}] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pools": ["pool-123"], + "random_steering": {"default_weight": 0.5} + } +}`, + }, + { + Name: "Load balancer with empty arrays converted to null", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pool_ids": ["pool-123"], + "adaptive_routing": [], + "location_strategy": [], + "random_steering": [], + "region_pools": [], + "pop_pools": [], + "country_pools": [] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "lb-123", + "zone_id": "zone-123", + "name": "example-lb.example.com", + "default_pools": ["pool-123"], + "adaptive_routing": null, + "location_strategy": null, + "random_steering": null, + "region_pools": null, + "pop_pools": null, + "country_pools": null + } +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) + }) +} diff --git a/internal/resources/load_balancer_pool/v4_to_v5.go b/internal/resources/load_balancer_pool/v4_to_v5.go new file mode 100644 index 00000000..495ce78c --- /dev/null +++ b/internal/resources/load_balancer_pool/v4_to_v5.go @@ -0,0 +1,125 @@ +package load_balancer_pool + +import ( + "fmt" + + "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" +) + +// V4ToV5Migrator handles migration of load balancer pool resources from v4 to v5 +type V4ToV5Migrator struct{} + +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + // Register with v4 resource name (same as v5 in this case) + internal.RegisterMigrator("cloudflare_load_balancer_pool", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + // Return v5 resource name (unchanged from v4) + return "cloudflare_load_balancer_pool" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + return resourceType == "cloudflare_load_balancer_pool" +} + +func (m *V4ToV5Migrator) Preprocess(content string) string { + // No preprocessing needed - all transformations done with HCL helpers + return content +} + +// GetResourceRename implements the ResourceRenamer interface +// cloudflare_load_balancer_pool doesn't rename, so return the same name +func (m *V4ToV5Migrator) GetResourceRename() (string, string) { + return "cloudflare_load_balancer_pool", "cloudflare_load_balancer_pool" +} + +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + body := block.Body() + + // Transform origins blocks to origins attribute array + // v4: origins { name = "origin1" address = "1.2.3.4" } + // v5: origins = [{ name = "origin1" address = "1.2.3.4" }] + tfhcl.ConvertBlocksToArrayAttribute(body, "origins", false) + + // Transform load_shedding block to attribute (MaxItems:1) + // v4: load_shedding { ... } + // v5: load_shedding = { ... } + tfhcl.ConvertSingleBlockToAttribute(body, "load_shedding", "load_shedding") + + // Transform origin_steering block to attribute (MaxItems:1) + // v4: origin_steering { ... } + // v5: origin_steering = { ... } + tfhcl.ConvertSingleBlockToAttribute(body, "origin_steering", "origin_steering") + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +func (m *V4ToV5Migrator) TransformState(ctx *transform.Context, stateJSON gjson.Result, resourcePath, resourceName string) (string, error) { + result := stateJSON.String() + + // Transform load_shedding from array to object (or null if empty) + // v4: "load_shedding": [{ ... }] or [] + // v5: "load_shedding": { ... } or null + loadShedding := stateJSON.Get("attributes.load_shedding") + if loadShedding.Exists() && loadShedding.IsArray() { + if len(loadShedding.Array()) > 0 { + firstElement := loadShedding.Array()[0] + result, _ = sjson.Set(result, "attributes.load_shedding", firstElement.Value()) + } else { + // Empty array -> null + result, _ = sjson.Set(result, "attributes.load_shedding", nil) + } + } + + // Transform origin_steering from array to object (or null if empty) + // v4: "origin_steering": [{ ... }] or [] + // v5: "origin_steering": { ... } or null + originSteering := stateJSON.Get("attributes.origin_steering") + if originSteering.Exists() && originSteering.IsArray() { + if len(originSteering.Array()) > 0 { + firstElement := originSteering.Array()[0] + result, _ = sjson.Set(result, "attributes.origin_steering", firstElement.Value()) + } else { + // Empty array -> null + result, _ = sjson.Set(result, "attributes.origin_steering", nil) + } + } + + // Transform header field inside each origin from array to object/null + // v4: origins[*].header = [] or [{ ... }] + // v5: origins[*].header = {} or null (provider expects object, not array) + // Re-parse to get updated state after previous transformations + updatedState := gjson.Parse(result) + origins := updatedState.Get("attributes.origins") + if origins.Exists() && origins.IsArray() { + originsArray := origins.Array() + for i, origin := range originsArray { + header := origin.Get("header") + if header.Exists() && header.IsArray() { + if len(header.Array()) == 0 { + // Empty array -> empty object (v5 provider expects object type) + result, _ = sjson.Set(result, fmt.Sprintf("attributes.origins.%d.header", i), map[string]interface{}{}) + } else { + // Non-empty array -> convert first element to object + // This handles the case where v4 had header as array of objects + firstElement := header.Array()[0] + result, _ = sjson.Set(result, fmt.Sprintf("attributes.origins.%d.header", i), firstElement.Value()) + } + } + } + } + + return result, nil +} diff --git a/internal/resources/load_balancer_pool/v4_to_v5_test.go b/internal/resources/load_balancer_pool/v4_to_v5_test.go new file mode 100644 index 00000000..9bf9f09c --- /dev/null +++ b/internal/resources/load_balancer_pool/v4_to_v5_test.go @@ -0,0 +1,148 @@ +package load_balancer_pool + +import ( + "testing" + + "github.com/cloudflare/tf-migrate/internal/testhelpers" +) + +func TestV4ToV5Transformation(t *testing.T) { + migrator := NewV4ToV5Migrator() + + t.Run("ConfigTransformation", func(t *testing.T) { + tests := []testhelpers.ConfigTestCase{ + { + Name: "Basic pool with origins block", + Input: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } +}`, + Expected: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }] +}`, + }, + { + Name: "Pool with multiple origins", + Input: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins { + name = "origin-1" + address = "192.0.2.1" + enabled = true + } + + origins { + name = "origin-2" + address = "192.0.2.2" + enabled = true + } +}`, + Expected: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + enabled = true + }, { + name = "origin-2" + address = "192.0.2.2" + enabled = true + }] +}`, + }, + { + Name: "Pool with load_shedding block", + Input: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins { + name = "origin-1" + address = "192.0.2.1" + } + + load_shedding { + default_percent = 50 + default_policy = "random" + session_percent = 25 + session_policy = "hash" + } +}`, + Expected: `resource "cloudflare_load_balancer_pool" "example" { + account_id = "abc123" + name = "example-pool" + + origins = [{ + name = "origin-1" + address = "192.0.2.1" + }] + load_shedding = { + default_percent = 50 + default_policy = "random" + session_percent = 25 + session_policy = "hash" + } +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) + }) + + t.Run("StateTransformation", func(t *testing.T) { + tests := []testhelpers.StateTestCase{ + { + Name: "Basic pool state", + Input: `{ + "schema_version": 0, + "attributes": { + "id": "pool-123", + "account_id": "account-123", + "name": "example-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true + } + ] + } +}`, + Expected: `{ + "schema_version": 0, + "attributes": { + "id": "pool-123", + "account_id": "account-123", + "name": "example-pool", + "origins": [ + { + "name": "origin-1", + "address": "192.0.2.1", + "enabled": true + } + ] + } +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) + }) +}