Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/troubleshooting.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,9 @@ produces that code.
| `0` | Success — includes `--dry-run` completing, a guided run you cancelled cleanly, and `doctor` passing with warnings only | all commands | `exitOK` |
| `1` | Generic failure with no more specific bucket (also any error without an explicit code) | `login`, `client …`, `delete`, mistyped commands | `exitFailure` |
| `2` | Your input didn't validate: schema validation failed (spec synthesized from flags, or your YAML), an unsupported/unknown `--task`, a task-scoped flag applied to the wrong task, an invalid dataset name, or a resource size that doesn't fit the machine | `data ingest`, `data validate`, `data delete`, `resources set` | `exitBadInput` |
| `2` | One or more checks failed | `doctor` | `exitChecksFailed` |
| `3` | Local environment problem: kubeconfig couldn't be loaded, the dataset path is missing or unreadable, the local layout is wrong, a YAML file didn't parse, or a prompt was needed but the run is non-interactive (`--no-input` / `--output-json` / no TTY) | `data ingest`, `data validate`, `data list`, `data delete`, `doctor`, `resources`, `resources set` | `exitLocalEnv` |
| `4` | Cluster reachable but no tracebloc client found in the namespace — or its shared storage / dataset list is missing, so the target can't be confirmed | `data ingest`, `data list`, `data delete`, `cluster info`, `resources`, `resources set` | `exitNoWorkspace` |
| `2` | One or more checks failed — for `client status --seal`: the environment is unsealed (a conformance check failed), or unknown (the chart ships no conformance checks, so the seal couldn't be verified) | `doctor`, `client status --seal` | `exitChecksFailed` |
| `3` | Local environment problem: kubeconfig couldn't be loaded, the dataset path is missing or unreadable, the local layout is wrong, a YAML file didn't parse, or a prompt was needed but the run is non-interactive (`--no-input` / `--output-json` / no TTY) | `data ingest`, `data validate`, `data list`, `data delete`, `doctor`, `resources`, `resources set`, `client status --seal` | `exitLocalEnv` |
| `4` | Cluster reachable but no tracebloc client found in the namespace — or its shared storage / dataset list is missing, so the target can't be confirmed | `data ingest`, `data list`, `data delete`, `cluster info`, `resources`, `resources set`, `client status --seal` | `exitNoWorkspace` |
| `5` | Auth: the ingestor SA token couldn't be obtained, or jobs-manager rejected it (401/403) | `data ingest`, `cluster info` | `exitAuth` |
| `5` | No dataset by that name on this client (nothing to delete) | `data delete` | `exitNoSuchDataset` |
| `6` | Destination table already exists — re-run with `--overwrite` to replace it, or pick a different `--name` | `data ingest` | `exitTableExists` |
Expand Down
179 changes: 0 additions & 179 deletions internal/cli/client.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,6 @@ import (
"path/filepath"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"

Expand DownExpand Up@@ -758,184 +757,6 @@ func runClientList(ctx context.Context, p *ui.Printer) error {
return nil
}

func newClientStatusCmd() *cobra.Command {
var wait bool
var timeout time.Duration
cmd := &cobra.Command{
Use: "status",
Short: "Show whether tracebloc can see this machine's client (online)",
Long: `Report tracebloc's view of this machine's active client — online, offline,
or pending. With --wait, poll until tracebloc reports it online (exit 0) or the
timeout elapses (non-zero), to confirm the client connected after setup.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// --timeout only governs the --wait poll; accepting it alone would be a
// silent no-op, so reject it rather than mislead.
if cmd.Flags().Changed("timeout") && !wait {
return &exitError{code: exitFailure, err: errors.New("--timeout has no effect without --wait")}
}
return runClientStatus(cmd.Context(), printerFor(cmd), wait, timeout)
},
}
cmd.Flags().BoolVar(&wait, "wait", false, "poll until tracebloc reports this client online")
cmd.Flags().DurationVar(&timeout, "timeout", 120*time.Second, "with --wait, give up after this long")
return cmd
}

// clientStatusPollInterval is how often --wait re-checks the backend. A const,
// not a seam: tests inject through pollAfter (which ignores the duration and
// fires instantly), so the value never needs overriding.
const clientStatusPollInterval = 3 * time.Second

func runClientStatus(ctx context.Context, p *ui.Printer, wait bool, timeout time.Duration) error {
client, cfg, err := authedClient()
if err != nil {
return &exitError{code: exitFailure, err: err}
}
active := cfg.Current().ActiveClientID
if active == "" {
return &exitError{code: exitFailure, err: errors.New(
"no active client on this machine — run `tracebloc client create` (or re-run the installer) first")}
}

// One-shot: report the current state and exit 0 (informational).
if !wait {
st, found, lerr := lookupClientStatus(ctx, client, active)
if lerr != nil {
return &exitError{code: exitFailure, err: lerr}
}
if !found {
return &exitError{code: exitFailure, err: fmt.Errorf(
"active client %s isn't in your account — run `tracebloc client create` "+
"(or re-run the installer) to provision this machine", active)}
}
p.Section("Client status")
p.Field("state", clientStateLabel(st))
return nil
}

// --wait: poll the same source `client list` renders until online or timeout.
// The backend-reported status is the honest Online signal (RFC-0001 §8.5); a
// local rollout check can't tell whether tracebloc can actually see the client.
sp := p.Spinner("Waiting for tracebloc to confirm…", "")
defer sp.Stop() // leak-proof net for every return; the online path Stops explicitly before its ✔
deadline := time.Now().Add(timeout)
var lastErr error // most recent transient (retryable) error, for the timeout message
lastState := -1 // most recent successfully-read status; -1 = none read yet
var ue *api.UpgradeRequiredError
var apiErr *api.APIError
for {
st, found, lerr := lookupClientStatus(ctx, client, active)
switch {
case errors.As(lerr, &ue):
// 426 (CLI too old) won't recover by waiting — surface the upgrade signal.
return &exitError{code: exitFailure, err: lerr}
case errors.As(lerr, &apiErr) && (apiErr.StatusCode == http.StatusUnauthorized || apiErr.StatusCode == http.StatusForbidden):
// A revoked/expired/forbidden token (401/403) won't recover by waiting —
// the client itself may be online. Fail fast, point at sign-in. Note 429
// and 5xx stay transient (below): those DO recover on retry.
return &exitError{code: exitFailure, err: errors.New(
"tracebloc rejected your credentials while waiting — run `tracebloc login`, then retry")}
case lerr != nil:
lastErr = lerr // transient (5xx / 429 / network) — keep waiting, remember why
case !found:
// The active client isn't in the account (deleted / wrong account) — no
// amount of waiting surfaces it. Fail fast, matching the one-shot path.
return &exitError{code: exitFailure, err: fmt.Errorf(
"active client %s isn't in your account — run `tracebloc client create` "+
"(or re-run the installer) to provision this machine", active)}
case st == clientStatusOnline:
sp.Stop()
p.Successf("tracebloc can see this client.")
return nil
default:
// A good poll (present, not online yet) supersedes any earlier transient
// error, so a later timeout reports the real state, not a stale failure.
lastErr, lastState = nil, st
}

// --timeout caps how long we WAIT: stop once the budget is spent, and clamp
// the sleep to what remains so total runtime doesn't overshoot by a poll
// cycle. A poll begun within budget is still honored (online → ✔ above).
remaining := time.Until(deadline)
if remaining <= 0 {
switch {
case lastErr != nil:
return &exitError{code: exitFailure, err: fmt.Errorf(
"timed out after %s waiting for tracebloc to report this client online; "+
"the last status check failed: %v", timeout, lastErr)}
case lastState >= 0:
return &exitError{code: exitFailure, err: fmt.Errorf(
"timed out after %s waiting for tracebloc to report this client online (last state: %s). "+
"Run `tracebloc doctor` to diagnose, or re-run the installer.", timeout, clientStateLabel(lastState))}
default:
return &exitError{code: exitFailure, err: fmt.Errorf(
"timed out after %s before tracebloc could confirm this client — retry, "+
"or run `tracebloc doctor`.", timeout)}
}
}
wait := clientStatusPollInterval
if wait > remaining {
wait = remaining
}
select {
case <-ctx.Done():
return &exitError{code: exitInterrupted} // Ctrl-C: exit quietly (no "Error: context canceled")
case <-pollAfter(wait):
}
}
}

// lookupClientStatus fetches the active client directly and returns its backend
// status code. found=false means no such client (deleted, or signed into the
// wrong account). A lookup error is returned verbatim so --wait can treat it as
// transient and retry. Fetches the single client by id (GET /edge-device/{id}/)
// rather than listing the whole account — the home-screen heartbeat runs this
// under a ~1.2s budget, and paging every client blew it (cli#338).
func lookupClientStatus(ctx context.Context, client *api.Client, active string) (status int, found bool, err error) {
id, err := strconv.Atoi(active)
if err != nil {
// A non-numeric active id can never match a backend client, so report it
// as not-found — exactly what the old ListClients+match path did — rather
// than a permanent error. A --wait loop fail-fasts on a missing client but
// treats errors as transient, so returning an error here would make it
// poll a permanent parse failure to the timeout (Bugbot: poll/retry loops
// must fail-fast on non-transient errors).
return 0, false, nil
}
c, err := client.GetClient(ctx, id)
if err != nil {
return 0, false, err
}
if c == nil {
return 0, false, nil // 404 — no such client
}
return c.Status, true, nil
}

// EdgeDevice.status codes mirrored from the backend (metaApi User.py).
const (
clientStatusOffline = 0
clientStatusOnline = 1
clientStatusPending = 2
)

// clientStateLabel maps the backend status code to a TTY/CI-safe word. Plain
// text (not an emoji glyph) on purpose — flag/emoji glyphs mojibake in CI logs
// and Windows consoles (RFC-0001 §12 watch-item).
func clientStateLabel(status int) string {
switch status {
case clientStatusOnline:
return "online"
case clientStatusOffline:
return "offline"
case clientStatusPending:
return "pending"
default:
return "unknown"
}
}

// setActiveClient points this env's profile at c, caching its namespace and
// display name alongside the id so the data commands can bind to the active
// client's cluster (§7.3) without a backend round-trip. Callers Save() after.
Expand Down
Loading
Loading