Skip to content

Phase 2: kubeconfig discovery + parent release detection + SA token - #2

Merged
saadqbal merged 9 commits into
developfrom
feat/150-cluster-discovery
May 21, 2026
Merged

Phase 2: kubeconfig discovery + parent release detection + SA token#2
saadqbal merged 9 commits into
developfrom
feat/150-cluster-discovery

Conversation

@saadqbal

@saadqbalsaadqbal commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Phase 2 of the v0.1 roadmap (tracebloc/client#147, closes#150). Adds the plumbing the future tracebloc dataset push flow needs:

  1. Read the customer's kubeconfig
  2. Discover the tracebloc/client release running in the configured namespace
  3. Mint an ingestor SA token (via TokenRequest, or fall back to a static secret)

End-to-end validated against the dev EKS cluster — the new tracebloc cluster info command produces:

Kubeconfig:
context: arn:aws:eks:eu-central-1:.../tb-client-dev-templates
server: https://....amazonaws.com
namespace: tracebloc-templates
Parent release:
name: tracebloc
chart version: 1.3.5
app version: 1.3.5
jobs-manager: http://jobs-manager.tracebloc-templates.svc.cluster.local:8080
ingestor SA: tracebloc-templates/ingestor
ingestor img: sha256:463e236748708a5e3564569eec9173ea8cb3bcf515992d4939c5b610f3807a4a
Ingestor SA token:
source: TokenRequest
sha256[:8]: 13ce860c576ae04c
expires in: ~10m0s (server may cap shorter)
Ready for `tracebloc dataset push` (coming in Phase 3).

The discovered values match what we've been validating manually with kubectl all week. CLI is "ready for tracebloc dataset push" in the sense that the auth + URL plumbing now works end-to-end.

What lands

FileWhat
internal/cluster/kubeconfig.goLoad() reads kubeconfig honoring kubectl conventions; NewClientset() for downstream use
internal/cluster/discover.goDiscoverParentRelease() finds the parent client release by listing chart-managed Deployments + filtering by -jobs-manager name suffix
internal/cluster/token.goMintIngestorToken() — TokenRequest primary, static-secret fallback, hard-stop on non-recoverable errors
internal/cli/cluster.gotracebloc cluster info subcommand
internal/cluster/*_test.go11 test cases across the three files; 83.8% coverage on internal/cluster

Two bugs the real-cluster smoke caught

Worth calling out — both are textbook examples of unit tests not enough on their own:

  1. ClientConfigLoadingRules{ExplicitPath: ""} does NOT fall back to ~/.kube/config. The default-loading-rules chain only kicks in via NewDefaultClientConfigLoadingRules(). Unit test never exercised the "kubeconfig defaults" path because it used a stub. Fixed + commented.

  2. Selector app.kubernetes.io/name=jobs-manager matches nothing. The chart shares app.kubernetes.io/name=client (the chart name) across all its resources — that's the helm convention. To pick jobs-manager from its mysql / requests-proxy siblings, filter the result set by Deployment name suffix. Added a regression test that seeds all three sibling deployments and asserts only jobs-manager comes back.

Test plan

  • make ci green locally: vet, test -race, fmt-check, schema-check
  • Real-cluster smoke: tracebloc cluster info --context arn:aws:eks:...:cluster/tb-client-dev-templates --namespace tracebloc-templates returns the expected values, exit 0
  • Sibling-filter regression test seeds 3 chart deployments, asserts only jobs-manager is picked
  • TokenRequest test (using k8s fake clientset + a reactor) returns the stamped token
  • Static-secret fallback test seeds a service-account-token Secret + makes TokenRequest fail with Forbidden, asserts fallback path
  • Non-recoverable error (simulated network failure) propagates verbatim instead of falling back
  • Real-cluster smoke against the static-secret fallback path — not exercised today (the dev cluster grants TokenRequest); will be exercised when a customer hits an older cluster

Library footprint

Brings in k8s.io/client-go + apimachinery + api (@v0.31.0). Cross-compiled binaries grow from ~10MB to ~30MB. Cost is acceptable for the customer-experience upside of "your kubeconfig is all you need."

Closes

tracebloc/client#150

🤖 Generated with Claude Code


Note

Medium Risk
Adds Kubernetes client-go based cluster discovery and ServiceAccount token minting logic plus a new CLI surface area, which can affect auth/RBAC handling and increases dependency footprint. CI linting is also reworked (dropping golangci-lint), so coverage of checks changes and may miss prior lints.

Overview
Introduces a new tracebloc cluster info command that loads kubeconfig (kubectl-compatible defaults/overrides), discovers the running tracebloc/client parent release by inspecting Helm-managed *-jobs-manager Deployments, and mints an ingestor ServiceAccount token via TokenRequest with static-secret fallback (printing only a short SHA256 fingerprint).

Updates build tooling by replacing the golangci-lint GitHub Action with standalone errcheck/gofmt -s/ineffassign/misspell steps, bumps the repo’s minimum Go version to 1.26.0, and adds k8s.io/* dependencies plus focused unit tests for discovery, kubeconfig path expansion, and token minting behavior.

Reviewed by Cursor Bugbot for commit f43d116. Bugbot is set up for automated code reviews on this repo. Configure here.

Phase 2 of the v0.1 roadmap. Adds the plumbing the future `tracebloc
dataset push` flow needs: where does the customer's kubeconfig point,
which tracebloc release lives there, and how do we authenticate to
its jobs-manager.
End-to-end validated against the dev EKS cluster
(tb-client-dev-templates): discovers chart 1.3.5, resolves
jobs-manager.tracebloc-templates.svc:8080, reads INGESTOR_IMAGE_DIGEST
out of the deployment env, mints a 10-minute SA token via
TokenRequest.
What lands:
- internal/cluster/kubeconfig.go — Load() that honors --kubeconfig,
$KUBECONFIG, ~/.kube/config (via clientcmd's full default loading
rules — *not* an empty ExplicitPath, which silently refuses to
fall back to defaults; that was the first bug the real-cluster
smoke caught).
- internal/cluster/discover.go — DiscoverParentRelease() finds the
tracebloc/client release in a namespace by listing chart-managed
Deployments and filtering by name suffix (-jobs-manager). The
chart shares app.kubernetes.io/name=client across mysql/jobs-
manager/requests-proxy, so suffix matching is what distinguishes
jobs-manager. Returns a friendly multi-release error when
ambiguous, with remediation text in the message.
- internal/cluster/token.go — MintIngestorToken() tries the modern
TokenRequest path first, falls back to a static
service-account-token Secret on RBAC denial / older clusters / SA
missing. Errors propagate verbatim on non-recoverable failures
(network, context cancellation) so customers see the real
problem instead of a misleading "static fallback also failed."
- internal/cli/cluster.go — `tracebloc cluster info` command. Prints
context, server, namespace, parent release info, SA + token state
(with SHA256(token)[:8] instead of the raw bytes — token must
never appear in scrollback). Exit codes 3 (kubeconfig issue) / 4
(no parent release) / 5 (token mint failed).
Tests:
- 5 new test files covering happy path, multi-release ambiguity,
service name fallback, the sibling-deployment filter regression
(mysql + requests-proxy + jobs-manager all share chart-level
labels — discovery must pick jobs-manager by name suffix),
TokenRequest happy path, static-secret fallback, non-recoverable
error pass-through, and the combined-failure remediation message.
Coverage: internal/cluster 83.8%, internal/cli 59.5% (cluster info
itself is hard to unit-test without a real cluster — Phase 3+ adds
integration tests against a kind cluster).
Library footprint: brings in k8s.io/client-go + apimachinery + api
(@v0.31.0) and sigs.k8s.io/yaml. Cross-compiled binaries grow from
~10MB to ~30MB; cost is acceptable for the customer-experience
upside of "your kubeconfig is all you need."
Closestracebloc/client#150.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 19 / 8

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

Comment threadinternal/cluster/discover.go
@saadqbalsaadqbal self-assigned this May 21, 2026
CI on PR #2 hit a lint typecheck error
("undefined: jsonschema / yaml (typecheck)") that took a few iterations
to diagnose. Root cause: the k8s.io/client-go v0.31 line pulls in
transitive deps (sigs.k8s.io/structured-merge-diff/v6 vs v4) that
fight in go.mod, and golangci-lint v1.61's bundled Go SDK can't
typecheck a module whose `go` directive is newer than what the linter
supports.
Resolution:
1. Bump k8s.io/client-go + api + apimachinery from v0.31.0 to
v0.36.1 (latest stable). Fixes the structured-merge-diff
version split — v0.36 uses v6 consistently across the
dependency chain.
2. Accept whatever `go mod tidy` writes to go.mod's `go` directive
(currently 1.26 on this dev machine, 1.24 on others — same
either way since Go modules are forward-compatible). Stop
fighting tidy; pinning a stale version produces typecheck
errors instead of real findings.
3. Bump golangci-lint in the workflow from v1.61.0 to v1.64.7, the
first version that handles the Go 1.24+ source the dep tree
now requires.
4. Update .golangci.yml `run.go: "1.24"` to match go.mod's effective
minimum.
5. Refresh the go.mod comment so future readers understand why
the version directive isn't pinned low.
Local validation: `make vet test fmt-check schema-check` all
green; cluster-info smoke against the dev EKS still discovers
chart 1.3.5 + mints a TokenRequest token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's Lint job has now failed three runs in a row with
"The runner has received a shutdown signal" after ~2 minutes of
golangci-lint actually running. Not a flaky runner — reproducible.
Root cause: `staticcheck` and `unused` do full-program SSA analysis
across every transitive dep. With k8s.io/client-go + apimachinery +
api in the graph (≈80 indirect modules), that exceeds whatever
budget the standard 4-CPU GitHub-hosted runner allots for the job.
The runner gets preempted before lint completes.
Drop `staticcheck` and `unused` from the active linter set. Keep
the cheap per-file linters that catch the bugs we've actually hit
this week (errcheck, govet, ineffassign, gofmt, goimports, misspell,
unconvert).
Filed v0.2 ticket to bring them back via either (a) a self-hosted
larger runner, (b) `-skip-files=k8s.io/*` patterns that don't
exist in golangci-lint v1.64 but do in v2.x, or (c) split the lint
job to run staticcheck only on `./internal/...` (our own code) and
skip module-cache packages.
The dropped linters' value relative to CI cycles spent debugging
this:
- staticcheck SA-checks are valuable but redundant with `govet`
for the most-likely-to-bite cases (printf, lock copies, etc.)
- `unused` rarely fires on a brand-new codebase where every
symbol is just-introduced.
Pragmatic tradeoff for v0.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
saadqbaland others added 4 commits May 21, 2026 20:41
Four consecutive Lint failures on PR #2, all hitting the same
"shutdown signal received" at ~2 minutes (15:17, 15:24, 15:33, 15:35).
Not lint config (the trim from staticcheck/unused didn't help),
not a flaky runner (reproducible), not an OOM (no resource warning).
golangci-lint-action@v6 + the k8s.io/* dep tree appears to be the
incompatible combination in early 2026's GitHub Actions environment.
Rather than spend another iteration debugging the action, replace
it with standalone tools that already work locally and have
predictable behavior:
- errcheck v1.7.0 (the bug class we've actually hit this week)
- gofmt -s (the formatting check; matches what `make fmt-check` does)
- ineffassign v0.1 (cheap dead-assignment detection)
- misspell (typo guard)
Combined runtime in standalone mode: ~10s. golangci-lint's value-add
beyond these was staticcheck + unused — both already deferred to #6
as v0.2 work pending a strategy for the dep tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
errcheck v1.7.0 + ineffassign v0.1.0 + misspell v0.3.4 all transitively
import a golang.org/x/tools version (v0.17 era) that fails to compile
under current Go ("invalid array length -delta * delta" in
tokeninternal.go). Pinning was the right instinct for reproducibility,
but the upstream tools haven't shipped current-Go-compat tags yet.
Use @latest for now; reproducibility tradeoff is acceptable given
these are lint tools, not runtime deps. Document in #6 as
"pin once upstream tags newer versions" follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
errcheck caught 20 unchecked Fprintf/Fprintln returns in
runClusterInfo — same class of finding that was previously caught
+ fixed in internal/cli/ingest.go and cmd/tracebloc/main.go.
I missed cluster.go when I added the explicit-discard pattern there.
Same rationale as the other sites: the exit code is the contract;
a pipe-write failure shouldn't convert a successful diagnostic into
a non-zero exit. Wrap each call with `_, _ =`.
Now caught by CI thanks to the standalone-errcheck swap from the
previous commit. The whole reason for the lint-job rework was to
catch this exact bug class earlier in the loop — we just had to
trade the golangci-lint-action for a working setup first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preempts the next errcheck cycle. Same explicit-discard rationale as
the cluster.go fixes — stderr unreachable shouldn't change the exit
code we propagate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erride
Bugbot caught a contradiction in PR #2's Phase 2 code:
- The comment said "Read INGESTOR_IMAGE_DIGEST + ingestor SA name
from the Deployment's pod-spec env"
- The struct doc said "customers can override; the value comes from
jobs-manager's environment"
- But the switch only handled INGESTOR_IMAGE_DIGEST. SA name was
hardcoded to "ingestor", silently ignoring any customer override.
Truthful fix:
1. Update the comment and struct doc to admit the limitation.
2. Add a `--ingestor-sa` flag on `cluster info` so customers who
set `ingestionAuthz.serviceAccountName` to a non-default value
in the parent client chart can still use the CLI today.
3. Plumb the override through `runClusterInfo` -> applied to the
discovered ParentRelease before token mint.
4. Drop the now-only-INGESTOR_IMAGE_DIGEST switch statement to a
plain `if` — clearer, errcheck-friendlier, and signals there's
only one env var being read.
File #7 in tracebloc/cli for the proper fix: discover the SA name
from the chart-rendered ingestionAuthz ConfigMap so the flag
becomes unnecessary. v0.2 work, not blocking Phase 2 ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 123b56a. Configure here.

Comment thread.golangci.yml Outdated
Comment threadinternal/cluster/token.go
… drift
Cursor Bugbot's re-review on 123b56a flagged two new issues in the
Phase 2 (#150) tree:
1. internal/cluster/token.go reimplemented isForbidden / isNotFound /
isMethodNotSupported / statusCode against Status.Code (numeric HTTP
code) when k8s.io/apimachinery/pkg/api/errors already exports
IsForbidden / IsNotFound / IsMethodNotSupported that key off
Status.Reason (the typed enum). The two can diverge silently for
non-standard status errors. The test file already imports apierrors
and constructs fake errors via apierrors.NewForbidden(), so deferring
to the stdlib is both safer AND removes ~20 lines of homegrown code.
The four token tests still pass at 82.1% pkg coverage because
NewForbidden() sets both Code and Reason fields.
2. go.mod's top-of-file comment claimed "Minimum Go is 1.22" but the
actual `go 1.26.0` directive (forced by k8s.io/* v0.36.x deps)
contradicted it, and .golangci.yml pinned `go: "1.24"` — also stale.
Rewrote the go.mod comment to admit reality + tell future-me to
bump both together, and bumped the lint config to "1.26" to match.
The third inline comment from Bugbot's re-review is a stale carry-over
of the SA-name finding fixed in 123b56a (same bug ID 5e4b5df0…, GitHub
auto-shifted its anchor onto the new lines). Bugbot's own review-body
count confirms 2 new findings, not 3.
Local: go vet, go test -race -cover, gofmt -s, errcheck — all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@saadqbal
saadqbal merged commit 57bd6d5 into developMay 21, 2026
9 checks passed
saadqbal pushed a commit that referenced this pull request Jul 9, 2026
…cluster_in_use (#191)
The CLI mapped every provisioning 409 to a static "registered to a different
tracebloc account — sign in to that account, or ask your admin" — which was
often FALSE (the same-account phantom case) and a dead end. With the backend's
fix#2 (backend#1021) the 409 body now distinguishes:
• cluster_conflict — genuinely another account; body carries owner_email;
• cluster_in_use — a same-account client is live on this cluster.
New conflictMessage() parses the 409 body and picks the right guidance:
• cross-account → "registered to another tracebloc account (<owner_email>) —
ask them to release it, or sign in as that account" (contact-the-owner, never
"delete the cluster" — it isn't ours to wipe; names the owner when supplied);
• cluster_in_use → "another tracebloc client (<name>) in your account is already
live on this cluster — offboard it first with `tracebloc delete`, or provision
on a separate machine".
Degrades gracefully against a backend without fix#2 (empty/unparseable body →
the generic cross-account text). The client-side not-owned refusal (no HTTP body)
keeps the generic message. Reworded crossAccountConflictMsg to match.
Companion to backend#1021 (fix#2) and cli#190 (fix#1) of the phantom-client
migration.
Tests: owner_email surfaced; cluster_in_use names the live client and does NOT
read as cross-account; existing cross-account + client-side-refusal messages
updated to the new wording. Full suite green; gofmt -s / errcheck / ineffassign /
misspell clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saadqbal
saadqbal deleted the feat/150-cluster-discovery branch July 10, 2026 10:37
LukasWodka added a commit that referenced this pull request Jul 10, 2026
#208's fix#2 (demote Inf/NaN columns to VARCHAR) is superseded by #185/#210:
the di#349 floatRE grammar pre-screens the token before ParseFloat, so
"inf"/"Infinity"/"NaN" already fall through to VARCHAR. Dropped the redundant
production change on rebase; kept the intent as a regression test, since no
parity-fixture case covers non-finite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka added a commit that referenced this pull request Jul 10, 2026
…canner buffer (bug-hunt MED) (#208)
* fix(auth): reject unknown --env/$CLIENT_ENV at login instead of silently using prod
BaseURL falls unknown/typo env values back to prod (a lenient library
default), so `login --env staging` or `CLIENT_ENV=prd` silently targeted
production AND persisted it as the active session env for every later
command — the class behind earlier dev-vs-prod confusion.
login PICKS and persists the session env, so a typo must fail there. Add
api.IsKnownEnv (dev/stg/prod, case-insensitive) and validate the resolved
env at runLogin entry, before any network call. ResolveEnv still maps
empty->prod, so the no-flag default is unaffected. BaseURL's unknown->prod
fallback is deliberately unchanged (TestBaseURL asserts it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(submit): raise log-scanner buffer to 16 MB so tqdm progress can't force a false exit 9
tqdm (a data-ingestors dep) redraws its progress bar with \r and no \n,
so a whole ingestion phase's redraws are one newline-delimited "line" that
grows for the life of the run. Past 1 MB the display scanner returned
bufio.ErrTooLong, cutting the log stream mid-run; a still-running Job then
couldn't be confirmed terminal in the 30s finalJobStatus poll, so watch
returned a false exit 9 on a healthy large ingestion — exactly the case the
1h JobWatchTimeout targets.
The parser is fed via the TeeReader, not the scanner, so this cap only ever
bounded the DISPLAY line and never the verdict. Raise it to 16 MB (clears a
fast ~10/s hour of redraws with headroom; the 1h cap bounds accumulation).
The buffer grows on demand, so ordinary log lines still cost 64 KB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(submit): drain past ErrTooLong so a giant tqdm line can't force a false exit 9 (review)
Addresses @saadqbal's review on #208: the 16 MB buffer bump only MOVED the
false-exit-9 threshold, it didn't close it. The tee is pulled only by the
DISPLAY scanner, so when a line trips ErrTooLong the scan loop exits, the tee
stops being read, and the parser never sees the rest of the stream (the closing
banner) → streamFailed && outcome==Unknown → a false exit 9 on a healthy run.
A long enough single '\r'-line (> the buffer) still breaks it.
Class-level fix (his suggestion): keep draining past ErrTooLong. Extracted the
display/parse loop into streamDisplayAndParse; on ErrTooLong it drains the rest
of the stream THROUGH the tee (io.Copy to io.Discard) so the parser still sees
the banner, and it is NOT fatal — the Job status poll is the verdict's source of
truth. Genuine read failures (network drop, ctx cancel) still propagate.
Corrected the now-wrong "cap never affects the verdict" comment; kept 16 MB as a
generous display headroom (the drain is the correctness guarantee).
New tests (the #3 no-test gap Asad noted): an oversized '\r'-line + the real
ingestor banner → the parser still resolves the summary (no false exit 9); and
a genuine read error still propagates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(push): pin Inf/NaN → not FLOAT against the di#349 inference
#208's fix#2 (demote Inf/NaN columns to VARCHAR) is superseded by #185/#210:
the di#349 floatRE grammar pre-screens the token before ParseFloat, so
"inf"/"Infinity"/"NaN" already fall through to VARCHAR. Dropped the redundant
production change on rebase; kept the intent as a regression test, since no
parity-fixture case covers non-finite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka added a commit that referenced this pull request Jul 13, 2026
…review #2/#3)
Addresses @aptracebloc's review — both confirmed Medium, one shared root:
the provisioned/namespace signal wasn't sourced uniformly across the home
screen and doctor.
- doctor ignored the active-client binding: runClusterDoctor now applies
bindActiveClientNamespace before cluster.Load — the same seam `cluster
info` / data / the home screen use — so with no --namespace/--context it
targets the active client's cached namespace, not the kubeconfig default.
Removes the home-screen <-> doctor contradiction. Binding-only: a local
config read, no extra cluster dial, matching what the home screen does
when provisioned (check the bound namespace, no cluster-wide scan).
- provisioned-vs-no-environment keyed off env.name (ActiveClientName) while
the probe's ownership gate keyed off the cached namespace, so a
provisioned-but-unnamed profile misread as "no environment / run the
installer". resolveHomeModel now takes a `provisioned` signal from the
SAME field the gate uses (ActiveClientNamespace) and renders offline on
`provisioned || env.name != ""`; the display name falls back to the
client ID so it stays a *named* offline.
Tests (mutation-proven): new TestResolveHomeModel_States case (provisioned,
namespace only, no name -> offline) + TestClusterDoctor_BindsActiveClientNamespace.
Updated TestHasTopLevelCommand (resources is wired now that #237 merged).
make ci green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc pushed a commit that referenced this pull request Jul 13, 2026
* feat(cli): status-aware `tracebloc` home screen
Rework the bare-`tracebloc` home screen from a stateless command list into
a status-aware landing screen that opens with where you actually stand —
signed in? is this machine's secure environment live? — then the commands.
Two separate, never-fused axes: sign-in (you) and the secure environment
(the machine), because the client heartbeats with its own credential. A
green "· Online" prints ONLY when the environment is live locally (chart
present + jobs-manager Ready) AND positively confirmed heartbeating to
tracebloc; a heartbeat we can't confirm degrades to "· running", never a
false green. States: not-signed-in / online / running-not-heard-from /
offline / no-environment.
Detection is best-effort and bounded so bare `tracebloc` (run constantly,
previously zero-I/O) stays snappy: probes run concurrently, each with its
own short timeout, all capped by a ~1.5s overall budget; any error/timeout
degrades to the softer state and the screen still renders. Logged out does
zero cluster/backend I/O. An unreachable cluster caps at the probe timeout
(~1.2s) instead of the OS default.
Reuses existing seams: config sign-in + cached email, the data commands'
namespace binding + release discovery, delete.go's `tb`-alias ownership
check, and node allocatable for the compute parenthetical (no `resources`
command on develop). `<inv>` echoes the invoked binary name (tb/tracebloc);
the doctor path shown is the real `cluster doctor`.
HEARTBEAT CREDENTIAL: read via the signed-in user token (ListClients),
the only path that exists — there's no login-free in-cluster-client
credential path, so the environment line is confirmable only while signed
in (documented in realHeartbeat).
Adds ui.CheckLine/CrossLine/WarnLine for the locked ✓/✗/⚠ status glyphs.
Tests are table-driven and cluster-free: every state, the honesty fallback
(can't-confirm → running, not Online), and the timeout/degrade path (a slow
probe still renders fast with the softer state).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): address home-screen review — honest offline, named degrades, starting state
Code-review follow-ups on the status-aware home screen:
- A provisioned machine is never told "no environment / run the installer".
A reachable cluster that doesn't host this release (wrong kube-context, or
the client runs on a cluster this kubeconfig doesn't point at) now degrades
to a NAMED offline when a client name is cached — the "runs elsewhere" case
the sibling data commands explain via binding.explain — instead of the
no-environment lie. Only a machine that was never provisioned shows no-env.
- The budget-timeout degrade keeps the remembered name. resolveHomeModel now
reads the cached client name up front (new rememberedName seam) and fills it
whenever the probe surfaced none — including the bctx.Done() path — so a
context-ignoring kubeconfig exec-credential plugin (aws eks get-token, etc.)
that outlives the render degrades to a named offline, not no-env.
- The offline copy is honest for BOTH causes (stopped/unreachable AND
reachable-but-release-not-here): "· can't reach it from here — run
<inv> cluster doctor", not the bare "offline".
- Degraded workload gets its own state + line (homeStarting: "· starting up,
not ready yet"), distinct from the live-but-unconfirmed-heartbeat "running,
but tracebloc hasn't heard from it" (heartbeat is never consulted when the
workload isn't Ready). Both still point at cluster doctor.
- Header comment: the budget bounds the RENDER (wall-clock), not the probe
goroutines — a context-ignoring probe can outlive it; the buffered channels
just keep it from blocking.
Invariants held + mutation-proven: no green Online without localLive +
beatOnline (honesty), and detection never hangs (collector bails on the
budget without probe cooperation). New/changed tests below; make ci green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(home): drain just-finished probes at the budget + honest running copy (Bugbot)
Two review findings on the status-aware home screen:
- Budget expiry could drop completed probe results: when bctx.Done() and a
buffered result are ready in the same select, the pick is random — a probe
that finished just as the budget fired could be discarded for the softer
default (live release rendering as offline/no-env). The collector (now
collectProbes, extracted for a deterministic test) drains both buffers
non-blocking on Done, so only probes that truly haven't reported degrade.
- The running line said "tracebloc hasn't heard from it" for BOTH heartbeat
answers. That claim is only earned when the backend positively reported
not-online (offline/pending); a mere couldn't-confirm (backend unreachable,
timeout) now says "couldn't confirm it's connected to tracebloc" instead of
asserting a backend view we never obtained. homeModel carries
confirmedNotOnline so the render stays pure.
Both fixes mutation-proven (drain removal and flag collapse each fail their
new tests); make ci green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(home): never adopt a foreign release as "your secure environment" (Bugbot)
With no active client cached, realProbeEnv's discovery fell back to the
kubeconfig's default namespace and then the cluster-wide scan
(binding.allowScan() is true when the binding isn't applied) — so on a shared
cluster a colleague's release could render as YOUR live environment, full data
menu included. The data commands only run that scan behind a visible retarget
note and an explicit user action; the home probe passes p=nil, so even that
disclosure was silently dropped, and §7.5's rule (a miss must never silently
retarget to some other client) applies doubly to a status screen.
Ownership gate: no active-client binding ⇒ report localNoRelease before any
cluster I/O — resolveHomeModel renders the honest no-env screen (or a named
offline via the remembered-name fallback). Provisioned machines are untouched
(binding scopes discovery to the active client's namespace, scan already
disabled). Side effect: the common unprovisioned re-entry now does zero
cluster I/O.
Mutation-proven both ways (gate removed → the unprovisioned probe dials out
and lands unreachable; over-gated → the provisioned probe stops short);
make ci green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(home): fold the locked home-screen design; top-level doctor; gated resources
Design sign-off for the status-aware `tracebloc` home screen (cli#244): fold
the LOCKED reference layout into the shipping renderer — byte-for-byte for the
signed-in/Online screen.
- ui: add Printer.MenuRow (dim · bullet, command padded to width, 4-space gap,
dimmed description) — the locked command-row style.
- home: rework renderHome to the locked layout — two-blank header, greeting by
first name (profile → clean email local-part → omit gracefully), a 30-col dim
rule, the two honest status axes (detection + no-false-Online invariant
unchanged), two command buckets ("Your data" + "Your secure environment",
`delete` folded in, "Manage" dropped), rows via MenuRow, and a dim
`love from tracebloc` sign-off. Not-signed-in + no-env restyled to match.
- doctor: promote `doctor` to a real top-level command; `cluster doctor` stays
as a hidden alias sharing one RunE (single diagnostic code path). The home
screen + env-status lines now read `<inv> doctor`.
- home: gate the `resources` row on the live command tree — absent until #237
wires `resources`, appears automatically once it does (never a hardcode).
- tests: byte-identical lock test against the reference render; name derivation;
resources gating (render + command-tree, both ways); top-level doctor shares
the cluster-doctor path; all existing state/honesty/timeout coverage kept.
make ci green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(home): bound + sanitize greeting name; retarget cluster-doctor hints to `doctor`
Review follow-ups on the locked home screen (cli#244):
- home: greetingName now runs BOTH the profile first name and the email
local-part through one clean-token check (letters only, single word, no
interior whitespace/newline/control chars) and caps the result at
greetingNameMax = 14 runes — otherwise it omits the name. Stops a long
local-part from stretching, or a newline-bearing FirstName from SPLITTING,
the locked single-line header. The clean short demo name is unaffected, so
the byte-identical golden render is unchanged.
- doctor: retarget the now-hidden `cluster doctor` remediation hints to the
canonical `tracebloc doctor` (the alias still runs, so non-breaking) —
client.go x4 (create-fail hint, discovery-fail error, two connect-timeout
errors) + cluster/discover.go's ErrNoParentRelease tail. Kept doctor.Run's
suffix-strip in lockstep (it trims that exact tail so doctor never tells you
to run doctor) and updated the two tests that pinned the old text.
- tests: greeting-name bound/sanitize cases (over-long / interior-newline / tab
/ control-char / multi-word -> omit; cap boundary used); an end-to-end
one-line-header guard; a direct hidden-alias assertion (`doctor` visible
top-level, `cluster doctor` present but Hidden). Fixed the golden test's
stale cmd/hsdemo comment.
make ci green; golden render still byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(home): two blank lines above every section title, not just the first
Lukas spacing tweak (cli#244): the status->first-section gap was 2 blanks (a
standalone pre-loop Newline + Section's own leading blank) while the inter-
section gaps were only 1. renderBuckets now emits a Newline before each Section,
and the standalone pre-loop Newline is dropped — so every section gets exactly
2 blanks above and the status->first-section gap stays 2 (not 3). Mirrors the
locked demo's new Newline+Section+Newline-per-bucket render.
Golden test updated (+1 blank before "Your secure environment"); make ci green,
render still byte-identical to the demo (850 bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(home,doctor): source the provisioned/namespace signal uniformly (review #2/#3)
Addresses @aptracebloc's review — both confirmed Medium, one shared root:
the provisioned/namespace signal wasn't sourced uniformly across the home
screen and doctor.
- doctor ignored the active-client binding: runClusterDoctor now applies
bindActiveClientNamespace before cluster.Load — the same seam `cluster
info` / data / the home screen use — so with no --namespace/--context it
targets the active client's cached namespace, not the kubeconfig default.
Removes the home-screen <-> doctor contradiction. Binding-only: a local
config read, no extra cluster dial, matching what the home screen does
when provisioned (check the bound namespace, no cluster-wide scan).
- provisioned-vs-no-environment keyed off env.name (ActiveClientName) while
the probe's ownership gate keyed off the cached namespace, so a
provisioned-but-unnamed profile misread as "no environment / run the
installer". resolveHomeModel now takes a `provisioned` signal from the
SAME field the gate uses (ActiveClientNamespace) and renders offline on
`provisioned || env.name != ""`; the display name falls back to the
client ID so it stays a *named* offline.
Tests (mutation-proven): new TestResolveHomeModel_States case (provisioned,
namespace only, no name -> offline) + TestClusterDoctor_BindsActiveClientNamespace.
Updated TestHasTopLevelCommand (resources is wired now that #237 merged).
make ci green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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

@saadqbal@LukasWodka@aptracebloc