Skip to content

apigee: fix permadiff on google_apigee_developer_app.key_expires_in#59

Open
jbbqqf wants to merge 11 commits into
mainfrom
feat/27071-apigee-developer-app-key-expires-permadiff
Open

apigee: fix permadiff on google_apigee_developer_app.key_expires_in#59
jbbqqf wants to merge 11 commits into
mainfrom
feat/27071-apigee-developer-app-key-expires-permadiff

Conversation

@jbbqqf
Copy link
Copy Markdown
Owner

@jbbqqf jbbqqf commented May 9, 2026

Summary

Stops google_apigee_developer_app from force-replacing on every apply
when key_expires_in is set. The Apigee API takes a duration on create
but never echoes it back — it returns an absolute expiration timestamp on
the credential. Marking keyExpiresIn with ignore_read: true preserves
the configured duration in state.

Fixes hashicorp/terraform-provider-google#27071 — see hashicorp/terraform-provider-google#27071

Why

Reproducer (from the issue, confirmed by maintainer @ggtisc):

resource "google_apigee_developer_app" "this" {
  name            = "..."
  org_id          = google_apigee_organization.org.id
  developer_email = google_apigee_developer.dev.email
  callback_url    = "..."
  key_expires_in  = 1807199238981   # ~21 days in ms
  api_products    = [google_apigee_api_product.p.name]
}

After apply, the next plan shows:

~ key_expires_in = "3584153354952" -> "1807199238981" # forces replacement

That 3584153354952 is a unix-millisecond expiration timestamp
(credentials[].expiresAt), not the user's input duration. The provider
flattens it into the key_expires_in state field, which permanently
diverges from the configured duration. The behavior is determined by the
Apigee API contract — see DeveloperApp / DeveloperAppCredential schemas:
Apigee REST reference.

What changed

mmv1 schema only — adds ignore_read: true on the keyExpiresIn field in
mmv1/products/apigee/DeveloperApp.yaml. The field is already immutable,
so a real config change still force-replaces; the only behavior change is
"don't overwrite the user's input with a value the API never returned".

 mmv1/products/apigee/DeveloperApp.yaml | 6 ++++++
 1 file changed, 6 insertions(+)

Edge cases tested

# Scenario HCL excerpt Expected Verified by
1 Field unset (default -1) # key_expires_in omitted API returns expiresAt: -1 on credential, but default_value: "-1" in YAML keeps state stable; with ignore_read no diff is observable on second plan static review of mmv1 codegen
2 Set to a finite duration key_expires_in = 1807199238981 apply ok; second plan: no diff. Without this fix: force-replace. the issue's reproducer
3 Edge: re-apply across multiple plan cycles (any value above) Repeated terraform plan shows no diff for as long as the resource exists inherent to ignore_read semantics

Test protocol

Test Result Notes
YAML schema review OK ignore_read: true is the standard mmv1 mechanism for "API never echoes this back"; precedent: datafusion/Instance.yaml, storagecontrol/*IntelligenceConfig.yaml
API behavior review OK Confirmed against Apigee Discovery: GET .../apps/{name} returns credentials[] with expiresAt (timestamp), no keyExpiresIn in response
Live BEFORE/AFTER smoke not run Apigee org provisioning takes 30+ minutes per phase; live smoke deferred. The bug is reproducible by the maintainer in the issue thread; the fix is a pure-codegen flag with established precedent.

A reviewer with an Apigee org already provisioned can validate by applying
the issue's HCL twice on the BEFORE branch (force-replace observed) and on
this branch (clean second plan).

Resources

Disclosure

This PR was drafted with assistance from Claude Code as part of a parallel
contribution batch on the magic-modules repository. The schema change was
reviewed manually against the Apigee Discovery doc and against existing
ignore_read usage in the codebase. The author (a human) will review the
diff and the modular-magician downstream PRs before requesting maintainer
review. Live BEFORE/AFTER smoke was not run because Apigee organisation
provisioning is too slow for a parallel batch (~30 min per phase); the
maintainer's reproducer in the issue thread already establishes the BEFORE
behavior.

jcromanu and others added 11 commits May 8, 2026 16:43
…#27071)

The Apigee API accepts `keyExpiresIn` as a duration in milliseconds at
create time, but the GET payload does not echo it back — instead it returns
the absolute expiration timestamp on the credential
(`credentials[].expiresAt`). Today the read step flattens this timestamp
into the `key_expires_in` state field, so on the next plan the configured
duration (e.g. `1807199238981` ms ≈ 21 days) diffs against a millisecond
unix timestamp (e.g. `3584153354952`) and forces resource replacement.

Marking the field with `ignore_read: true` keeps the user's configured
duration in state and stops the spurious replacement. The field remains
`immutable` so any genuine config change still triggers a replace as
documented.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

google_apigee_developer_app: Resource is replaced on every apply due to key_expires_in

8 participants