diff --git a/README.md b/README.md index 23c8539..d0e301e 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,45 @@ dns_record: Use `--apply-exemptions` to ignore these known drifts during testing. +**Import Annotations (Import-Only Resources):** + +Some Cloudflare resources cannot be created via Terraform and must be imported from existing infrastructure (e.g., `zero_trust_organization`). The E2E runner supports automatic imports via annotations in `_e2e.tf` files: + +```hcl +# tf-migrate:import-address=${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id + name = "Test Organization" + auth_domain = "test.cloudflareaccess.com" +} +``` + +**How It Works:** +1. E2E runner scans for `# tf-migrate:import-address=
` annotations +2. Substitutes variables: `${var.cloudflare_account_id}` → actual account ID (e.g., `abc123`) +3. Executes: `terraform import module.. abc123` +4. Continues with normal E2E workflow (apply, migrate, verify) + +**Supported Variables:** +- `${var.cloudflare_account_id}` - Account ID from environment +- `${var.cloudflare_zone_id}` - Zone ID from environment +- `${var.cloudflare_domain}` - Domain from environment + +**Multiple Imports:** +Multiple resources can be annotated in a single file - all will be imported automatically before v4 apply. + +**Example Import Addresses:** +```hcl +# Account-scoped resource (just the account ID) +# tf-migrate:import-address=${var.cloudflare_account_id} + +# Zone-scoped resource with path +# tf-migrate:import-address=zones/${var.cloudflare_zone_id}/settings/waf + +# Complex path (for resources that need multiple identifiers) +# tf-migrate:import-address=${var.cloudflare_account_id}/item/${var.item_id} +``` + **Project Structure:** ``` diff --git a/integration/v4_to_v5/testdata/zero_trust_organization/expected/terraform.tfstate b/integration/v4_to_v5/testdata/zero_trust_organization/expected/terraform.tfstate new file mode 100644 index 0000000..ac5255e --- /dev/null +++ b/integration/v4_to_v5/testdata/zero_trust_organization/expected/terraform.tfstate @@ -0,0 +1,792 @@ +{ + "lineage": "test-zero-trust-organization-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-minimal-account.cloudflareaccess.com", + "id": "test-account-123", + "name": "Minimal Account Organization", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "minimal_account", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "auth_domain": "cftftest-minimal-zone.cloudflareaccess.com", + "id": "test-zone-456", + "name": "Minimal Zone Organization", + "zone_id": "test-zone-456", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "minimal_zone", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": true, + "auth_domain": "cftftest-complete.cloudflareaccess.com", + "auto_redirect_to_identity": true, + "custom_pages": { + "forbidden": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "identity_denied": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + }, + "id": "test-account-123", + "is_ui_read_only": true, + "login_design": { + "background_color": "#000000", + "footer_text": "Powered by Cloudflare Access", + "header_text": "Welcome to Our Platform", + "logo_path": "https://assets.cf-tf-test.com/logo.png", + "text_color": "#FFFFFF" + }, + "name": "Complete Organization", + "session_duration": "24h", + "ui_read_only_toggle_reason": "Managed via Terraform", + "user_seat_expiration_inactive_time": "730h", + "warp_auth_session_duration": "12h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "complete", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-login-design.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "background_color": "#1a1a2e", + "footer_text": "© 2026 Enterprise Inc.", + "header_text": "Enterprise Portal", + "logo_path": "https://assets.cf-tf-test.com/custom-logo.png", + "text_color": "#eaeaea" + }, + "name": "Organization with Custom Login Design", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_login_design", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-custom-pages.cloudflareaccess.com", + "custom_pages": { + "forbidden": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "identity_denied": "dddddddd-dddd-dddd-dddd-dddddddddddd" + }, + "id": "test-account-123", + "name": "Organization with Custom Pages", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_custom_pages", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-both-nested.cloudflareaccess.com", + "custom_pages": { + "forbidden": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" + }, + "id": "test-account-123", + "login_design": { + "background_color": "#2c3e50", + "text_color": "#ecf0f1" + }, + "name": "Organization with Both Nested Blocks", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_both_nested", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": true, + "auth_domain": "cftftest-booleans-true.cloudflareaccess.com", + "auto_redirect_to_identity": true, + "id": "test-account-123", + "is_ui_read_only": true, + "name": "Organization with Booleans True" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_booleans_true", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-durations.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization with All Duration Fields", + "session_duration": "48h", + "user_seat_expiration_inactive_time": "1460h", + "warp_auth_session_duration": "2h30m", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_durations", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-deprecated.cloudflareaccess.com", + "id": "test-account-123", + "name": "Using Deprecated Resource Name", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "deprecated_name", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-current.cloudflareaccess.com", + "id": "test-account-123", + "name": "Using Current Resource Name", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "current_name", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-partial-login-1.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "background_color": "#16213e", + "text_color": "#f1f1f1" + }, + "name": "Partial Login Design - Colors Only", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_login_1", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-partial-login-2.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "logo_path": "https://assets.cf-tf-test.com/simple-logo.png" + }, + "name": "Partial Login Design - Logo Only", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_login_2", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-partial-custom-1.cloudflareaccess.com", + "custom_pages": { + "identity_denied": "11111111-1111-1111-1111-111111111111" + }, + "id": "test-account-123", + "name": "Partial Custom Pages - Identity Denied Only", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_custom_1", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-partial-custom-2.cloudflareaccess.com", + "custom_pages": { + "forbidden": "22222222-2222-2222-2222-222222222222" + }, + "id": "test-account-123", + "name": "Partial Custom Pages - Forbidden Only", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_custom_2", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-only-session.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization with Only Session Duration", + "session_duration": "6h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_session", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-only-seat.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization with Only Seat Expiration", + "user_seat_expiration_inactive_time": "730h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_seat_expiration", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-only-warp.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization with Only WARP Duration", + "warp_auth_session_duration": "1h30m", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_warp_duration", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-only-readonly.cloudflareaccess.com", + "id": "test-account-123", + "is_ui_read_only": true, + "name": "Organization with Only Read-Only Flag", + "ui_read_only_toggle_reason": "Locked down for compliance", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_readonly", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-special.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "footer_text": "Questions? Email: support@cf-tf-test.com", + "header_text": "Welcome! Let's get started..." + }, + "name": "Org with Special: @#$% & Chars!", + "ui_read_only_toggle_reason": "Locked: Deployment (2026-01-20 @ 14:00 UTC)", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "special_chars", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-long.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "footer_text": "This footer contains legal disclaimers and copyright notices", + "header_text": "This is a very long header text about the organization" + }, + "name": "Organization with Very Long Name That Contains Many Characters", + "ui_read_only_toggle_reason": "This organization is locked because of maintenance", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "long_strings", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-duration-formats.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization with Various Duration Formats", + "session_duration": "2h45m", + "user_seat_expiration_inactive_time": "730h", + "warp_auth_session_duration": "90m", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "duration_formats", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-colors.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "background_color": "#abc", + "text_color": "#FFFFFF" + }, + "name": "Organization with Various Color Formats", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "color_formats", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": false, + "auth_domain": "cftftest-booleans-false.cloudflareaccess.com", + "auto_redirect_to_identity": false, + "id": "test-account-123", + "is_ui_read_only": false, + "name": "Organization with Booleans False" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "booleans_false", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-org-alpha.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization org-alpha", + "session_duration": "24h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "org-alpha", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-org-beta.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization org-beta", + "session_duration": "24h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "org-beta", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-org-gamma.cloudflareaccess.com", + "id": "test-account-123", + "name": "Organization org-gamma", + "session_duration": "24h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "org-gamma", + "schema_version": 0 + } + ], + "mode": "managed", + "name": "foreach_set", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-dev.cloudflareaccess.com", + "id": "test-account-123", + "name": "Environment: dev", + "session_duration": "12h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "dev", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-staging.cloudflareaccess.com", + "id": "test-account-123", + "name": "Environment: staging", + "session_duration": "24h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "staging", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-prod.cloudflareaccess.com", + "id": "test-account-123", + "name": "Environment: prod", + "session_duration": "8h", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": "prod", + "schema_version": 0 + } + ], + "mode": "managed", + "name": "foreach_map", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": true, + "auth_domain": "cftftest-count-0.cloudflareaccess.com", + "id": "test-account-123", + "name": "Count-based Organization 0", + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": 0, + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": false, + "auth_domain": "cftftest-count-1.cloudflareaccess.com", + "id": "test-account-123", + "name": "Count-based Organization 1", + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "index_key": 1, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_count", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-locals.cloudflareaccess.com", + "id": "test-account-123", + "login_design": { + "header_text": "Welcome to cftftest" + }, + "name": "cftftest Organization with Locals", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_locals", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "auth_domain": "cftftest-conditional.cloudflareaccess.com", + "auto_redirect_to_identity": true, + "id": "test-account-123", + "is_ui_read_only": true, + "name": "Organization with Conditional", + "session_duration": "12h", + "allow_authenticate_via_warp": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "conditional", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": false, + "auth_domain": "cftftest-lifecycle.cloudflareaccess.com", + "auto_redirect_to_identity": false, + "id": "test-account-123", + "is_ui_read_only": false, + "name": "Organization with Lifecycle", + "session_duration": "24h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_lifecycle", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "allow_authenticate_via_warp": false, + "auth_domain": "cftftest-depends.cloudflareaccess.com", + "auto_redirect_to_identity": false, + "id": "test-account-123", + "is_ui_read_only": false, + "name": "Organization with depends_on" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_depends", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + }, + { + "instances": [ + { + "attributes": { + "allow_authenticate_via_warp": true, + "auth_domain": "cftftest-zone-comprehensive.cloudflareaccess.com", + "auto_redirect_to_identity": true, + "custom_pages": { + "forbidden": "33333333-3333-3333-3333-333333333333", + "identity_denied": "44444444-4444-4444-4444-444444444444" + }, + "id": "test-zone-456", + "is_ui_read_only": true, + "login_design": { + "background_color": "#1e1e1e", + "footer_text": "Zone-level Access", + "header_text": "Zone Portal", + "logo_path": "https://zone.cf-tf-test.com/logo.png", + "text_color": "#ffffff" + }, + "name": "Comprehensive Zone-Scoped Organization", + "session_duration": "12h", + "ui_read_only_toggle_reason": "Zone-level configuration", + "warp_auth_session_duration": "6h", + "zone_id": "test-zone-456" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "zone_comprehensive", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_organization" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/zero_trust_organization/expected/zero_trust_organization.tf b/integration/v4_to_v5/testdata/zero_trust_organization/expected/zero_trust_organization.tf new file mode 100644 index 0000000..b9645af --- /dev/null +++ b/integration/v4_to_v5/testdata/zero_trust_organization/expected/zero_trust_organization.tf @@ -0,0 +1,347 @@ +variable "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" + org_names = toset(["org-alpha", "org-beta", "org-gamma"]) + org_configs = { + dev = { auth_domain = "${local.name_prefix}-dev.cloudflareaccess.com", session = "12h" } + staging = { auth_domain = "${local.name_prefix}-staging.cloudflareaccess.com", session = "24h" } + prod = { auth_domain = "${local.name_prefix}-prod.cloudflareaccess.com", session = "8h" } + } +} + +# ===== Basic Scenarios (8) ===== +resource "cloudflare_zero_trust_organization" "minimal_account" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-minimal-account.cloudflareaccess.com" + name = "Minimal Account Organization" +} + +resource "cloudflare_zero_trust_organization" "minimal_zone" { + zone_id = var.cloudflare_zone_id + auth_domain = "${local.name_prefix}-minimal-zone.cloudflareaccess.com" + name = "Minimal Zone Organization" +} + +resource "cloudflare_zero_trust_organization" "complete" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-complete.cloudflareaccess.com" + name = "Complete Organization" + is_ui_read_only = true + ui_read_only_toggle_reason = "Managed via Terraform" + user_seat_expiration_inactive_time = "730h" + auto_redirect_to_identity = true + session_duration = "24h" + allow_authenticate_via_warp = true + warp_auth_session_duration = "12h" + + + login_design = { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://assets.cf-tf-test.com/logo.png" + header_text = "Welcome to Our Platform" + footer_text = "Powered by Cloudflare Access" + } + custom_pages = { + forbidden = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + identity_denied = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } +} + +resource "cloudflare_zero_trust_organization" "with_login_design" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-login-design.cloudflareaccess.com" + name = "Organization with Custom Login Design" + + login_design = { + background_color = "#1a1a2e" + text_color = "#eaeaea" + logo_path = "https://assets.cf-tf-test.com/custom-logo.png" + header_text = "Enterprise Portal" + footer_text = "© 2026 Enterprise Inc." + } +} + +resource "cloudflare_zero_trust_organization" "with_custom_pages" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-custom-pages.cloudflareaccess.com" + name = "Organization with Custom Pages" + + custom_pages = { + forbidden = "cccccccc-cccc-cccc-cccc-cccccccccccc" + identity_denied = "dddddddd-dddd-dddd-dddd-dddddddddddd" + } +} + +resource "cloudflare_zero_trust_organization" "with_both_nested" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-both-nested.cloudflareaccess.com" + name = "Organization with Both Nested Blocks" + + + login_design = { + background_color = "#2c3e50" + text_color = "#ecf0f1" + } + custom_pages = { + forbidden = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" + } +} + +resource "cloudflare_zero_trust_organization" "with_booleans_true" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-booleans-true.cloudflareaccess.com" + name = "Organization with Booleans True" + is_ui_read_only = true + auto_redirect_to_identity = true + allow_authenticate_via_warp = true +} + +resource "cloudflare_zero_trust_organization" "with_durations" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-durations.cloudflareaccess.com" + name = "Organization with All Duration Fields" + session_duration = "48h" + user_seat_expiration_inactive_time = "1460h" + warp_auth_session_duration = "2h30m" +} + +# ===== v4 Resource Name Variations (2) ===== +resource "cloudflare_zero_trust_organization" "deprecated_name" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-deprecated.cloudflareaccess.com" + name = "Using Deprecated Resource Name" +} + +resource "cloudflare_zero_trust_organization" "current_name" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-current.cloudflareaccess.com" + name = "Using Current Resource Name" +} + +# ===== Partial Field Combinations (8) ===== +resource "cloudflare_zero_trust_organization" "partial_login_1" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-login-1.cloudflareaccess.com" + name = "Partial Login Design - Colors Only" + + login_design = { + background_color = "#16213e" + text_color = "#f1f1f1" + } +} + +resource "cloudflare_zero_trust_organization" "partial_login_2" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-login-2.cloudflareaccess.com" + name = "Partial Login Design - Logo Only" + + login_design = { + logo_path = "https://assets.cf-tf-test.com/simple-logo.png" + } +} + +resource "cloudflare_zero_trust_organization" "partial_custom_1" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-custom-1.cloudflareaccess.com" + name = "Partial Custom Pages - Identity Denied Only" + + custom_pages = { + identity_denied = "11111111-1111-1111-1111-111111111111" + } +} + +resource "cloudflare_zero_trust_organization" "partial_custom_2" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-custom-2.cloudflareaccess.com" + name = "Partial Custom Pages - Forbidden Only" + + custom_pages = { + forbidden = "22222222-2222-2222-2222-222222222222" + } +} + +resource "cloudflare_zero_trust_organization" "only_session" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-session.cloudflareaccess.com" + name = "Organization with Only Session Duration" + session_duration = "6h" +} + +resource "cloudflare_zero_trust_organization" "only_seat_expiration" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-seat.cloudflareaccess.com" + name = "Organization with Only Seat Expiration" + user_seat_expiration_inactive_time = "730h" +} + +resource "cloudflare_zero_trust_organization" "only_warp_duration" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-warp.cloudflareaccess.com" + name = "Organization with Only WARP Duration" + warp_auth_session_duration = "1h30m" +} + +resource "cloudflare_zero_trust_organization" "only_readonly" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-readonly.cloudflareaccess.com" + name = "Organization with Only Read-Only Flag" + is_ui_read_only = true + ui_read_only_toggle_reason = "Locked down for compliance" +} + +# ===== Edge Cases (5) ===== +resource "cloudflare_zero_trust_organization" "special_chars" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-special.cloudflareaccess.com" + name = "Org with Special: @#$% & Chars!" + ui_read_only_toggle_reason = "Locked: Deployment (2026-01-20 @ 14:00 UTC)" + + login_design = { + header_text = "Welcome! Let's get started..." + footer_text = "Questions? Email: support@cf-tf-test.com" + } +} + +resource "cloudflare_zero_trust_organization" "long_strings" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-long.cloudflareaccess.com" + name = "Organization with Very Long Name That Contains Many Characters" + ui_read_only_toggle_reason = "This organization is locked because of maintenance" + + login_design = { + header_text = "This is a very long header text about the organization" + footer_text = "This footer contains legal disclaimers and copyright notices" + } +} + +resource "cloudflare_zero_trust_organization" "duration_formats" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-duration-formats.cloudflareaccess.com" + name = "Organization with Various Duration Formats" + session_duration = "2h45m" + user_seat_expiration_inactive_time = "730h" + warp_auth_session_duration = "90m" +} + +resource "cloudflare_zero_trust_organization" "color_formats" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-colors.cloudflareaccess.com" + name = "Organization with Various Color Formats" + + login_design = { + background_color = "#abc" + text_color = "#FFFFFF" + } +} + +resource "cloudflare_zero_trust_organization" "booleans_false" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-booleans-false.cloudflareaccess.com" + name = "Organization with Booleans False" + is_ui_read_only = false + auto_redirect_to_identity = false + allow_authenticate_via_warp = false +} + +# ===== Terraform Patterns (8) ===== +resource "cloudflare_zero_trust_organization" "foreach_set" { + for_each = local.org_names + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-${each.value}.cloudflareaccess.com" + name = "Organization ${each.value}" + session_duration = "24h" +} + +resource "cloudflare_zero_trust_organization" "foreach_map" { + for_each = local.org_configs + account_id = var.cloudflare_account_id + auth_domain = each.value.auth_domain + name = "Environment: ${each.key}" + session_duration = each.value.session +} + +resource "cloudflare_zero_trust_organization" "with_count" { + count = 2 + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-count-${count.index}.cloudflareaccess.com" + name = "Count-based Organization ${count.index}" + allow_authenticate_via_warp = count.index == 0 +} + +resource "cloudflare_zero_trust_organization" "with_locals" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-locals.cloudflareaccess.com" + name = "${local.name_prefix} Organization with Locals" + + login_design = { + header_text = "Welcome to ${local.name_prefix}" + } +} + +resource "cloudflare_zero_trust_organization" "conditional" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-conditional.cloudflareaccess.com" + name = "Organization with Conditional" + is_ui_read_only = true + auto_redirect_to_identity = true + session_duration = true ? "12h" : "24h" +} + +# ===== Meta-Arguments (3) ===== +resource "cloudflare_zero_trust_organization" "with_lifecycle" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-lifecycle.cloudflareaccess.com" + name = "Organization with Lifecycle" + session_duration = "24h" + + lifecycle { + create_before_destroy = true + } +} + +resource "cloudflare_zero_trust_organization" "with_depends" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-depends.cloudflareaccess.com" + name = "Organization with depends_on" + + depends_on = [cloudflare_zero_trust_organization.minimal_account] +} + +resource "cloudflare_zero_trust_organization" "zone_comprehensive" { + zone_id = var.cloudflare_zone_id + auth_domain = "${local.name_prefix}-zone-comprehensive.cloudflareaccess.com" + name = "Comprehensive Zone-Scoped Organization" + is_ui_read_only = true + ui_read_only_toggle_reason = "Zone-level configuration" + auto_redirect_to_identity = true + session_duration = "12h" + allow_authenticate_via_warp = true + warp_auth_session_duration = "6h" + + + login_design = { + background_color = "#1e1e1e" + text_color = "#ffffff" + logo_path = "https://zone.cf-tf-test.com/logo.png" + header_text = "Zone Portal" + footer_text = "Zone-level Access" + } + custom_pages = { + forbidden = "33333333-3333-3333-3333-333333333333" + identity_denied = "44444444-4444-4444-4444-444444444444" + } +} diff --git a/integration/v4_to_v5/testdata/zero_trust_organization/input/terraform.tfstate b/integration/v4_to_v5/testdata/zero_trust_organization/input/terraform.tfstate new file mode 100644 index 0000000..5a9c4fc --- /dev/null +++ b/integration/v4_to_v5/testdata/zero_trust_organization/input/terraform.tfstate @@ -0,0 +1,701 @@ +{ + "lineage": "test-zero-trust-organization-lineage", + "outputs": {}, + "resources": [ + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-minimal-account.cloudflareaccess.com", + "name": "Minimal Account Organization" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "minimal_account", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "zone_id": "test-zone-456", + "id": "test-zone-456", + "auth_domain": "cftftest-minimal-zone.cloudflareaccess.com", + "name": "Minimal Zone Organization" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "minimal_zone", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-complete.cloudflareaccess.com", + "name": "Complete Organization", + "is_ui_read_only": true, + "ui_read_only_toggle_reason": "Managed via Terraform", + "user_seat_expiration_inactive_time": "730h", + "auto_redirect_to_identity": true, + "session_duration": "24h", + "allow_authenticate_via_warp": true, + "warp_auth_session_duration": "12h", + "login_design": { + "background_color": "#000000", + "text_color": "#FFFFFF", + "logo_path": "https://assets.cf-tf-test.com/logo.png", + "header_text": "Welcome to Our Platform", + "footer_text": "Powered by Cloudflare Access" + }, + "custom_pages": { + "forbidden": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "identity_denied": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "complete", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-login-design.cloudflareaccess.com", + "name": "Organization with Custom Login Design", + "login_design": { + "background_color": "#1a1a2e", + "text_color": "#eaeaea", + "logo_path": "https://assets.cf-tf-test.com/custom-logo.png", + "header_text": "Enterprise Portal", + "footer_text": "© 2026 Enterprise Inc." + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_login_design", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-custom-pages.cloudflareaccess.com", + "name": "Organization with Custom Pages", + "custom_pages": { + "forbidden": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "identity_denied": "dddddddd-dddd-dddd-dddd-dddddddddddd" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_custom_pages", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-both-nested.cloudflareaccess.com", + "name": "Organization with Both Nested Blocks", + "login_design": { + "background_color": "#2c3e50", + "text_color": "#ecf0f1" + }, + "custom_pages": { + "forbidden": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_both_nested", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-booleans-true.cloudflareaccess.com", + "name": "Organization with Booleans True", + "is_ui_read_only": true, + "auto_redirect_to_identity": true, + "allow_authenticate_via_warp": true + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_booleans_true", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-durations.cloudflareaccess.com", + "name": "Organization with All Duration Fields", + "session_duration": "48h", + "user_seat_expiration_inactive_time": "1460h", + "warp_auth_session_duration": "2h30m" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_durations", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-deprecated.cloudflareaccess.com", + "name": "Using Deprecated Resource Name" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "deprecated_name", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-current.cloudflareaccess.com", + "name": "Using Current Resource Name" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "current_name", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-partial-login-1.cloudflareaccess.com", + "name": "Partial Login Design - Colors Only", + "login_design": { + "background_color": "#16213e", + "text_color": "#f1f1f1" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_login_1", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-partial-login-2.cloudflareaccess.com", + "name": "Partial Login Design - Logo Only", + "login_design": { + "logo_path": "https://assets.cf-tf-test.com/simple-logo.png" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_login_2", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-partial-custom-1.cloudflareaccess.com", + "name": "Partial Custom Pages - Identity Denied Only", + "custom_pages": { + "identity_denied": "11111111-1111-1111-1111-111111111111" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_custom_1", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-partial-custom-2.cloudflareaccess.com", + "name": "Partial Custom Pages - Forbidden Only", + "custom_pages": { + "forbidden": "22222222-2222-2222-2222-222222222222" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "partial_custom_2", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-only-session.cloudflareaccess.com", + "name": "Organization with Only Session Duration", + "session_duration": "6h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_session", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-only-seat.cloudflareaccess.com", + "name": "Organization with Only Seat Expiration", + "user_seat_expiration_inactive_time": "730h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_seat_expiration", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-only-warp.cloudflareaccess.com", + "name": "Organization with Only WARP Duration", + "warp_auth_session_duration": "1h30m" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_warp_duration", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-only-readonly.cloudflareaccess.com", + "name": "Organization with Only Read-Only Flag", + "is_ui_read_only": true, + "ui_read_only_toggle_reason": "Locked down for compliance" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "only_readonly", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-special.cloudflareaccess.com", + "name": "Org with Special: @#$% & Chars!", + "ui_read_only_toggle_reason": "Locked: Deployment (2026-01-20 @ 14:00 UTC)", + "login_design": { + "header_text": "Welcome! Let's get started...", + "footer_text": "Questions? Email: support@cf-tf-test.com" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "special_chars", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-long.cloudflareaccess.com", + "name": "Organization with Very Long Name That Contains Many Characters", + "ui_read_only_toggle_reason": "This organization is locked because of maintenance", + "login_design": { + "header_text": "This is a very long header text about the organization", + "footer_text": "This footer contains legal disclaimers and copyright notices" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "long_strings", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-duration-formats.cloudflareaccess.com", + "name": "Organization with Various Duration Formats", + "session_duration": "2h45m", + "user_seat_expiration_inactive_time": "730h", + "warp_auth_session_duration": "90m" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "duration_formats", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-colors.cloudflareaccess.com", + "name": "Organization with Various Color Formats", + "login_design": { + "background_color": "#abc", + "text_color": "#FFFFFF" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "color_formats", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-booleans-false.cloudflareaccess.com", + "name": "Organization with Booleans False", + "is_ui_read_only": false, + "auto_redirect_to_identity": false, + "allow_authenticate_via_warp": false + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "booleans_false", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-org-alpha.cloudflareaccess.com", + "name": "Organization org-alpha", + "session_duration": "24h" + }, + "index_key": "org-alpha", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-org-beta.cloudflareaccess.com", + "name": "Organization org-beta", + "session_duration": "24h" + }, + "index_key": "org-beta", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-org-gamma.cloudflareaccess.com", + "name": "Organization org-gamma", + "session_duration": "24h" + }, + "index_key": "org-gamma", + "schema_version": 0 + } + ], + "mode": "managed", + "name": "foreach_set", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-dev.cloudflareaccess.com", + "name": "Environment: dev", + "session_duration": "12h" + }, + "index_key": "dev", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-staging.cloudflareaccess.com", + "name": "Environment: staging", + "session_duration": "24h" + }, + "index_key": "staging", + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-prod.cloudflareaccess.com", + "name": "Environment: prod", + "session_duration": "8h" + }, + "index_key": "prod", + "schema_version": 0 + } + ], + "mode": "managed", + "name": "foreach_map", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-count-0.cloudflareaccess.com", + "name": "Count-based Organization 0", + "allow_authenticate_via_warp": true + }, + "index_key": 0, + "schema_version": 0 + }, + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-count-1.cloudflareaccess.com", + "name": "Count-based Organization 1", + "allow_authenticate_via_warp": false + }, + "index_key": 1, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_count", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-locals.cloudflareaccess.com", + "name": "cftftest Organization with Locals", + "login_design": { + "header_text": "Welcome to cftftest" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_locals", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-conditional.cloudflareaccess.com", + "name": "Organization with Conditional", + "is_ui_read_only": true, + "auto_redirect_to_identity": true, + "session_duration": "12h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "conditional", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-lifecycle.cloudflareaccess.com", + "name": "Organization with Lifecycle", + "session_duration": "24h" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_lifecycle", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "account_id": "test-account-123", + "id": "test-account-123", + "auth_domain": "cftftest-depends.cloudflareaccess.com", + "name": "Organization with depends_on" + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "with_depends", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_access_organization" + }, + { + "instances": [ + { + "attributes": { + "zone_id": "test-zone-456", + "id": "test-zone-456", + "auth_domain": "cftftest-zone-comprehensive.cloudflareaccess.com", + "name": "Comprehensive Zone-Scoped Organization", + "is_ui_read_only": true, + "ui_read_only_toggle_reason": "Zone-level configuration", + "auto_redirect_to_identity": true, + "session_duration": "12h", + "allow_authenticate_via_warp": true, + "warp_auth_session_duration": "6h", + "login_design": { + "background_color": "#1e1e1e", + "text_color": "#ffffff", + "logo_path": "https://zone.cf-tf-test.com/logo.png", + "header_text": "Zone Portal", + "footer_text": "Zone-level Access" + }, + "custom_pages": { + "forbidden": "33333333-3333-3333-3333-333333333333", + "identity_denied": "44444444-4444-4444-4444-444444444444" + } + }, + "schema_version": 0 + } + ], + "mode": "managed", + "name": "zone_comprehensive", + "provider": "provider[\"registry.terraform.io/cloudflare/cloudflare\"]", + "type": "cloudflare_zero_trust_access_organization" + } + ], + "serial": 1, + "terraform_version": "1.0.0", + "version": 4 +} diff --git a/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization.tf b/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization.tf new file mode 100644 index 0000000..10c09ed --- /dev/null +++ b/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization.tf @@ -0,0 +1,347 @@ +variable "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" + org_names = toset(["org-alpha", "org-beta", "org-gamma"]) + org_configs = { + dev = { auth_domain = "${local.name_prefix}-dev.cloudflareaccess.com", session = "12h" } + staging = { auth_domain = "${local.name_prefix}-staging.cloudflareaccess.com", session = "24h" } + prod = { auth_domain = "${local.name_prefix}-prod.cloudflareaccess.com", session = "8h" } + } +} + +# ===== Basic Scenarios (8) ===== +resource "cloudflare_access_organization" "minimal_account" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-minimal-account.cloudflareaccess.com" + name = "Minimal Account Organization" +} + +resource "cloudflare_access_organization" "minimal_zone" { + zone_id = var.cloudflare_zone_id + auth_domain = "${local.name_prefix}-minimal-zone.cloudflareaccess.com" + name = "Minimal Zone Organization" +} + +resource "cloudflare_access_organization" "complete" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-complete.cloudflareaccess.com" + name = "Complete Organization" + is_ui_read_only = true + ui_read_only_toggle_reason = "Managed via Terraform" + user_seat_expiration_inactive_time = "730h" + auto_redirect_to_identity = true + session_duration = "24h" + allow_authenticate_via_warp = true + warp_auth_session_duration = "12h" + + login_design { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://assets.cf-tf-test.com/logo.png" + header_text = "Welcome to Our Platform" + footer_text = "Powered by Cloudflare Access" + } + + custom_pages { + forbidden = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + identity_denied = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } +} + +resource "cloudflare_access_organization" "with_login_design" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-login-design.cloudflareaccess.com" + name = "Organization with Custom Login Design" + + login_design { + background_color = "#1a1a2e" + text_color = "#eaeaea" + logo_path = "https://assets.cf-tf-test.com/custom-logo.png" + header_text = "Enterprise Portal" + footer_text = "© 2026 Enterprise Inc." + } +} + +resource "cloudflare_access_organization" "with_custom_pages" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-custom-pages.cloudflareaccess.com" + name = "Organization with Custom Pages" + + custom_pages { + forbidden = "cccccccc-cccc-cccc-cccc-cccccccccccc" + identity_denied = "dddddddd-dddd-dddd-dddd-dddddddddddd" + } +} + +resource "cloudflare_access_organization" "with_both_nested" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-both-nested.cloudflareaccess.com" + name = "Organization with Both Nested Blocks" + + login_design { + background_color = "#2c3e50" + text_color = "#ecf0f1" + } + + custom_pages { + forbidden = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" + } +} + +resource "cloudflare_access_organization" "with_booleans_true" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-booleans-true.cloudflareaccess.com" + name = "Organization with Booleans True" + is_ui_read_only = true + auto_redirect_to_identity = true + allow_authenticate_via_warp = true +} + +resource "cloudflare_access_organization" "with_durations" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-durations.cloudflareaccess.com" + name = "Organization with All Duration Fields" + session_duration = "48h" + user_seat_expiration_inactive_time = "1460h" + warp_auth_session_duration = "2h30m" +} + +# ===== v4 Resource Name Variations (2) ===== +resource "cloudflare_access_organization" "deprecated_name" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-deprecated.cloudflareaccess.com" + name = "Using Deprecated Resource Name" +} + +resource "cloudflare_zero_trust_access_organization" "current_name" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-current.cloudflareaccess.com" + name = "Using Current Resource Name" +} + +# ===== Partial Field Combinations (8) ===== +resource "cloudflare_access_organization" "partial_login_1" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-login-1.cloudflareaccess.com" + name = "Partial Login Design - Colors Only" + + login_design { + background_color = "#16213e" + text_color = "#f1f1f1" + } +} + +resource "cloudflare_access_organization" "partial_login_2" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-login-2.cloudflareaccess.com" + name = "Partial Login Design - Logo Only" + + login_design { + logo_path = "https://assets.cf-tf-test.com/simple-logo.png" + } +} + +resource "cloudflare_access_organization" "partial_custom_1" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-custom-1.cloudflareaccess.com" + name = "Partial Custom Pages - Identity Denied Only" + + custom_pages { + identity_denied = "11111111-1111-1111-1111-111111111111" + } +} + +resource "cloudflare_access_organization" "partial_custom_2" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-partial-custom-2.cloudflareaccess.com" + name = "Partial Custom Pages - Forbidden Only" + + custom_pages { + forbidden = "22222222-2222-2222-2222-222222222222" + } +} + +resource "cloudflare_access_organization" "only_session" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-session.cloudflareaccess.com" + name = "Organization with Only Session Duration" + session_duration = "6h" +} + +resource "cloudflare_access_organization" "only_seat_expiration" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-seat.cloudflareaccess.com" + name = "Organization with Only Seat Expiration" + user_seat_expiration_inactive_time = "730h" +} + +resource "cloudflare_access_organization" "only_warp_duration" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-warp.cloudflareaccess.com" + name = "Organization with Only WARP Duration" + warp_auth_session_duration = "1h30m" +} + +resource "cloudflare_access_organization" "only_readonly" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-only-readonly.cloudflareaccess.com" + name = "Organization with Only Read-Only Flag" + is_ui_read_only = true + ui_read_only_toggle_reason = "Locked down for compliance" +} + +# ===== Edge Cases (5) ===== +resource "cloudflare_access_organization" "special_chars" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-special.cloudflareaccess.com" + name = "Org with Special: @#$% & Chars!" + ui_read_only_toggle_reason = "Locked: Deployment (2026-01-20 @ 14:00 UTC)" + + login_design { + header_text = "Welcome! Let's get started..." + footer_text = "Questions? Email: support@cf-tf-test.com" + } +} + +resource "cloudflare_access_organization" "long_strings" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-long.cloudflareaccess.com" + name = "Organization with Very Long Name That Contains Many Characters" + ui_read_only_toggle_reason = "This organization is locked because of maintenance" + + login_design { + header_text = "This is a very long header text about the organization" + footer_text = "This footer contains legal disclaimers and copyright notices" + } +} + +resource "cloudflare_access_organization" "duration_formats" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-duration-formats.cloudflareaccess.com" + name = "Organization with Various Duration Formats" + session_duration = "2h45m" + user_seat_expiration_inactive_time = "730h" + warp_auth_session_duration = "90m" +} + +resource "cloudflare_access_organization" "color_formats" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-colors.cloudflareaccess.com" + name = "Organization with Various Color Formats" + + login_design { + background_color = "#abc" + text_color = "#FFFFFF" + } +} + +resource "cloudflare_access_organization" "booleans_false" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-booleans-false.cloudflareaccess.com" + name = "Organization with Booleans False" + is_ui_read_only = false + auto_redirect_to_identity = false + allow_authenticate_via_warp = false +} + +# ===== Terraform Patterns (8) ===== +resource "cloudflare_access_organization" "foreach_set" { + for_each = local.org_names + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-${each.value}.cloudflareaccess.com" + name = "Organization ${each.value}" + session_duration = "24h" +} + +resource "cloudflare_zero_trust_access_organization" "foreach_map" { + for_each = local.org_configs + account_id = var.cloudflare_account_id + auth_domain = each.value.auth_domain + name = "Environment: ${each.key}" + session_duration = each.value.session +} + +resource "cloudflare_access_organization" "with_count" { + count = 2 + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-count-${count.index}.cloudflareaccess.com" + name = "Count-based Organization ${count.index}" + allow_authenticate_via_warp = count.index == 0 +} + +resource "cloudflare_access_organization" "with_locals" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-locals.cloudflareaccess.com" + name = "${local.name_prefix} Organization with Locals" + + login_design { + header_text = "Welcome to ${local.name_prefix}" + } +} + +resource "cloudflare_access_organization" "conditional" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-conditional.cloudflareaccess.com" + name = "Organization with Conditional" + is_ui_read_only = true + auto_redirect_to_identity = true + session_duration = true ? "12h" : "24h" +} + +# ===== Meta-Arguments (3) ===== +resource "cloudflare_access_organization" "with_lifecycle" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-lifecycle.cloudflareaccess.com" + name = "Organization with Lifecycle" + session_duration = "24h" + + lifecycle { + create_before_destroy = true + } +} + +resource "cloudflare_access_organization" "with_depends" { + account_id = var.cloudflare_account_id + auth_domain = "${local.name_prefix}-depends.cloudflareaccess.com" + name = "Organization with depends_on" + + depends_on = [cloudflare_access_organization.minimal_account] +} + +resource "cloudflare_zero_trust_access_organization" "zone_comprehensive" { + zone_id = var.cloudflare_zone_id + auth_domain = "${local.name_prefix}-zone-comprehensive.cloudflareaccess.com" + name = "Comprehensive Zone-Scoped Organization" + is_ui_read_only = true + ui_read_only_toggle_reason = "Zone-level configuration" + auto_redirect_to_identity = true + session_duration = "12h" + allow_authenticate_via_warp = true + warp_auth_session_duration = "6h" + + login_design { + background_color = "#1e1e1e" + text_color = "#ffffff" + logo_path = "https://zone.cf-tf-test.com/logo.png" + header_text = "Zone Portal" + footer_text = "Zone-level Access" + } + + custom_pages { + forbidden = "33333333-3333-3333-3333-333333333333" + identity_denied = "44444444-4444-4444-4444-444444444444" + } +} diff --git a/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization_e2e.tf b/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization_e2e.tf new file mode 100644 index 0000000..632e65d --- /dev/null +++ b/integration/v4_to_v5/testdata/zero_trust_organization/input/zero_trust_organization_e2e.tf @@ -0,0 +1,90 @@ +# E2E Test Configuration for zero_trust_organization +# +# IMPORTANT: This resource is IMPORT-ONLY in v4 and is a SINGLETON. +# - Organizations cannot be created via Terraform +# - They are created when you enable Zero Trust in the Cloudflare dashboard +# - Each account has exactly ONE organization +# - Therefore, this config has only ONE resource +# +# E2E TEST WORKFLOW (AUTOMATED): +# ================================ +# +# The E2E runner handles imports automatically via the tf-migrate:import-address annotation! +# +# Automated workflow (run via `e2e run`): +# 1. PREREQUISITE: Enable Zero Trust in your Cloudflare account via dashboard +# 2. Runner automatically imports the organization (detects annotation below) +# 3. V4 apply configures the imported organization +# 4. Migration transforms v4 config to v5 +# 5. V5 plan verifies no drift +# 6. V5 apply succeeds +# +# Manual workflow (for testing without E2E runner): +# 1. PREREQUISITE: Enable Zero Trust via dashboard +# 2. IMPORT: terraform import cloudflare_access_organization.test YOUR_ACCOUNT_ID +# 3. APPLY: terraform apply +# 4. MIGRATE: tf-migrate migrate --config-dir . +# 5. UPGRADE: terraform init -upgrade +# 6. VERIFY: terraform plan # Should show "No changes" +# 7. APPLY: terraform apply +# +# SUCCESS CRITERIA: +# - Import succeeds (automatic in E2E runner) +# - V4 apply succeeds +# - Migration transforms config correctly +# - V5 plan shows no changes +# - V5 apply succeeds +# +# NOTE: We only have ONE resource because organizations are singletons. +# We cannot test both v4 resource names (cloudflare_access_organization and +# cloudflare_zero_trust_access_organization) simultaneously because they would +# both try to manage the same underlying organization. + +locals { + name_prefix = "cftftest" +} + +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 +} + +# Basic organization configuration for E2E testing +# NOTE: This is a SINGLETON resource - only one organization per account. +# +# IMPORT ANNOTATION: The line below tells the E2E runner to automatically import this resource. +# The runner will execute: terraform import module.zero_trust_organization.cloudflare_access_organization.test +# tf-migrate:import-address=${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id + name = "${local.name_prefix} E2E Test Organization" + auth_domain = "${local.name_prefix}-e2e.cloudflareaccess.com" + + # Test MaxItems:1 block transformation + login_design { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://e2e-test.cf-tf-test.com/logo.png" + header_text = "E2E Test Portal" + footer_text = "E2E Testing" + } + + # Test all optional fields + session_duration = "24h" + user_seat_expiration_inactive_time = "730h" + warp_auth_session_duration = "12h" + is_ui_read_only = true + # ui_read_only_toggle_reason = "E2E Testing" + auto_redirect_to_identity = true + allow_authenticate_via_warp = true +} diff --git a/internal/e2e-runner/import.go b/internal/e2e-runner/import.go new file mode 100644 index 0000000..4770701 --- /dev/null +++ b/internal/e2e-runner/import.go @@ -0,0 +1,190 @@ +// import.go handles Terraform import operations for import-only resources. +// +// This file provides functionality to: +// - Parse import annotations from Terraform configuration files +// - Execute terraform import commands for resources that cannot be created +// - Support variable interpolation in import addresses +// +// Import annotations use the format: +// # tf-migrate:import-address=
+// resource "type" "name" { ... } +// +// Where
can include variable references like ${var.cloudflare_account_id} +package e2e + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// ImportSpec represents a resource that needs to be imported +type ImportSpec struct { + ResourceType string // e.g., "cloudflare_access_organization" + ResourceName string // e.g., "test" + ResourceAddress string // e.g., "cloudflare_access_organization.test" + ImportAddress string // e.g., "account/${var.cloudflare_account_id}" + ModuleName string // e.g., "zero_trust_organization" (extracted from file path) +} + +// findImportSpecs scans a directory for import annotations and returns import specifications +func findImportSpecs(dir string) ([]ImportSpec, error) { + var specs []ImportSpec + + // Walk through all .tf files in subdirectories (modules) + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip if not a .tf file + if info.IsDir() || !strings.HasSuffix(info.Name(), ".tf") { + return nil + } + + // Skip root directory files (only look in modules) + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + if !strings.Contains(relPath, string(filepath.Separator)) { + return nil // Skip files in root directory + } + + // Extract module name from path (first directory component) + moduleName := strings.Split(relPath, string(filepath.Separator))[0] + + // Parse file for import annotations + fileSpecs, err := parseImportAnnotations(path, moduleName) + if err != nil { + return fmt.Errorf("failed to parse %s: %w", path, err) + } + + specs = append(specs, fileSpecs...) + return nil + }) + + return specs, err +} + +// parseImportAnnotations parses a single .tf file for import annotations +func parseImportAnnotations(filePath, moduleName string) ([]ImportSpec, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var specs []ImportSpec + scanner := bufio.NewScanner(file) + + // Regex patterns + importAnnotation := regexp.MustCompile(`^\s*#\s*tf-migrate:import-address=(.+)$`) + resourceDeclaration := regexp.MustCompile(`^\s*resource\s+"([^"]+)"\s+"([^"]+)"\s*{`) + + var pendingImportAddress string + + for scanner.Scan() { + line := scanner.Text() + + // Check for import annotation + if matches := importAnnotation.FindStringSubmatch(line); matches != nil { + pendingImportAddress = strings.TrimSpace(matches[1]) + continue + } + + // Check for resource declaration following an import annotation + if pendingImportAddress != "" { + if matches := resourceDeclaration.FindStringSubmatch(line); matches != nil { + resourceType := matches[1] + resourceName := matches[2] + + specs = append(specs, ImportSpec{ + ResourceType: resourceType, + ResourceName: resourceName, + ResourceAddress: fmt.Sprintf("%s.%s", resourceType, resourceName), + ImportAddress: pendingImportAddress, + ModuleName: moduleName, + }) + + pendingImportAddress = "" // Reset + } + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return specs, nil +} + +// resolveImportAddress replaces variable references in import address with actual values +func resolveImportAddress(address string, env *E2EEnv) string { + // Replace common variable references + address = strings.ReplaceAll(address, "${var.cloudflare_account_id}", env.AccountID) + address = strings.ReplaceAll(address, "${var.cloudflare_zone_id}", env.ZoneID) + address = strings.ReplaceAll(address, "${var.cloudflare_domain}", env.Domain) + + return address +} + +// executeImports runs terraform import commands for all import specs +func executeImports(ctx *testContext, specs []ImportSpec) error { + if len(specs) == 0 { + return nil // No imports needed + } + + printHeader("Importing Resources") + printYellow("Found %d resource(s) marked for import", len(specs)) + fmt.Println() + + tf := NewTerraformRunner(ctx.v4Dir) + + // Set R2 credentials for terraform commands + r2AccessKey := os.Getenv("CLOUDFLARE_R2_ACCESS_KEY_ID") + r2SecretKey := os.Getenv("CLOUDFLARE_R2_SECRET_ACCESS_KEY") + if r2AccessKey != "" && r2SecretKey != "" { + tf.EnvVars["AWS_ACCESS_KEY_ID"] = r2AccessKey + tf.EnvVars["AWS_SECRET_ACCESS_KEY"] = r2SecretKey + } + + for _, spec := range specs { + // Resolve variables in import address + importAddress := resolveImportAddress(spec.ImportAddress, ctx.env) + + // Build full resource address including module prefix + fullResourceAddress := fmt.Sprintf("module.%s.%s", spec.ModuleName, spec.ResourceAddress) + + printYellow("Importing %s...", fullResourceAddress) + printBlue(" Import address: %s", importAddress) + + // Run terraform import + output, err := tf.Run("import", "-no-color", "-input=false", fullResourceAddress, importAddress) + if err != nil { + // Check if resource already exists in state + if strings.Contains(output, "Resource already managed by Terraform") || + strings.Contains(output, "already exists in state") { + printGreen(" ✓ Resource already imported") + continue + } + + printError("Failed to import %s", fullResourceAddress) + fmt.Println() + printRed("Error output:") + fmt.Println(output) + return fmt.Errorf("import failed for %s: %w", fullResourceAddress, err) + } + + printSuccess("Successfully imported %s", fullResourceAddress) + fmt.Println() + } + + printSuccess("All imports completed") + fmt.Println() + + return nil +} diff --git a/internal/e2e-runner/import_test.go b/internal/e2e-runner/import_test.go new file mode 100644 index 0000000..1a74809 --- /dev/null +++ b/internal/e2e-runner/import_test.go @@ -0,0 +1,256 @@ +package e2e + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseImportAnnotations(t *testing.T) { + tests := []struct { + name string + fileContent string + moduleName string + expectedSpecs int + expectedFirst *ImportSpec + }{ + { + name: "single import annotation", + fileContent: `# Some comment +# tf-migrate:import-address=account/${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id +} +`, + moduleName: "zero_trust_organization", + expectedSpecs: 1, + expectedFirst: &ImportSpec{ + ResourceType: "cloudflare_access_organization", + ResourceName: "test", + ResourceAddress: "cloudflare_access_organization.test", + ImportAddress: "account/${var.cloudflare_account_id}", + ModuleName: "zero_trust_organization", + }, + }, + { + name: "multiple import annotations", + fileContent: `# tf-migrate:import-address=zones/${var.cloudflare_zone_id}/settings/waf +resource "cloudflare_waf_package" "test" { + zone_id = var.cloudflare_zone_id +} + +# tf-migrate:import-address=account/${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id +} +`, + moduleName: "test_module", + expectedSpecs: 2, + expectedFirst: &ImportSpec{ + ResourceType: "cloudflare_waf_package", + ResourceName: "test", + ResourceAddress: "cloudflare_waf_package.test", + ImportAddress: "zones/${var.cloudflare_zone_id}/settings/waf", + ModuleName: "test_module", + }, + }, + { + name: "no import annotations", + fileContent: `# Regular comment +resource "cloudflare_record" "test" { + zone_id = var.cloudflare_zone_id + name = "test" +} +`, + moduleName: "dns_record", + expectedSpecs: 0, + }, + { + name: "annotation with spaces", + fileContent: ` # tf-migrate:import-address=account/${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id +} +`, + moduleName: "zero_trust_organization", + expectedSpecs: 1, + expectedFirst: &ImportSpec{ + ResourceType: "cloudflare_access_organization", + ResourceName: "test", + ResourceAddress: "cloudflare_access_organization.test", + ImportAddress: "account/${var.cloudflare_account_id}", + ModuleName: "zero_trust_organization", + }, + }, + { + name: "annotation not followed by resource", + fileContent: `# tf-migrate:import-address=account/${var.cloudflare_account_id} +# Some other comment +variable "test" { + type = string +} +`, + moduleName: "test_module", + expectedSpecs: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test.tf") + if err := os.WriteFile(tmpFile, []byte(tt.fileContent), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + // Parse annotations + specs, err := parseImportAnnotations(tmpFile, tt.moduleName) + if err != nil { + t.Fatalf("parseImportAnnotations() error = %v", err) + } + + // Check number of specs + if len(specs) != tt.expectedSpecs { + t.Errorf("parseImportAnnotations() got %d specs, want %d", len(specs), tt.expectedSpecs) + } + + // Check first spec if expected + if tt.expectedFirst != nil && len(specs) > 0 { + got := specs[0] + want := tt.expectedFirst + + if got.ResourceType != want.ResourceType { + t.Errorf("ResourceType = %q, want %q", got.ResourceType, want.ResourceType) + } + if got.ResourceName != want.ResourceName { + t.Errorf("ResourceName = %q, want %q", got.ResourceName, want.ResourceName) + } + if got.ResourceAddress != want.ResourceAddress { + t.Errorf("ResourceAddress = %q, want %q", got.ResourceAddress, want.ResourceAddress) + } + if got.ImportAddress != want.ImportAddress { + t.Errorf("ImportAddress = %q, want %q", got.ImportAddress, want.ImportAddress) + } + if got.ModuleName != want.ModuleName { + t.Errorf("ModuleName = %q, want %q", got.ModuleName, want.ModuleName) + } + } + }) + } +} + +func TestFindImportSpecs(t *testing.T) { + // Create temporary directory structure + tmpDir := t.TempDir() + + // Create module directories + module1Dir := filepath.Join(tmpDir, "module1") + module2Dir := filepath.Join(tmpDir, "module2") + if err := os.MkdirAll(module1Dir, 0755); err != nil { + t.Fatalf("failed to create module1 dir: %v", err) + } + if err := os.MkdirAll(module2Dir, 0755); err != nil { + t.Fatalf("failed to create module2 dir: %v", err) + } + + // Create test files + module1Content := `# tf-migrate:import-address=account/${var.cloudflare_account_id} +resource "cloudflare_access_organization" "test" { + account_id = var.cloudflare_account_id +} +` + if err := os.WriteFile(filepath.Join(module1Dir, "main.tf"), []byte(module1Content), 0644); err != nil { + t.Fatalf("failed to write module1 file: %v", err) + } + + module2Content := `# Regular resource, no import needed +resource "cloudflare_record" "test" { + zone_id = var.cloudflare_zone_id + name = "test" +} +` + if err := os.WriteFile(filepath.Join(module2Dir, "main.tf"), []byte(module2Content), 0644); err != nil { + t.Fatalf("failed to write module2 file: %v", err) + } + + // Create a root-level file (should be ignored) + rootContent := `# tf-migrate:import-address=should_be_ignored +resource "cloudflare_something" "root" { + id = "test" +} +` + if err := os.WriteFile(filepath.Join(tmpDir, "provider.tf"), []byte(rootContent), 0644); err != nil { + t.Fatalf("failed to write root file: %v", err) + } + + // Find import specs + specs, err := findImportSpecs(tmpDir) + if err != nil { + t.Fatalf("findImportSpecs() error = %v", err) + } + + // Should find only the one from module1 + if len(specs) != 1 { + t.Errorf("findImportSpecs() got %d specs, want 1", len(specs)) + } + + if len(specs) > 0 { + got := specs[0] + if got.ModuleName != "module1" { + t.Errorf("ModuleName = %q, want %q", got.ModuleName, "module1") + } + if got.ResourceType != "cloudflare_access_organization" { + t.Errorf("ResourceType = %q, want %q", got.ResourceType, "cloudflare_access_organization") + } + } +} + +func TestResolveImportAddress(t *testing.T) { + env := &E2EEnv{ + AccountID: "test-account-123", + ZoneID: "test-zone-456", + Domain: "example.com", + } + + tests := []struct { + name string + address string + expected string + }{ + { + name: "account ID substitution", + address: "account/${var.cloudflare_account_id}", + expected: "account/test-account-123", + }, + { + name: "zone ID substitution", + address: "zones/${var.cloudflare_zone_id}/settings/waf", + expected: "zones/test-zone-456/settings/waf", + }, + { + name: "domain substitution", + address: "${var.cloudflare_domain}/path", + expected: "example.com/path", + }, + { + name: "multiple substitutions", + address: "account/${var.cloudflare_account_id}/zone/${var.cloudflare_zone_id}", + expected: "account/test-account-123/zone/test-zone-456", + }, + { + name: "no substitutions", + address: "static/path/123", + expected: "static/path/123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveImportAddress(tt.address, env) + if got != tt.expected { + t.Errorf("resolveImportAddress() = %q, want %q", got, tt.expected) + } + }) + } +} diff --git a/internal/e2e-runner/runner.go b/internal/e2e-runner/runner.go index 63ede80..76e1773 100644 --- a/internal/e2e-runner/runner.go +++ b/internal/e2e-runner/runner.go @@ -739,6 +739,21 @@ func runV4Tests(ctx *testContext) error { } printSuccess("Terraform init successful (remote state loaded from R2)") + // Check for resources that need to be imported + fmt.Println() + importSpecs, err := findImportSpecs(ctx.v4Dir) + if err != nil { + printError("Failed to scan for import annotations") + return fmt.Errorf("failed to find import specs: %w", err) + } + + // Execute imports if needed + if len(importSpecs) > 0 { + if err := executeImports(ctx, importSpecs); err != nil { + return err + } + } + // Run terraform plan printYellow("Running terraform plan in v4/...") planArgs := append([]string{"plan", "-no-color", "-out=" + filepath.Join(ctx.tmpDir, "v4.tfplan"), "-input=false"}, ctx.targetArgs...) diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 68c5eec..58d634f 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -53,6 +53,7 @@ import ( "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_gateway_policy" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_list" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_local_fallback_domain" + "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_organization" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_tunnel_cloudflared" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_tunnel_cloudflared_config" "github.com/cloudflare/tf-migrate/internal/resources/zero_trust_tunnel_cloudflared_route" @@ -124,6 +125,7 @@ func RegisterAllMigrations() { zero_trust_gateway_policy.NewV4ToV5Migrator() zero_trust_list.NewV4ToV5Migrator() zero_trust_local_fallback_domain.NewV4ToV5Migrator() + zero_trust_organization.NewV4ToV5Migrator() zero_trust_tunnel_cloudflared.NewV4ToV5Migrator() zero_trust_tunnel_cloudflared_config.NewV4ToV5Migrator() zero_trust_tunnel_cloudflared_route.NewV4ToV5Migrator() diff --git a/internal/resources/zero_trust_organization/v4_to_v5.go b/internal/resources/zero_trust_organization/v4_to_v5.go new file mode 100644 index 0000000..fe4677e --- /dev/null +++ b/internal/resources/zero_trust_organization/v4_to_v5.go @@ -0,0 +1,120 @@ +package zero_trust_organization + +import ( + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/cloudflare/tf-migrate/internal" + "github.com/cloudflare/tf-migrate/internal/transform" + "github.com/cloudflare/tf-migrate/internal/transform/state" + tfhcl "github.com/cloudflare/tf-migrate/internal/transform/hcl" +) + +type V4ToV5Migrator struct { +} + +// NewV4ToV5Migrator creates a new migrator instance and registers BOTH v4 resource names. +// v4 has two aliases: cloudflare_access_organization (deprecated) and +// cloudflare_zero_trust_access_organization (current). Both use the same schema +// and migrate to cloudflare_zero_trust_organization in v5. +func NewV4ToV5Migrator() transform.ResourceTransformer { + migrator := &V4ToV5Migrator{} + // Register BOTH v4 resource names (they're aliases with identical schemas) + internal.RegisterMigrator("cloudflare_access_organization", "v4", "v5", migrator) + internal.RegisterMigrator("cloudflare_zero_trust_access_organization", "v4", "v5", migrator) + return migrator +} + +func (m *V4ToV5Migrator) GetResourceType() string { + // Return the v5 resource name + return "cloudflare_zero_trust_organization" +} + +func (m *V4ToV5Migrator) CanHandle(resourceType string) bool { + // Handle BOTH v4 resource names + return resourceType == "cloudflare_access_organization" || + resourceType == "cloudflare_zero_trust_access_organization" +} + +// GetResourceRename implements the ResourceRenamer interface. +// Both v4 names rename to the same v5 name. +func (m *V4ToV5Migrator) GetResourceRename() (string, string) { + // This is called for the registered name, but both v4 names go to the same v5 name + return "cloudflare_access_organization", "cloudflare_zero_trust_organization" +} + +// Preprocess performs any string-level transformations before HCL parsing. +// For zero_trust_organization, no preprocessing is needed. +func (m *V4ToV5Migrator) Preprocess(content string) string { + return content +} + +// TransformConfig transforms the HCL configuration from v4 to v5. +func (m *V4ToV5Migrator) TransformConfig(ctx *transform.Context, block *hclwrite.Block) (*transform.TransformResult, error) { + // Rename resource type from EITHER v4 name to v5 name + currentType := tfhcl.GetResourceType(block) + if currentType == "cloudflare_access_organization" || currentType == "cloudflare_zero_trust_access_organization" { + tfhcl.RenameResourceType(block, currentType, "cloudflare_zero_trust_organization") + } + + body := block.Body() + + // Convert login_design block to attribute (MaxItems:1 → SingleNestedAttribute) + // v4: login_design { background_color = "#000" ... } + // v5: login_design = { background_color = "#000" ... } + tfhcl.ConvertBlocksToAttribute(body, "login_design", "login_design", func(block *hclwrite.Block) {}) + + // Convert custom_pages block to attribute (MaxItems:1 → SingleNestedAttribute) + // v4: custom_pages { forbidden = "id" ... } + // v5: custom_pages = { forbidden = "id" ... } + tfhcl.ConvertBlocksToAttribute(body, "custom_pages", "custom_pages", func(block *hclwrite.Block) {}) + + return &transform.TransformResult{ + Blocks: []*hclwrite.Block{block}, + RemoveOriginal: false, + }, nil +} + +// TransformState transforms the JSON state from v4 to v5. +func (m *V4ToV5Migrator) TransformState(ctx *transform.Context, stateJSON gjson.Result, resourcePath, resourceName string) (string, error) { + result := stateJSON.String() + + // Get attributes + attrs := stateJSON.Get("attributes") + if !attrs.Exists() { + // Even for invalid instances, set schema_version + result, _ = sjson.Set(result, "schema_version", 0) + return result, nil + } + + // Handle account_id / zone_id mutual exclusivity + // v5 requires that only one is set, the other must be null + accountID := attrs.Get("account_id") + zoneID := attrs.Get("zone_id") + + if accountID.Exists() && accountID.String() != "" { + // This is an account-level organization, ensure zone_id is null + result, _ = sjson.Delete(result, "attributes.zone_id") + } else if zoneID.Exists() && zoneID.String() != "" { + // This is a zone-level organization, ensure account_id is null + result, _ = sjson.Delete(result, "attributes.account_id") + } + + // Convert MaxItems:1 arrays to objects + // login_design: [{"background_color": "#000", ...}] → {"background_color": "#000", ...} + // Handles empty arrays by deleting them + result = state.ConvertMaxItemsOneArrayToObject(result, "attributes", attrs, "login_design") + result = state.ConvertMaxItemsOneArrayToObject(result, "attributes", attrs, "custom_pages") + + // Add default boolean values if missing (v5 has defaults, v4 didn't) + // This prevents PATCH operations when migrating resources that didn't set these + result = state.EnsureField(result, "attributes", attrs, "allow_authenticate_via_warp", false) + result = state.EnsureField(result, "attributes", attrs, "auto_redirect_to_identity", false) + result = state.EnsureField(result, "attributes", attrs, "is_ui_read_only", false) + + // Set schema_version to 0 for v5 (ALWAYS required!) + result, _ = sjson.Set(result, "schema_version", 0) + + return result, nil +} diff --git a/internal/resources/zero_trust_organization/v4_to_v5_test.go b/internal/resources/zero_trust_organization/v4_to_v5_test.go new file mode 100644 index 0000000..253f801 --- /dev/null +++ b/internal/resources/zero_trust_organization/v4_to_v5_test.go @@ -0,0 +1,485 @@ +package zero_trust_organization + +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: "Minimal resource - cloudflare_access_organization", + Input: `resource "cloudflare_access_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" +}`, + }, + { + Name: "Minimal resource - cloudflare_zero_trust_access_organization", + Input: `resource "cloudflare_zero_trust_access_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" +}`, + }, + { + Name: "With login_design block", + Input: `resource "cloudflare_access_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + login_design { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://example.com/logo.png" + header_text = "Welcome" + footer_text = "Powered by Cloudflare" + } +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + login_design = { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://example.com/logo.png" + header_text = "Welcome" + footer_text = "Powered by Cloudflare" + } +}`, + }, + { + Name: "With custom_pages block", + Input: `resource "cloudflare_access_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + custom_pages { + forbidden = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + identity_denied = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + } +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + custom_pages = { + forbidden = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + identity_denied = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + } +}`, + }, + { + Name: "Complete organization with all fields", + Input: `resource "cloudflare_access_organization" "example" { + account_id = "f037e56e89293a057740de681ac9abbe" + auth_domain = "example.cloudflareaccess.com" + name = "Complete Organization" + + is_ui_read_only = true + ui_read_only_toggle_reason = "Managed by Terraform" + + user_seat_expiration_inactive_time = "730h" + auto_redirect_to_identity = true + session_duration = "24h" + + login_design { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://example.com/logo.png" + header_text = "Welcome" + footer_text = "Powered by Cloudflare" + } + + custom_pages { + forbidden = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + identity_denied = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + } + + allow_authenticate_via_warp = true + warp_auth_session_duration = "12h" +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + account_id = "f037e56e89293a057740de681ac9abbe" + auth_domain = "example.cloudflareaccess.com" + name = "Complete Organization" + + is_ui_read_only = true + ui_read_only_toggle_reason = "Managed by Terraform" + + user_seat_expiration_inactive_time = "730h" + auto_redirect_to_identity = true + session_duration = "24h" + + login_design = { + background_color = "#000000" + text_color = "#FFFFFF" + logo_path = "https://example.com/logo.png" + header_text = "Welcome" + footer_text = "Powered by Cloudflare" + } + + custom_pages = { + forbidden = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + identity_denied = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + } + + allow_authenticate_via_warp = true + warp_auth_session_duration = "12h" +}`, + }, + { + Name: "Partial login_design block", + Input: `resource "cloudflare_access_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + login_design { + background_color = "#000000" + text_color = "#FFFFFF" + } +}`, + Expected: `resource "cloudflare_zero_trust_organization" "example" { + auth_domain = "example.cloudflareaccess.com" + name = "My Organization" + + login_design = { + background_color = "#000000" + text_color = "#FFFFFF" + } +}`, + }, + { + Name: "Multiple resources in one file", + Input: `resource "cloudflare_access_organization" "account_org" { + account_id = "f037e56e89293a057740de681ac9abbe" + auth_domain = "account.cloudflareaccess.com" + name = "Account Organization" +} + +resource "cloudflare_zero_trust_access_organization" "zone_org" { + zone_id = "023e105f4ecef8ad9ca31a8372d0c353" + auth_domain = "zone.cloudflareaccess.com" + name = "Zone Organization" +}`, + Expected: `resource "cloudflare_zero_trust_organization" "account_org" { + account_id = "f037e56e89293a057740de681ac9abbe" + auth_domain = "account.cloudflareaccess.com" + name = "Account Organization" +} + +resource "cloudflare_zero_trust_organization" "zone_org" { + zone_id = "023e105f4ecef8ad9ca31a8372d0c353" + auth_domain = "zone.cloudflareaccess.com" + name = "Zone Organization" +}`, + }, + } + + testhelpers.RunConfigTransformTests(t, tests, migrator) + }) + + t.Run("StateTransformation", func(t *testing.T) { + tests := []testhelpers.StateTestCase{ + { + Name: "Minimal state", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization" + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "With login_design array (MaxItems:1)", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "login_design": [{ + "background_color": "#000000", + "text_color": "#FFFFFF", + "logo_path": "https://example.com/logo.png", + "header_text": "Welcome", + "footer_text": "Powered by Cloudflare" + }] + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "login_design": { + "background_color": "#000000", + "text_color": "#FFFFFF", + "logo_path": "https://example.com/logo.png", + "header_text": "Welcome", + "footer_text": "Powered by Cloudflare" + }, + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "Empty login_design array", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "login_design": [] + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "Zone-scoped organization (zone_id instead of account_id)", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization" + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "With custom_pages array (MaxItems:1)", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "custom_pages": [{ + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "identity_denied": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + }] + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "custom_pages": { + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "identity_denied": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + }, + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "With both login_design and custom_pages arrays", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "login_design": [{ + "background_color": "#000000", + "text_color": "#FFFFFF" + }], + "custom_pages": [{ + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }] + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "login_design": { + "background_color": "#000000", + "text_color": "#FFFFFF" + }, + "custom_pages": { + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "allow_authenticate_via_warp": false, + "auto_redirect_to_identity": false, + "is_ui_read_only": false + } +}`, + }, + { + Name: "With existing boolean values (should be preserved)", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "allow_authenticate_via_warp": true, + "auto_redirect_to_identity": true, + "is_ui_read_only": true + } +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "My Organization", + "allow_authenticate_via_warp": true, + "auto_redirect_to_identity": true, + "is_ui_read_only": true + } +}`, + }, + { + Name: "Complete state with all fields", + Input: `{ + "type": "cloudflare_zero_trust_access_organization", + "name": "example", + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "Complete Organization", + "session_duration": "24h", + "user_seat_expiration_inactive_time": "730h", + "warp_auth_session_duration": "12h", + "ui_read_only_toggle_reason": "Managed by Terraform", + "is_ui_read_only": true, + "auto_redirect_to_identity": true, + "allow_authenticate_via_warp": true, + "login_design": [{ + "background_color": "#000000", + "text_color": "#FFFFFF", + "logo_path": "https://example.com/logo.png", + "header_text": "Welcome", + "footer_text": "Powered by Cloudflare" + }], + "custom_pages": [{ + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "identity_denied": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + }] + } +}`, + Expected: `{ + "type": "cloudflare_zero_trust_access_organization", + "name": "example", + "schema_version": 0, + "attributes": { + "account_id": "f037e56e89293a057740de681ac9abbe", + "auth_domain": "example.cloudflareaccess.com", + "name": "Complete Organization", + "session_duration": "24h", + "user_seat_expiration_inactive_time": "730h", + "warp_auth_session_duration": "12h", + "ui_read_only_toggle_reason": "Managed by Terraform", + "is_ui_read_only": true, + "auto_redirect_to_identity": true, + "allow_authenticate_via_warp": true, + "login_design": { + "background_color": "#000000", + "text_color": "#FFFFFF", + "logo_path": "https://example.com/logo.png", + "header_text": "Welcome", + "footer_text": "Powered by Cloudflare" + }, + "custom_pages": { + "forbidden": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "identity_denied": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + } + } +}`, + }, + { + Name: "Invalid instance (no attributes) - should still set schema_version", + Input: `{ + "type": "cloudflare_access_organization", + "name": "example" +}`, + Expected: `{ + "type": "cloudflare_access_organization", + "name": "example", + "schema_version": 0 +}`, + }, + } + + testhelpers.RunStateTransformTests(t, tests, migrator) + }) +}