Skip to content

fix(certificate): give owner_role_name a declarative clear path - #184

Open
spbsoluble wants to merge 2 commits into
fix/ca-schedule-daily-variantfrom
fix/certificate-owner-role-clear
Open

fix(certificate): give owner_role_name a declarative clear path#184
spbsoluble wants to merge 2 commits into
fix/ca-schedule-daily-variantfrom
fix/certificate-owner-role-clear

Conversation

@spbsoluble

Copy link
Copy Markdown
Collaborator

Summary

Third stacked PR (#173#182 ← this one) for the unset+drift attribute contract work. Gives keyfactor_certificate's owner_role_name a declarative clear path (G1).

Contract: omitting owner_role_name from 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}/Owner with an empty JSON object {} (both NewRoleId and NewRoleName omitted) clears certificate ownership -- HTTP 204, subsequent GET shows OwnerRoleId/OwnerRoleName null. 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's encoding/json omitempty (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 already omitempty pointers:

type OwnerRequest struct {
	NewRoleId   *int    `json:"NewRoleId,omitempty"`
	NewRoleName *string `json:"NewRoleName,omitempty"`
}

So &api.OwnerRequest{} (both fields left nil) serializes to exactly {} -- the provider only needed to stop constructing a non-nil NewRoleName pointer for the empty-string case.

Changes

  • ownerChangeRequestForPlan (resource_keyfactor_certificate.go): builds the clear payload ({}) for owner_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.
  • Schema description + regenerated docs document the omit/clear contract.

Note on UseStateForUnknown (owner_role_name has no sibling attribute)

owner_role_name keeps its existing UseStateForUnknown plan modifier -- unchanged. Removing owner_role_name from 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, because certificateOwnerRoleChanged only fires when plan and state values actually differ, and a resurrected value trivially matches state.

What didn't need to change

  • certificateOwnerRoleChanged already refuses to fire when the plan value is Null/Unknown (undeclared), and already fires correctly for an explicit plan="" vs state="Administrator" diff (values differ) or a no-op when plan="" matches an already-empty state.
  • Update's post-apply state already echoes plan.OwnerRoleName verbatim via knownStringFromPlan, 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 through api.Client.ChangeCertificateOwnerRole against an httptest server; asserts the captured request body is exactly {}. Red before the fix: captured body was {"NewRoleName":""}.
  • TestUnitCertificateOwnerRoleClearNoopWhenAlreadyEmpty -- state owner null, plan ""certificateOwnerRoleChanged returns false, no API call.
  • TestUnitCertificateReadKeepsOwnerClearSentinel -- server reports no owner, prior state "" → state stays "". Red before the fix (keepStringSentinel unconditionally 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 -w
  • GOWORK=off go build ./...
  • make testunit -- all green
  • make tfdocs -- regenerated docs/resources/certificate.md
  • Lab lifecycle validation (plan/apply/import/drift/destroy) against a live Command instance

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.
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.

1 participant