Skip to content

refactor: replace string-matched errors with typed control-plane errors - #29

Merged
rawkode merged 2 commits into
developfrom
claude/typed-control-plane-errors
Sep 19, 2026
Merged

rawkode merged 2 commits into
developfrom
claude/typed-control-plane-errors

Conversation

@rawkode

@rawkode rawkode commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

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 sentinel

Repo::listen_key_for_tunnel bailed with the literal "KEY_NOT_FOUND" and resolve_listen_key in the binary recovered by e.to_string().contains(...). A missing key is an expected state, so the method now returns Result<Option<SecretKey>> and the caller matches on None.

Breaking change to the connect-lib public API. Repo is 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_tunnel directly and would print the raw KEY_NOT_FOUND sentinel if the key was missing. It now goes through resolve_listen_key and gets the same regenerate-and-rewire handling as --id.

Control-plane failures: ControlPlaneError enum

project_lookup_error in 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 time find_connector had the typed kube::Error in hand, matched on e.code, and then baked datumctl ctx switch guidance into a string inside the library.

New in kube_error.rs:

  • ControlPlaneError { ProjectNotFound, PermissionDenied, Unauthorized } with thiserror. Each variant keeps the original kube::Error as #[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 a list call to the enum and wraps everything else with the given context. A 404 on a list is 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_ref only 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 the TunnelService API is unchanged. Every list that propagates out of TunnelService goes 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:

  • Guidance now applies to every control-plane failure that reaches main, not only the --endpoint lookup. Previously a 404 from find_connector fell through to a bare "Failed to list connectors".
  • The datumctl command text no longer lives in the embeddable library.
  • The underlying error is printed on its own line after the guidance instead of inlined.

Not in scope

is_dns_error in progress.rs also matches on message text, but reqwest::Error exposes no DNS classification and the source chain bottoms out in an uncategorised io::Error. Fixing it properly means resolving with hickory first and handing reqwest the IPs, which changes behaviour. Left for a separate PR.

Verification

Check Result
cargo clippy --workspace --all-targets same nine pre-existing errors as develop, no new findings (PR #27 fixes those)
cargo test --workspace 77 passed, 0 failed, including 6 new tests
rustfmt --check on touched lib files clean (main.rs kept to my hunks since develop is not fmt-clean)
grep KEY_NOT_FOUND|datumctl ctx switch connect-lib/lib no matches
grep 'std_context("Failed to list' connect-lib/lib no matches

@rawkode
rawkode marked this pull request as ready for review September 18, 2026 22:17
Copilot AI lite review requested due to automatic review settings September 18, 2026 22:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Three moderate findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 Medium severity

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.

Comment thread connect-lib/lib/src/kube_error.rs Outdated
Comment thread connect-lib/lib/src/repo.rs
Comment thread connect-lib/lib/src/tunnels.rs
@rawkode

rawkode commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts on this branch.

Copilot AI commented Sep 19, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts on this branch.

Resolved and pushed. I merged origin/develop into this branch, resolved the conflicts, and finalized it in f87716a (with a small follow-up synchronization fix in 151cb50).

@cla-assistant

cla-assistant Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cla-assistant

cla-assistant Bot commented Sep 19, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ rawkode
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

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.
@rawkode
rawkode force-pushed the claude/typed-control-plane-errors branch from 151cb50 to 06337b9 Compare September 19, 2026 15:57
@rawkode
rawkode merged commit 8810766 into develop Sep 19, 2026
3 checks passed
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.

3 participants