Skip to content

Migration UX improvements and bug fixes#254

Merged
tamas-jozsa merged 16 commits intomainfrom
leaked-credential
Mar 27, 2026
Merged

Migration UX improvements and bug fixes#254
tamas-jozsa merged 16 commits intomainfrom
leaked-credential

Conversation

@tamas-jozsa
Copy link
Copy Markdown
Collaborator

Migration UX improvements and bug fixes

Actionable import blocks

Two resources that previously required manual terraform import commands now auto-generate import {} blocks with placeholder IDs in the migrated files:

  • cloudflare_argo — when both smart_routing and tiered_caching are present, the cloudflare_argo_tiered_caching split resource gets an import block. If zone_id is a literal string it's embedded automatically; variable references fall back to <zone_id>.
  • cloudflare_leaked_credential_check_rule — the v4 provider had a bug where detection_id was never stored in state. The migrator now generates an import block with <zone_id>/<detection_id> placeholders and includes the exact curl command to find the real detection ID.

Both warnings include the full file path so users know exactly where to make the edit.

Cleaner diagnostic output

Introduced transform.DiagInfo as a named info severity level (suppressed by default, shown with --verbose). Deprecation notices that the migrator already handled automatically are demoted from warnings to info:

  • certificate_packwait_for_active_status removed
  • workers_scripttags removed
  • page_ruleminify / disable_railgun removed
  • origin_ca_certificatemin_days_for_renewal removed

The summary line now reads: Migration diagnostics: 2 warning(s), 4 info message(s) suppressed (use --verbose to see)

Improved next steps output

  • "Review the warnings above" appears as step 1 only when there are actionable warnings, on both the success and version-fetch-failure paths
  • Explicit git add . / git commit / git push commands shown instead of prose
  • Steps auto-number correctly regardless of which steps are shown

Provider version detection fixes

  • Fixed the GitHub API fetch to include pre-releases (prerelease: true) — previously only draft was checked, so all beta releases were skipped and the hardcoded fallback was always used
  • Removed the hardcoded fallback version entirely — if the API is unreachable, users are told explicitly to update required_providers manually with a link to the releases page
  • New --target-provider-version flag to bypass the API lookup entirely (useful in CI or air-gapped environments)

Bug fixes

  • tiered_cache migrator idempotency — re-running on an already-migrated file previously generated duplicate resource blocks causing terraform init to fail with "Duplicate resource" errors. Fixed by bailing out early when cache_type is absent.
  • Research team migration issues — TKT-001 and TKT-004 fixes carried forward

Adds test cases that reproduce 6 migration bugs discovered when the
research team (terraform-cfaccounts MR !7756) migrated their Terraform
config from Cloudflare Provider v4 → v5.

TKT-001: cloudflare_authenticated_origin_pulls — cert_id still references
  cloudflare_authenticated_origin_pulls_certificate instead of
  cloudflare_authenticated_origin_pulls_hostname_certificate in for_each
  resources. The research team had to manually fix this with sed.

TKT-002: cloudflare_zero_trust_access_policy — include/exclude/require
  remain as HCL blocks instead of being converted to include = [{ ... }]
  attribute list syntax. This causes 'invalid JSON, expected {, got ['
  during state upgrade.

TKT-003: cloudflare_zero_trust_access_policy — application_id and
  precedence are not removed from policies and not moved to the linked
  access application's policies = [...] block. Causes
  'unsupported attribute application_id' state decode failure.

TKT-004: service_token = [id] list not converted to
  service_token = { token_id = id } object format.

TKT-005: email_domain = ['domain'] list not converted to
  email_domain = { domain = 'domain' } object format.

TKT-006: any_valid_service_token = true not converted to
  any_valid_service_token = {} empty object format.

The expected output files reflect the CURRENT (buggy) migration output.
Tests pass now to confirm bugs are reproduced. They will need to be
updated to correct v5 syntax once the bugs are fixed.

Tracking tickets created in tickets/ directory.
TKT-001: Fix cloudflare_authenticated_origin_pulls cert_id reference update
  The GetResourceRename for authenticated_origin_pulls_certificate now returns
  the hostname certificate type as the rename target, so the global cross-file
  reference postprocessor correctly renames all cert_id references from
  cloudflare_authenticated_origin_pulls_certificate.X to
  cloudflare_authenticated_origin_pulls_hostname_certificate.X — including
  for_each references like resource.name[each.key].id.
  Removed the heuristic name-based Postprocess fallback.

TKT-004: Fix service_token list not converted to {token_id=...} object
  Added 'service_token' to the arrayAttrs map in expandArrayAttribute with
  inner field name 'token_id', so service_token = [id_ref] is correctly
  converted to service_token = { token_id = id_ref }.

Root cause of TKT-004 (and fix): The buildExprTokens serializer only emitted
  the root name of ScopeTraversalExpr, dropping .attr, [index] etc. Fixed to
  serialize the full traversal chain including attribute access, index access,
  and relative traversals (each.key, each.value). Also added handlers for
  RelativeTraversalExpr, IndexExpr, and FunctionCallExpr.

TKT-002/005/006 were already working correctly — email_domain, any_valid_service_token,
  and include block → list conversion all produce correct v5 output.

TKT-003: application_id is already removed with a warning diagnostic. The
  warning guides the user to manually add policies = [...] to the application.

Tests: Updated expected fixtures to reflect correct migration output.
  Added new test patterns covering all research team issues.
…al API credentials

authenticated_origin_pulls:
- Add authenticated_origin_pulls_e2e.tf with only zone-wide AOP
- Exclude per-hostname certificate resources — the API rejects fake cert data
  (error 1408). TKT-001 cert_id fix is validated by integration tests only.

zero_trust_access_policy:
- Add zero_trust_access_policy_e2e.tf excluding:
  - service_token_policy/multi/combined (need real service token resources)
  - app_scoped_policy (needs real domain/application, TKT-003)
- Fix no_service_token_policy: change decision to 'allow' (non_identity +
  email_domain is invalid in the API), use email_domain instead of email
- All TKT fixes are validated by both integration tests and the e2e subset
Five additional provider-side bugs discovered in the research team PR
(terraform-cfaccounts MR !7756) that cause UpgradeResourceState failures
during terraform plan.

All follow the same pattern: the v5 provider UpgradeState handler expects
a JSON object {} but the v4 state stored the field as a JSON array [] or
plain string.

Affected resources:
- TKT-007: zero_trust_access_policy.connection_rules ([] → {})
- TKT-008: cloudflare_zone.plan ('enterprise' string → {})
- TKT-009: cloudflare_logpush_job.output_options ([] → {})
- TKT-010: cloudflare_notification_policy.filters ([] → {})
- TKT-011: cloudflare_ruleset.action_parameters ([] → {})

These are provider bugs in cloudflare-terraform-next, not tf-migrate issues.
…pty ID bug

The v4 provider had a bug where cloudflare_leaked_credential_check_rule was
created successfully but the detection_id was not stored in state (id = '').

When migrating to v5:
1. The v5 provider's Read() detects the empty ID and removes the resource
   from state (our earlier fix to resource.go)
2. On the next terraform apply, Terraform tries to POST a new rule
3. If the rule was actually created by v4, the API returns error 11003:
   'custom detection for given username and password already exists'

The migrator now appends a MIGRATION WARNING comment to each
cloudflare_leaked_credential_check_rule resource block, guiding users to
find the existing detection_id via the API and import the resource before
running terraform apply.
…nal output

The MIGRATION WARNING comment was only written to the .tf file as a code
comment — it was invisible in the terminal output during migration.

Now also appends a hcl.Diagnostic warning so it appears in the migration
diagnostics summary alongside other actionable warnings like resource splits
and deprecated field removals.

The terminal output now shows:

  ⚠ Manual action required for cloudflare_leaked_credential_check_rule.<name>
  The v4 provider had a bug where the detection_id was not stored in state.
  Steps to fix:
    1. Find the existing detection_id: curl ...
    2. Import: terraform import cloudflare_leaked_credential_check_rule.<name> ...
…curl and import commands

When the zone_id attribute is a literal string, extract it and use it
directly in the curl and import commands so they are immediately runnable.

Before:
  curl -s "...zones/<zone_id>/..." ...
  terraform import ...lcc_rule_basic <zone_id>/<detection_id>

After (zone_id is a literal):
  curl -s "...zones/63a1772ec2bacdee371d3119f58c3300/..." ...
  terraform import ...lcc_rule_basic 63a1772ec2bacdee371d3119f58c3300/<detection_id>

Also improved the jq filter to show {id, username, password} so users
can identify which detection matches their config.
… a literal hex value

ExtractStringFromAttribute was returning 'var' when zone_id = var.zone_id,
producing a broken curl URL: zones/var/leaked-credential-checks/detections

Now only use the extracted value if it is exactly 32 characters (a real
Cloudflare zone ID). Variable references like var.zone_id, local.zone_id,
etc. fall back cleanly to the <zone_id> placeholder.
…lder IDs

Instead of only emitting a MIGRATION WARNING comment and terminal
diagnostic, the migrator now generates an actual import {} block in the
migrated file with placeholder IDs (<zone_id>/<detection_id>).

This is more actionable: the user just needs to:
  1. Find the detection_id via the curl command in the terminal output
  2. Replace <detection_id> in the import block

When zone_id is a literal 32-char hex string, it is embedded in the
import block ID automatically (only <detection_id> needs to be replaced).
When zone_id is a variable reference, both placeholders are present.

The fix to use RemoveOriginal: true is required because the pipeline
handler only processes extra blocks (blocksToAdd) when RemoveOriginal
is true - the original block is included in result.Blocks.

Also updates unit tests and integration test fixtures to reflect the
new import block generation.
…teps

Diagnostics
-----------
Introduce transform.DiagInfo (= hcl.DiagInvalid, the zero value) as a
named info severity level. Diagnostics at this level are suppressed by
default and only shown with --verbose.

Demote four 'Deprecated field(s) removed' diagnostics to DiagInfo since
the migrator already handled the change automatically -- no user action
required:
  - certificate_pack: wait_for_active_status removed
  - workers_script: tags field removed
  - page_rule: minify / disable_railgun removed
  - origin_ca_certificate: min_days_for_renewal removed

Keep actionable warnings at DiagWarning (resource splits, manual
imports, dynamic blocks requiring migration, etc.).

printDiagnostics now shows a hint in the summary line when info messages
are suppressed:
  Migration diagnostics: 1 warning(s), 3 info message(s) suppressed (use --verbose to see)

Provider version detection
--------------------------
Fix targetProviderVersion to include prereleases (prerelease: true).
Previously only Draft was checked, causing beta releases to be skipped
and falling through to the hardcoded fallback (5.19.0-beta.2).
Update fallback to 5.19.0-beta.3.

Next steps output
-----------------
- Add step 1 'Review warnings above' only when actionable warnings exist
- Show explicit git commands: git add . / git commit / git push
- Auto-number steps dynamically so numbering is always correct
When cloudflare_argo has both smart_routing and tiered_caching, the
v5 migration splits it into two resources:
  - cloudflare_argo_smart_routing (moved block, state carries over)
  - cloudflare_argo_tiered_caching (new, no state entry)

Previously the user was told to run 'terraform import' manually.
Now an import {} block is generated alongside the resource block, so
the user only needs to replace <zone_id> with the real value (or
nothing at all if zone_id is a literal 32-char hex string).

The diagnostic message is updated to reference the generated block
and explain what still needs to be done.
Add FilePath (full path) field to transform.Context alongside the
existing Filename (base name only) field. Populate it from the full
file path at both Context creation sites in main.go.

Use ctx.FilePath in the two diagnostics that generate import blocks
so users see exactly which file to edit:

  ⚠ Action required: fill in detection_id for ...lcc_rule_basic

  An import {} block has been generated in:
    /path/to/leaked_credential_check_rule.tf

  ⚠ Action required: import tiered caching resource for cloudflare_argo.xxx

  An import {} block has been added in:
    /path/to/argo.tf
targetProviderVersion now returns (version, fromAPI bool) so callers
can distinguish between a live GitHub API result and the hardcoded
fallback. updateProviderVersionConstraint logs at debug level:

  - 'Provider version fetched from GitHub API' when the API responded
  - 'GitHub API unavailable, using built-in fallback version' when
    rate-limited, offline, or the request otherwise failed

Use --log-level debug to see which path was taken.
…ailable

Remove the hardcoded fallback version map. When the GitHub releases API
is unreachable (rate-limited, offline, or returns non-200), skip updating
required_providers entirely and instead print an explicit warning with
instructions for the user to update the version manually:

  ⚠ Could not fetch the latest provider version from GitHub
    (network unavailable or rate-limited).
    The provider version in required_providers has NOT been updated.

  Next steps:
    1. Find the latest version: https://github.com/.../releases
    2. Update required_providers manually
    3. Regenerate the lock file
    4. Commit and push

A stale hardcoded version is worse than being explicit — it would silently
pin users to an outdated beta without any indication something went wrong.
If a cloudflare_tiered_cache block has already been migrated (has 'value'
but no 'cache_type'), return it unchanged. Previously re-running the
migrator would treat the already-migrated value attribute as if it were
a variable reference, generate a second cloudflare_tiered_cache block,
and (for generic/smart/off) also a second cloudflare_argo_tiered_caching
block — causing duplicate resource errors on terraform init.
…n all next-steps paths

--target-provider-version
  New migrate flag that accepts an explicit provider version string
  (e.g. 5.19.0-beta.3), bypassing the GitHub API lookup entirely.
  Useful in CI or air-gapped environments where the API is unreachable.

  Example:
    tf-migrate migrate --target-provider-version 5.19.0-beta.3

Review warnings step in version-fetch-failure path
  printVersionFetchFailure now receives hcl.Diagnostics and prepends
  'Review the warnings above...' as step 1 when there are actionable
  warnings — consistent with the normal success path.
@tamas-jozsa tamas-jozsa merged commit 7fcd2ba into main Mar 27, 2026
9 checks passed
@tamas-jozsa tamas-jozsa deleted the leaked-credential branch March 27, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants