fix(github): name the operator's apply id in relayed control rejections - #905
Conversation
A remote data-plane deployment knows an apply only by its own id, so its control rejections (volume, stop, cancel, start, revert, skip-revert) name an identifier that resolves to nothing on the control plane — the PR reply reads like an answer about a different schema change and leaks an internal identifier into public markdown. The API layer now rewrites the remote id (the apply's stored external_id) to the operator-facing apply identifier at every point where a data-plane rejection message is relayed to callers, stored on a failed control request, or written to the apply history. The data plane is still addressed by its own id, and the raw message stays triageable from the server logs and the apply row's external_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves control-command rejection messaging in hybrid/remote deployments by rewriting data-plane apply IDs (external_id) in rejection text to the operator-facing apply identifier, preventing confusing responses and avoiding leaking internal identifiers into PR-facing output.
Changes:
- Add
operatorFacingControlErrorto rewrite remote apply IDs in control-operation rejection messages. - Apply the rewrite across stop/cancel/start/volume responses, immediate-stop apply-log messages, and revert/skip-revert failure persistence.
- Add a volume-handler test asserting the operator-facing ID is returned while the data plane is still addressed by its own ID.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/api/control_handlers.go | Introduces and applies operatorFacingControlError so relayed control rejections reference the operator-facing apply identifier. |
| pkg/api/handlers_test.go | Adds an HTTP-level test ensuring volume rejections rewrite the remote apply ID in the response message. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
🤖 Review findings - created by Kiran's code review agent - for pull/905, 0c1264b. Verdict: 3 findings — the rewrite itself is mechanically safe and at the right altitude, but the driver's retry path still stores the same failure records raw, and the invariant the PR claims doesn't yet hold for Findings
The one thing that could have broken, verifiedThe blind Notes (not blocking)
Verified correct
This review was generated by Claude Code (claude-fable-5). |
A control rejection reaches the durable record by two paths: the API's immediate attempt, and the driver's retry after that attempt never landed. Only the first translated the data plane's apply id, so the same rejection was stored with a different name for the schema change depending on which path happened to reach the data plane. Move the rewrite onto the apply row as OperatorFacingMessage so both writers share it, and have failPendingControlRequests translate before it persists. The helper now also takes the remote identifiers a caller addressed, which is what a multi-operation apply needs: its parent deliberately carries no external_id, so the remote id only exists on the claimed operation. The rejection paths that had no server-side record of the raw text now log it before rewriting, so the data plane's own identifier stays greppable during triage. The immediate-stop apply log carries the outcome instead of the raw transport error, which is already logged with its identifiers. Cover the sites that had no test: the helper's own guards, both failure records, the immediate-stop apply log, and the driver retry for single- and multi-operation applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion-apply-id # Conflicts: # pkg/api/control_handlers.go # pkg/tern/grpc_client.go
The driver's revert and skip-revert rejection handlers stored the operator-facing rewrite without recording the data plane's raw message anywhere server-side, leaving those two paths without the greppable raw text every other rewriting path keeps. Log it before the rewrite, and scope the helper's triage note to what the paths actually guarantee. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Follow-up on these findings, posted on Armand's behalf. All three are addressed at the current head:
Your non-blocking notes also landed: the raw-text logging gap you flagged is fully closed as of 06641c7 (the API paths got their warn logs earlier; that commit adds them to the driver's revert/skip-revert rejection paths, and scopes the helper's triage note to what the paths actually guarantee), the stop/cancel/start relays are documented in the body as uniformity rather than live fixes, and the substring-safety invariant is recorded in the helper's godoc for whoever adds an engine with differently-shaped identifiers. This reply was posted by Claude Code (claude-fable-5). |
Why this matters
An apply that runs on a remote data-plane deployment exists as two records — one on each plane, each named by the side that created it. The operator addresses the control plane's record by its apply identifier; the data plane's record carries its own identifier, which the control plane stores in
external_id. So when the data plane rejects a control command (volume, stop, cancel, start, revert, skip-revert), its rejection copy naturally names its own record — an identifier the operator has never seen.The result is confusing at the worst moment. The operator just asked to change
apply-vol123, and SchemaBot replies about someapply-remote999— an id that resolves to nothing on the control plane (runningprogresson it returns nothing). It reads like SchemaBot answered about a different schema change, and a data-plane identifier leaks into public PR markdown.What it does
Adds
Apply.OperatorFacingMessage: before a data-plane rejection is relayed to the caller, stored on a failed control request, or written to the apply history, every remote identifier in it is rewritten to the operator-facing apply identifier.A rejection can reach the durable record by either of two paths, and both now translate:
The helper lives on the apply row so the API relay and the driver's retry share it — a rejection must not name the schema change differently depending on which path happened to reach the data plane. It also takes the claimed operation's remote id, which is the only remote identifier a multi-operation apply has (its parent deliberately carries none).
Alongside the translation, the rejection paths that had no server-side record of the raw text now log it before rewriting, so the data plane's own identifier stays greppable during triage. The immediate-stop apply log carries the outcome instead of the raw transport error, per the repo's rule against rendering driver text on operator-facing surfaces.
Notes on scope:
apply.ErrorMessageby the driver and rendered in the PR comment error block. That text names the remote id and raw gRPC error detail, so it needs sanitizing rather than translating — separate follow-up work, along with the sibling paths that posterr.Error()verbatim.🤖 Generated with Claude Code