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: 6 additions & 0 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,6 +153,12 @@ jobs:
go install golang.org/x/tools/cmd/deadcode@v0.48.0
deadcode ./cmd/tracebloc

- name: File budget (line-count ratchet)
# Per-file line ceilings; they only ratchet DOWN — raising one is a
# deliberate, reviewed edit to scripts/file-budget.sh. Keeps the next
# 1500-line data.go from growing quietly (backend#1106 WS-B).
run: ./scripts/file-budget.sh

govulncheck:
timeout-minutes: 10
name: govulncheck
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ GOVULNCHECK_VERSION ?= v1.1.4
# ---- top-level targets -------------------------------------------

.PHONY: ci
ci: vet test lint fmt-check schema-check vulncheck deadcode
ci: vet test lint fmt-check schema-check vulncheck file-budget deadcode
@echo "==> ci: all green"

.PHONY: build
Expand DownExpand Up@@ -155,6 +155,14 @@ schema-check:
schema-sync:
./scripts/sync-schema.sh

# file-budget: per-file line ceilings (ratchet down only — raising one is a
# reviewed edit to scripts/file-budget.sh). Keeps the next 1500-line data.go
# from growing quietly (backend#1106 WS-B). Also enforced by build.yml's
# lint job, keeping the "make ci green => CI green" invariant.
.PHONY: file-budget
file-budget:
./scripts/file-budget.sh

# ---- cleanup -----------------------------------------------------

.PHONY: clean
Expand Down
27 changes: 27 additions & 0 deletions docs/troubleshooting.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,33 @@ If you have a `~/.bash_profile` that doesn't source `~/.profile` or `~/.bashrc`,
the PATH entry may be skipped. Either add the `export PATH=…` line to the file
your login shell actually reads, or use the `/usr/local/bin` approach above.

## Exit codes

Every command exits `0` on success. Non-zero codes are a scripting contract —
they're stable, and each command's `--help` documents the subset it can
produce (`tracebloc data ingest --help` has the fullest list). This is the
cross-command view; the names in the last column are the constants in
`internal/cli/exitcodes.go`, so grepping a name finds every site that
produces that code.

| Code | Meaning | Produced by | Constant |
|------|---------|-------------|----------|
| `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` |
| `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` |
| `7` | Pre-flight succeeded but staging the files failed (Pod creation, image pull, exec stream, or remote tar error) | `data ingest` | `exitStagingFailed` |
| `7` | Removing an existing table + its files failed partway (see the error for the recovery command) | `data delete`, `data ingest --overwrite` | `exitTeardownFailed` |
| `7` | The cluster couldn't be queried for its datasets | `data list` | `exitQueryFailed` |
| `8` | jobs-manager rejected the submitted run (a non-auth 4xx/5xx), or the port-forward to it couldn't be set up | `data ingest` | `exitSubmitFailed` |
| `9` | The ingestion Job exited non-zero, completed with row-level failures the summary panel reports, or its outcome couldn't be determined / followed | `data ingest` | `exitIngestFailed` |
| `130` | You hit Ctrl-C at an interactive prompt (128+SIGINT) | interactive prompts | `exitInterrupted` |

## Still stuck?

Open an issue at [github.com/tracebloc/cli/issues](https://github.com/tracebloc/cli/issues)
Expand Down
36 changes: 18 additions & 18 deletions internal/cli/auth.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,15 +53,15 @@ var (
func runLogin(ctx context.Context, p *ui.Printer, envFlag string) error {
cfg, err := config.Load()
if err != nil {
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}
env := api.ResolveEnv(envFlag)
// login PICKS the session env and persists it (cfg.CurrentEnv below), so a
// typo must fail HERE, not silently resolve to prod. BaseURL's lenient
// unknown→prod fallback would otherwise route `--env staging` / `CLIENT_ENV=prd`
// to production and store it as the active env for every later command.
if !api.IsKnownEnv(env) {
return &exitError{code: 1, err: fmt.Errorf(
return &exitError{code: exitFailure, err: fmt.Errorf(
"unknown backend environment %q — valid values are dev, stg, prod (default). "+
"Check --env / $CLIENT_ENV", env)}
}
Expand All@@ -72,11 +72,11 @@ func runLogin(ctx context.Context, p *ui.Printer, envFlag string) error {
if err != nil {
var ae *api.APIError
if errors.As(err, &ae) && ae.StatusCode == http.StatusNotFound {
return &exitError{code: 1, err: fmt.Errorf(
return &exitError{code: exitFailure, err: fmt.Errorf(
"this backend (%s) doesn't support browser login yet — the device-grant "+
"endpoints land in backend#835: %w", env, err)}
}
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}

p.Section("Sign in to tracebloc")
Expand DownExpand Up@@ -117,7 +117,7 @@ func runLogin(ctx context.Context, p *ui.Printer, envFlag string) error {
prof.FirstName = id.FirstName
}
if err := cfg.Save(); err != nil {
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}
if prof.Email != "" {
p.Successf("Signed in as %s.", prof.Email)
Expand DownExpand Up@@ -149,11 +149,11 @@ func pollForToken(ctx context.Context, p *ui.Printer, client *api.Client, dc *ap

for {
if !deadline.IsZero() && time.Now().After(deadline) {
return "", &exitError{code: 1, err: errors.New("login timed out — re-run `tracebloc login`")}
return "", &exitError{code: exitFailure, err: errors.New("login timed out — re-run `tracebloc login`")}
}
select {
case <-ctx.Done():
return "", &exitError{code: 130} // Ctrl-C: exit quietly (no "Error: context canceled")
return "", &exitError{code: exitInterrupted} // Ctrl-C: exit quietly (no "Error: context canceled")
case <-pollAfter(time.Duration(interval) * time.Second):
}

Expand All@@ -168,11 +168,11 @@ func pollForToken(ctx context.Context, p *ui.Printer, client *api.Client, dc *ap
// interval by 5 seconds for this and all subsequent polls.
interval += 5
case errors.Is(err, api.ErrExpiredToken):
return "", &exitError{code: 1, err: errors.New("the sign-in code expired — re-run `tracebloc login`")}
return "", &exitError{code: exitFailure, err: errors.New("the sign-in code expired — re-run `tracebloc login`")}
case errors.Is(err, api.ErrAccessDenied):
return "", &exitError{code: 1, err: errors.New("sign-in was denied in the browser")}
return "", &exitError{code: exitFailure, err: errors.New("sign-in was denied in the browser")}
default:
return "", &exitError{code: 1, err: err}
return "", &exitError{code: exitFailure, err: err}
}
}
}
Expand All@@ -188,7 +188,7 @@ func newLogoutCmd() *cobra.Command {
p := printerFor(cmd)
cfg, err := config.Load()
if err != nil {
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}
if !cfg.SignedIn() {
p.Hintf("Already signed out.")
Expand All@@ -212,7 +212,7 @@ func newLogoutCmd() *cobra.Command {
// would bleed into the next sign-in on this env.
*prof = config.Profile{}
if err := cfg.Save(); err != nil {
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}

// Then revoke the token server-side so a copied/leaked credential stops
Expand DownExpand Up@@ -259,7 +259,7 @@ func newAuthStatusCmd() *cobra.Command {
}
cfg, err := config.Load()
if err != nil {
return &exitError{code: 1, err: err}
return &exitError{code: exitFailure, err: err}
}
p := printerFor(cmd)
if !cfg.SignedIn() {
Expand DownExpand Up@@ -309,7 +309,7 @@ func runAuthCheck(ctx context.Context, p *ui.Printer, envFlag string) error {
if p.Verbose() {
p.Hintf("Not signed in. Run `tracebloc login`.")
}
return &exitError{code: 1}
return &exitError{code: exitFailure}
}
target := api.ResolveEnv(envFlag)
if !cfg.SignedIn() || cfg.CurrentEnv != target {
Expand All@@ -320,7 +320,7 @@ func runAuthCheck(ctx context.Context, p *ui.Printer, envFlag string) error {
p.Hintf("Not signed in. Run `tracebloc login`.")
}
}
return &exitError{code: 1}
return &exitError{code: exitFailure}
}
// Signed in AND CurrentEnv == target: probe it. authedClient() builds the client
// for sessionEnv (== CurrentEnv == target) with the stored token — reuse it and
Expand All@@ -330,15 +330,15 @@ func runAuthCheck(ctx context.Context, p *ui.Printer, envFlag string) error {
if p.Verbose() {
p.Hintf("Not signed in. Run `tracebloc login`.")
}
return &exitError{code: 1}
return &exitError{code: exitFailure}
}
if _, err := client.WhoAmI(ctx); err != nil {
// A 426 means the CLI is too old, not that the session is invalid — surface
// the upgrade instruction (non-silent, so it shows even without --verbose)
// instead of the "re-login" advice, which wouldn't help.
var ue *api.UpgradeRequiredError
if errors.As(err, &ue) {
return &exitError{code: 1, err: ue}
return &exitError{code: exitFailure, err: ue}
}
if p.Verbose() {
// Only a 401/403 is genuinely a rejected token (where re-login helps); a
Expand All@@ -351,7 +351,7 @@ func runAuthCheck(ctx context.Context, p *ui.Printer, envFlag string) error {
p.Hintf("Couldn't verify your session with the backend (%v).", err)
}
}
return &exitError{code: 1}
return &exitError{code: exitFailure}
}
if p.Verbose() {
if email := cfg.Current().Email; email != "" {
Expand Down
Loading
Loading