fix(certificate): give owner_role_name a declarative clear path - #184
Open
spbsoluble wants to merge 2 commits into
Open
fix(certificate): give owner_role_name a declarative clear path#184spbsoluble wants to merge 2 commits into
spbsoluble wants to merge 2 commits into
Conversation
spbsoluble
force-pushed
the
fix/ca-schedule-daily-variant
branch
from
July 23, 2026 16:57
73cd698 to
a6c41ab
Compare
owner_role_name only ever supported setting a new owner: an explicit
owner_role_name = "" fell into the same code path as a real role name,
sending {"NewRoleName":""} to PUT /Certificates/{id}/Owner. Per the
verified live Command behavior (Swagger: "If removing the owner, leave
both empty"; confirmed against a v25.5 lab -- HTTP 204, subsequent GET
shows OwnerRoleId/OwnerRoleName null), the correct clear payload omits
both NewRoleId and NewRoleName entirely.
- ownerChangeRequestForPlan builds {} (both fields nil) for an explicit
"" plan value, relying on api.OwnerRequest's existing `omitempty`
pointer fields -- no go-client change needed.
- keepStringSentinel (attribute_contract.go) gives owner_role_name
sentinel stability on Read: when the server reports no owner and
prior state held the "" clear sentinel, state keeps "" instead of
collapsing to null, so config-vs-state stays settled instead of
producing a permanent "" -> null -> "" diff. A real owner appearing
out-of-band still surfaces as drift over a stale sentinel.
- Documented the omit/clear contract on the schema description and
regenerated docs.
certificateOwnerRoleChanged already refused to fire on an undeclared
(Null/Unknown) plan value, and Update's post-apply state already echoed
the plan value verbatim via knownStringFromPlan, so both continue to
behave correctly for the clear case with no change needed there.
Adds a v2.9.1 entry for owner_role_name = "" now sending Command's verified owner-clear payload (omit both NewRoleId/NewRoleName) instead of a literal empty-string role name; omitting the field still leaves ownership unmanaged.
spbsoluble
force-pushed
the
fix/certificate-owner-role-clear
branch
from
July 23, 2026 16:58
06de89d to
8d67bc0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third stacked PR (#173 ← #182 ← this one) for the unset+drift attribute contract work. Gives
keyfactor_certificate'sowner_role_namea declarative clear path (G1).Contract: omitting
owner_role_namefrom config leaves ownership unmanaged/preserved -- Terraform never sends a clearing value, and drift from an out-of-band owner change is still surfaced on plan/refresh. Declaring an explicit empty string (owner_role_name = "") is a declarative clear: Terraform sends a PUT with no role identifier, which Command interprets as removing the certificate's owner.Verified clear payload (live v25.5 lab)
PUT /Certificates/{id}/Ownerwith an empty JSON object{}(bothNewRoleIdandNewRoleNameomitted) clears certificate ownership -- HTTP 204, subsequent GET showsOwnerRoleId/OwnerRoleNamenull. Swagger doc: "If removing the owner, leave both empty."Before this fix, an explicit
owner_role_name = ""fell into the same branch as a real role name:strconv.Atoi("")fails, so it sent{"NewRoleName":""}-- a non-nil pointer to an empty string is not omitted by Go'sencoding/jsonomitempty(only a nil pointer is), so this was never the verified clear payload, just an untested accidental shape.What the go-client already supports
No go-client change was needed.
api.OwnerRequest's fields are alreadyomitemptypointers:So
&api.OwnerRequest{}(both fields left nil) serializes to exactly{}-- the provider only needed to stop constructing a non-nilNewRoleNamepointer for the empty-string case.Changes
ownerChangeRequestForPlan(resource_keyfactor_certificate.go): builds the clear payload ({}) forowner_role_name = "", otherwise tries numeric role ID then falls back to name, same as before.keepStringSentinel(attribute_contract.go): sentinel-stability helper for a single Optional+Computed string attribute. On Read, if the server reports no owner ("") AND prior state already held the""clear sentinel, state keeps""instead of collapsing to null -- otherwise the attribute would never settle (config still declares"", state flips back to null every refresh, producing a permanent"" -> null -> ""diff). If a real owner shows up out-of-band, that's surfaced as drift, never masked by a stale sentinel.Note on
UseStateForUnknown(owner_role_name has no sibling attribute)owner_role_namekeeps its existingUseStateForUnknownplan modifier -- unchanged. Removingowner_role_namefrom config entirely (config goes from declared""back to omitted) resurrects the last-applied state value into the plan, same as any other undeclared Optional+Computed attribute. Since this attribute has no paired sibling (unlike the CA schedule pairs in #182), that resurrection is the intended "omit = don't touch it" behavior, not a bug: nothing is sent to the API either way, becausecertificateOwnerRoleChangedonly fires when plan and state values actually differ, and a resurrected value trivially matches state.What didn't need to change
certificateOwnerRoleChangedalready refuses to fire when the plan value is Null/Unknown (undeclared), and already fires correctly for an explicitplan=""vsstate="Administrator"diff (values differ) or a no-op whenplan=""matches an already-emptystate.plan.OwnerRoleNameverbatim viaknownStringFromPlan, so the""sentinel is naturally preserved after Update/Create without change -- only the Read path (server-truth refresh) needed sentinel-stability logic.Tests (all reproduced red before the fix, green after)
TestUnitCertificateOwnerRoleClearSendsEmptyPayload-- drives the real HTTP call throughapi.Client.ChangeCertificateOwnerRoleagainst an httptest server; asserts the captured request body is exactly{}. Red before the fix: captured body was{"NewRoleName":""}.TestUnitCertificateOwnerRoleClearNoopWhenAlreadyEmpty-- state owner null, plan""→certificateOwnerRoleChangedreturns false, no API call.TestUnitCertificateReadKeepsOwnerClearSentinel-- server reports no owner, prior state""→ state stays"". Red before the fix (keepStringSentinelunconditionally collapsed to null).TestUnitCertificateReadSurfacesOwnerDriftOverSentinel-- server reports"SomeRole", prior state""→ state becomes"SomeRole"(drift surfaced, not masked).make testunit: all green, 0 failures, 3 skips (baseline), plus the 4 new tests above.Test plan
gofmt -wGOWORK=off go build ./...make testunit-- all greenmake tfdocs-- regenerateddocs/resources/certificate.md