refactor: replace string-matched errors with typed control-plane errors - #29
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Three moderate findings remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 3
Open (3)
What changed in this PR
This PR replaces string-matched errors with typed control-plane errors and explicit missing-key handling.
Changes:
- Uses
Option<SecretKey>for missing tunnel keys. - Adds typed error classification and source-chain lookup.
- Moves control-plane guidance into the binary.
| File | Summary |
|---|---|
connect-lib/lib/src/tunnels.rs |
Applies typed classification to tunnel list failures. Moderate (2 votes): classify subsequent list operations as well. |
connect-lib/lib/src/repo.rs |
Implements optional listen-key lookup. Moderate (2 votes): address the public API compatibility decision. |
connect-lib/lib/src/lib.rs |
Re-exports ControlPlaneError. |
connect-lib/lib/src/kube_error.rs |
Defines error classification and chain lookup. Moderate (3 votes): preserve original API errors for diagnostics. |
connect-lib/bin/src/main.rs |
Handles key recovery and user-facing guidance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@copilot resolve the merge conflicts on this branch. |
Resolved and pushed. I merged |
|
|
Two places relied on matching error message text to decide what to do, which breaks silently the moment the wording changes. Repo::listen_key_for_tunnel bailed with the literal "KEY_NOT_FOUND" and the binary recovered by grepping the Display output. A missing key is an expected state, not an error, so it now returns Ok(None) and the caller matches on that. The interactive picker path gets the same missing-key handling as --id instead of surfacing the raw sentinel. The binary classified project lookup failures by searching for "not found" or "404" in an error that had already been wrapped twice, so any 404 matched, not only a missing control plane. Meanwhile find_connector had the typed kube::Error in hand, matched on the status code, and then baked datumctl guidance into a string inside the library. Both now go through a ControlPlaneError enum (ProjectNotFound, PermissionDenied, Unauthorized) classified from the HTTP status of the list call where the error is still typed. It travels inside the existing AnyError so the TunnelService API is unchanged; ControlPlaneError::find_in walks the source chain because AnyError::downcast_ref only inspects the outermost layer and every context call adds one. The binary recovers the variant in main() and attaches the user-facing guidance there, which also removes the datumctl command text from the embeddable library. The guidance now applies to every control-plane failure that reaches main rather than only the --endpoint lookup. This commit was created with the assistance of a LLM.
Review follow-ups on the typed control-plane errors. classify_list_error discarded the original kube::Error for 401/403/404 and kept only the synthetic ControlPlaneError, so the binary's "Underlying error" line lost the API status, reason and message. Each variant now carries the kube::Error as its #[source]; the alternate Display walks the chain so the detail is printed again. The enum drops Clone and PartialEq because kube::Error has neither; tests match on shape instead. A project_id() accessor replaces field access for callers that do not care which variant they hold. Only the first list in list_project_with_orphans was classified. If the HTTPProxy list succeeded but a later ConnectorAdvertisement or Connector list failed with a project-level status, for example a token expiring between calls, the error fell through as plain context and main could not attach guidance. Every list that propagates out of TunnelService now goes through the classifier. This commit was created with the assistance of a LLM.
151cb50 to
06337b9
Compare

Summary
Two places decided what to do by matching error message text. Both break silently the moment the wording changes.
Missing tunnel key:
Option, not a sentinelRepo::listen_key_for_tunnelbailed with the literal"KEY_NOT_FOUND"andresolve_listen_keyin the binary recovered bye.to_string().contains(...). A missing key is an expected state, so the method now returnsResult<Option<SecretKey>>and the caller matches onNone.Breaking change to the
connect-libpublic API.Repois re-exported and the signature changes. The crate is 0.1.0 and unpublished, and the only caller is the binary in this repo. A parallel optional-returning method was rejected because it would leave the sentinel in place.Behaviour change: the interactive picker path previously called
listen_key_for_tunneldirectly and would print the rawKEY_NOT_FOUNDsentinel if the key was missing. It now goes throughresolve_listen_keyand gets the same regenerate-and-rewire handling as--id.Control-plane failures:
ControlPlaneErrorenumproject_lookup_errorin the binary searched for"not found","NotFound"and"404"in an error that had already been wrapped twice, so any 404 matched, not only a missing control plane. At the same timefind_connectorhad the typedkube::Errorin hand, matched one.code, and then bakeddatumctl ctx switchguidance into a string inside the library.New in
kube_error.rs:ControlPlaneError { ProjectNotFound, PermissionDenied, Unauthorized }withthiserror. Each variant keeps the originalkube::Erroras#[source], so the API status, reason and message still reach the binary's "Underlying error" line.classify_list_error(project_id, context, kube::Error)maps 404/403/401 from alistcall to the enum and wraps everything else with the given context. A 404 on alistis precise: an empty namespace returns 200, so it can only mean the control-plane path is missing.ControlPlaneError::find_in(&AnyError)walks the source chain.AnyError::downcast_refonly inspects the outermost error and every.context(..)adds a layer, so a plain downcast at the top level misses it. There is a test for this.The error travels inside the existing
n0_error::AnyError, so theTunnelServiceAPI is unchanged. Everylistthat propagates out ofTunnelServicegoes through the classifier, so a project-level failure on any call in a sequence, not only the first, gets guidance.The binary recovers the variant in
main()and attaches the guidance there. Behaviour changes:main, not only the--endpointlookup. Previously a 404 fromfind_connectorfell through to a bare "Failed to list connectors".datumctlcommand text no longer lives in the embeddable library.Not in scope
is_dns_errorinprogress.rsalso matches on message text, butreqwest::Errorexposes no DNS classification and the source chain bottoms out in an uncategorisedio::Error. Fixing it properly means resolving with hickory first and handing reqwest the IPs, which changes behaviour. Left for a separate PR.Verification
cargo clippy --workspace --all-targetsdevelop, no new findings (PR #27 fixes those)cargo test --workspacerustfmt --checkon touched lib filesmain.rskept to my hunks sincedevelopis not fmt-clean)grep KEY_NOT_FOUND|datumctl ctx switch connect-lib/libgrep 'std_context("Failed to list' connect-lib/lib