Skip to content

okta_push_group: failed delete after successful deactivation leaves state showing ACTIVE while group push has stopped #2903

Description

@exitcode0

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request.
  • Please do not leave +1 or me too comments, they generate extra noise for issue followers and do not help prioritize the request.
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment.

Terraform Version & Okta Provider Version(s)

Terraform v1.12.1
on darwin_arm64

  • provider registry.terraform.io/okta/okta v6.9.0

The Delete implementation is byte-for-byte identical from v6.6.0 through v6.13.0 and current master, so the defect is present on all of those versions (verified by diffing the function across tags).

Affected Resource(s)

  • okta_push_group

Can this be done in the Admin UI?

Yes

Can this be done in the actual API call?

Yes (deactivate and delete both work individually; the bug is in how the provider handles a failure between them)

Customer Information

Organization Name: (redacted — large paid enterprise org, happy to share privately)
Paid Customer: yes

Terraform Configuration

resource "okta_push_group" "example" {
  app_id                         = "0oaXXXXXXXXXXXXXXXXX" # app ID
  source_group_id                = "00gXXXXXXXXXXXXXXXXX" # source group ID
  status                         = "ACTIVE"
  delete_target_group_on_destroy = false
}

Debug Output

Debug logs are not available from the CI run where this occurred. The failure is fully explained by code inspection (see Actual Behavior); the destroy failed with the provider error:

Error: failed to delete push group mapping:

  with okta_push_group.example,
  on ... line ..., in resource "okta_push_group" "example":

... giving up after N attempt(s)

Expected Behavior

Delete performs two API calls: it deactivates the mapping, then deletes it. If the deactivation succeeds and the delete then fails, the provider should persist the fact that the mapping is now INACTIVE to state before returning the error, so that state reflects reality.

Per the plugin framework resource delete documentation, state set in the Delete response is preserved when error diagnostics are returned — the framework only removes the resource from state when Delete reports no errors. That mechanism exists precisely for partially-completed deletes like this one.

At an absolute minimum, the error message should tell the operator that the mapping was already deactivated and has stopped pushing group membership downstream, rather than the current bare "failed to delete push group mapping".

Actual Behavior

Delete in okta/services/idaas/resource_okta_push_group.go (lines 280–307 on master; identical code at lines 271–298 in v6.9.0) is a two-phase sequence with no intermediate state write:

	if state.Status.ValueString() != "INACTIVE" {
		_, _, err := r.config.OktaIDaaSClient.OktaSDKClientV6().GroupPushMappingAPI.UpdateGroupPushMapping(ctx, state.AppId.ValueString(), state.ID.ValueString()).Body(v6okta.UpdateGroupPushMappingRequest{
			Status: "INACTIVE",
		}).Execute()
		if err != nil {
			resp.Diagnostics.AddError("failed to delete push group mapping: ", err.Error())
			return
		}
	}

	_, err := r.config.OktaIDaaSClient.OktaSDKClientV6().GroupPushMappingAPI.DeleteGroupPushMapping(ctx, state.AppId.ValueString(), state.ID.ValueString()).DeleteTargetGroup(state.DeleteTargetGroupOnDestroy.ValueBool()).Execute()
	if err != nil {
		resp.Diagnostics.AddError("failed to delete push group mapping: ", err.Error())
		return
	}
}
  1. Lines 292–300 (master): if the mapping is not already INACTIVE, UpdateGroupPushMapping is called with Status: "INACTIVE". The Okta API requires a mapping to be deactivated before it can be deleted — the provider's own guard message at line 288 says as much ("To delete a group push mapping, the status must be INACTIVE").
  2. Lines 302–306: DeleteGroupPushMapping.

If step 1 succeeds and step 2 fails — for example the provider's HTTP transport exhausts its 429 retries in a rate-limited org (giving up after N attempt(s)), or the API returns a 5xx — Delete returns an error and no state is written. The framework preserves the prior state, which still records status = "ACTIVE".

The divergence is a silent functional change, not just a cosmetic state mismatch:

  • Reality in Okta: the mapping is INACTIVE. It has stopped pushing group membership to the downstream application, so membership changes in the Okta source group no longer propagate.
  • Terraform state and the next plan: status = "ACTIVE".
  • What the operator sees: failed to delete push group mapping: ..., with no indication that the mapping was already deactivated and is no longer syncing members downstream.

It does self-heal on the next run: the next refresh reads status = "INACTIVE", and Delete then skips step 1 and retries the DELETE. But between the failed apply and the next apply, downstream group membership has silently stopped syncing while state claims the mapping is ACTIVE. In a scheduled or CI-driven pipeline that gap can be hours or days, and nothing in the plan output flags it.

This is the same class of defect as #2899 / #2900 in okta_request_condition ("fix(request_condition): save state before post-create calls"), and the same principle applies: persist what the provider already knows before making a follow-up API call.

Suggested fix

After the deactivation at step 1 succeeds, write the observed status back to resp.State before attempting the DELETE:

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status"), "INACTIVE")...)

(or set the whole model via resp.State.Set with state.Status updated). The framework nuance is worth stating explicitly, because it is the opposite of the intuition for Create: in internal/fwserver/server_deleteresource.go the response state is seeded from prior state and RemoveResource is only called when deleteResp.Diagnostics.HasError() is false, while resp.NewState = &deleteResp.State is assigned unconditionally. So an attribute written in Delete survives on the error path and is discarded on the success path — exactly the behaviour needed here. path is already imported in this file, and Delete already reads prior state from resp.State at line 282, so the change is small.

Offered as a suggestion rather than a prescription — a maintainer may prefer to re-set the full model. Even if the state write is rejected, please consider making the error message distinguish "could not deactivate" from "deactivated but could not delete", so the operator knows group push has stopped.

Steps to reproduce

  1. Apply the configuration above so an ACTIVE push group mapping exists and is in state.
  2. Remove the resource from the configuration (or run terraform destroy -target=okta_push_group.example).
  3. Arrange for the DELETE to fail after the deactivation PATCH (PATCH /api/v1/apps/{appId}/group-push/mappings/{mappingId} with {"status":"INACTIVE"}) succeeds. Deterministic repro: intercept DELETE /api/v1/apps/{appId}/group-push/mappings/{mappingId} with a proxy and return HTTP 429 (or any 5xx) repeatedly until the transport gives up. In the real world this happens organically in orgs under API rate-limit pressure.
  4. terraform apply / terraform destroy fails with failed to delete push group mapping.
  5. Inspect the mapping in Okta (GET /api/v1/apps/{appId}/group-push/mappings/{mappingId}) — status is INACTIVE, and membership is no longer being pushed to the downstream app.
  6. Inspect terraform state show okta_push_group.examplestatus = "ACTIVE". terraform plan also shows nothing about the status change; the only signal is the destroy error from the previous run.

Important Factoids

  • Nothing atypical about authentication: standard service-account (API token) auth.
  • The same shape exists in Update (single call, so no window) and in Create (a single POST, so no window) — this report is specific to Delete, which is the only method in this resource making two mutating calls.
  • The window is not hypothetical for large orgs: 429-driven exhaustion of the transport's retry budget is the common trigger, and it hits the second call more often than the first because the first call has already consumed part of the rate-limit budget for that endpoint.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions