Skip to content

feat: portable resource-deleter idempotency contract (already-gone = success)#1033

Open
c1-squire-dev[bot] wants to merge 1 commit into
mainfrom
paul.querna/nhi/resource-delete-idempotency
Open

feat: portable resource-deleter idempotency contract (already-gone = success)#1033
c1-squire-dev[bot] wants to merge 1 commit into
mainfrom
paul.querna/nhi/resource-delete-idempotency

Conversation

@c1-squire-dev

@c1-squire-dev c1-squire-dev Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Formalizes a small, portable contract that makes provider resource deletion safely retryable
after a crash
, by wiring the already-declared c1.connector.v2.ResourceDoesNotExist annotation as
a typed "already-gone = success" outcome — the resource-plane analog of GrantAlreadyRevoked.

This is the SDK half of the durable-credential revocation lifecycle: when a delete's response is lost
(crash / transport failure after the provider mutation), a blind retry is only safe if the deleter
contract makes already-absent a typed success instead of an ambiguous NotFound error.

No .proto change. ResourceDoesNotExist {} already exists in
proto/c1/connector/v2/annotation_resource.proto but was never produced or consumed. This PR makes
it normative and gives connectors a helper to emit it. The change is purely additive and independent
of #941 (touches none of its files).

The contract

  • ResourceDeleter / ResourceDeleterV2 Delete MUST be idempotent.
  • When the provider authoritatively confirms the addressed resource is absent, Delete MUST
    return nil error + a ResourceDoesNotExist annotation (success-equivalent).
  • Delete MUST NOT emit ResourceDoesNotExist when the not-found could instead mean a wrong/malformed
    resourceId, parentResourceID, or tenant — those return an error, so callers distinguish a real
    deletion from a mis-addressed one.
  • The SDK plumbs the connector-supplied annotation unchanged; it does not sniff gRPC NotFound and
    convert it (consistent with how GrantAlreadyRevoked works).
  • Connectors participating in this contract should implement ResourceDeleterV2: the V1→V2 shim drops
    parentResourceID, which is often required to address the resource for absence confirmation.

Why not blanket SDK NotFound→success

Empirically, connector Delete 404-handling is inconsistent (baton-aws IAM/SSO and baton-github
invitation normalize to success; baton-okta group, baton-github/gitlab user do not). Only the connector
knows whether a provider 404 addresses the intended resource, so central NotFound-swallowing would
hide mis-addressing bugs and silently change existing behavior. The typed, connector-emitted annotation
lets C1 distinguish confirmed already-gone (⇒ revoked) from ambiguous NotFound (⇒ reconcile).

Changes

  • pkg/connectorbuilder/resource_manager.go — normative godoc on ResourceDeleterLimited.Delete and
    ResourceDeleterV2Limited.Delete.
  • pkg/types/resource/resource.goResourceDoesNotExistAnnotations() discoverability helper.
  • pkg/connectorbuilder/resource_manager_test.go — tests locking the annotation round-trip through
    both delete RPC handlers, a normal-delete negative test, an error-still-surfaces test, and a helper
    unit test.

Compatibility

  • No proto change; DeleteResource(V2)Response shape unchanged.
  • Existing connectors unaffected until they opt in by returning the annotation.
  • Non-breaking under buf.yaml (no message/field added or removed).

Validation

  • gofmt -l on changed files: clean.
  • go build ./...: ok.
  • go test ./pkg/connectorbuilder/... ./pkg/types/resource/... ./pkg/annotations/... -count=1: ok
    (new tests pass, no regressions).
  • golangci-lint run on changed packages: 0 issues.
  • buf: no .proto modified, so buf breaking is trivially clean.

Follow-ups (not in this PR)

  • C1 backend: honor ResourceDoesNotExist in delete-outcome/error classification (currently NotFound
    is terminal) so already-gone maps to a revoked/success outcome and opaque NotFound stays terminal.
  • Connectors: emit the annotation from their Delete on confirmed absence (start with those that
    already normalize 404).
  • Optional: a per-resource-type capability advertising idempotent-delete, so the platform can skip a
    retry for non-conforming deleters.

Draft: opened for design review of the SDK contract before connector/backend rollout.

Formalize an already-gone-is-success contract for ResourceDeleter/
ResourceDeleterV2.Delete, mirroring the existing GrantAlreadyRevoked idiom
used by Revoke. When a connector's delete (or get) authoritatively confirms
the addressed resource is already absent at the provider, Delete must return
a nil error with a ResourceDoesNotExist annotation instead of an error, so a
delete retried after a crash or lost response is safe to treat as success.
This is normative for connector authors; baton-sdk is the contract layer
only (proto message and generated pb already existed and are unchanged) —
the C1 backend is the consumer.

Adds ResourceDoesNotExistAnnotations() in pkg/types/resource as a
discoverability helper so connectors don't hand-roll the annotation, and
tests locking the plumbing through both the DeleteResource (V1) and
DeleteResourceV2 RPC handlers: the annotation survives end-to-end, a normal
successful delete carries no such annotation, and a genuine delete error
still surfaces as an error.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

General PR Review: feat: portable resource-deleter idempotency contract (already-gone = success)

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base dd39a54a9c95.
Review mode: full
View review run

Review Summary

The full PR diff was scanned for security and correctness. This change is purely additive: godoc documenting the idempotency / ResourceDoesNotExist contract on ResourceDeleterLimited.Delete and ResourceDeleterV2Limited.Delete, a new ResourceDoesNotExistAnnotations() discoverability helper in pkg/types/resource, and tests. No interface signatures, proto/wire types, serialized-state formats, defaults, or dependencies changed, so there is no compatibility break. Verified the DeleteResource/DeleteResourceV2 RPC handlers propagate the returned annotations end-to-end and that the V1→V2 shim drops parentResourceID exactly as the new godoc warns. No new issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

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