diff --git a/internal/cli/copy_catalog_test.go b/internal/cli/copy_catalog_test.go index 615a920..2433de5 100644 --- a/internal/cli/copy_catalog_test.go +++ b/internal/cli/copy_catalog_test.go @@ -365,19 +365,28 @@ func TestCopyCatalog(t *testing.T) { []run{{"tracebloc upgrade --help", help("upgrade")}}, ) + // ── 12 prepare-host ────────────────────────────────────────────────────────── + prepareHostFile := doc( + "tb prepare-host — one-time admin step so a non-admin can install", + "What you see when you run `tb prepare-host` — the one-time administrator step\nthat readies a shared / HPC host so a non-admin user can then install tracebloc\nwith no root. It re-runs the installer's verified prepare-host step; the\nprivileged prep + its progress stream from the installer (not CLI copy). Only the\n--help is byte-exact below.", + nil, + []run{{"tracebloc prepare-host --help", help("prepare-host")}}, + ) + files := map[string]string{ - "00-home.golden": homeFile, - "01-data-ingest.golden": dataIngestFile, - "02-data-list.golden": dataListFile, - "03-data-delete.golden": dataDeleteFile, - "04-resources.golden": resourcesFile, - "05-doctor.golden": doctorFile, - "06-delete.golden": deleteFile, - "07-login.golden": loginFile, - "08-client.golden": clientFile, - "09-cluster.golden": clusterFile, - "10-version.golden": versionFile, - "11-upgrade.golden": upgradeFile, + "00-home.golden": homeFile, + "01-data-ingest.golden": dataIngestFile, + "02-data-list.golden": dataListFile, + "03-data-delete.golden": dataDeleteFile, + "04-resources.golden": resourcesFile, + "05-doctor.golden": doctorFile, + "06-delete.golden": deleteFile, + "07-login.golden": loginFile, + "08-client.golden": clientFile, + "09-cluster.golden": clusterFile, + "10-version.golden": versionFile, + "11-upgrade.golden": upgradeFile, + "12-prepare-host.golden": prepareHostFile, "zz-all-strings.golden": "every user-facing string in the source (AST-harvested — all arguments to the\n" + "Printer methods + errors.New/fmt.Errorf/fmt.Sprintf, plus the text/remedy\n" + "fields of healthLine{} and doctor.Result{} literals, both \"…\" and `…` raw\n" + diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 328041b..ced5d57 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -19,10 +19,16 @@ import ( "github.com/tracebloc/cli/internal/ui" ) +// installerURL is the single source of truth for the installer script URL. +// Everything that downloads or points at the installer (installCmd here, +// prepareHostInstallerCmd in prepare_host.go) derives from this so a URL change +// updates every path at once. +const installerURL = "https://tracebloc.io/i.sh" + // installCmd is the one-line installer we point people at when there's no // secure environment on this machine, or a component needs reinstalling. Kept in // one place so every remedy says the same thing. -const installCmd = "bash <(curl -fsSL https://tracebloc.io/i.sh)" +const installCmd = "bash <(curl -fsSL " + installerURL + ")" // doctorRunFn is a test seam over doctor.Run (the cluster-side probe). Tests // inject a fixed []doctor.Result so the roll-up + render can be exercised with a diff --git a/internal/cli/prepare_host.go b/internal/cli/prepare_host.go new file mode 100644 index 0000000..6f1757f --- /dev/null +++ b/internal/cli/prepare_host.go @@ -0,0 +1,200 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "runtime" + "strings" + "time" + + "github.com/spf13/cobra" +) + +// prepareHostUserRe validates the researcher username before we pass it to the +// installer as TB_PREPARE_USER. Conservative Linux-username shape: starts +// alphanumeric, then letters/digits/._- (usermod quotes it, but reject nonsense +// early with a clear error rather than a confusing failure deep in the installer). +var prepareHostUserRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$`) + +// prepareHostInstallerCmd runs the official installer's admin-only prepare-host +// step. Like `tracebloc upgrade`, this deliberately delegates to the verified +// installer (cosign-checked) instead of re-implementing any privileged host prep +// in the CLI — the privileged surface stays in one audited place. +// +// We download the installer to a temp file and run THAT, rather than +// `curl | bash -s`. Two reasons, both Bugbot #394: +// - stdin: with `curl | bash -s`, the inner bash reads its *program* from the +// pipe, so the installer's stdin is no longer the terminal. Any interactive +// prompt in prepare-host (e.g. which non-admin user gets runtime access) +// would get EOF. Running a downloaded file leaves stdin on the TTY. +// - fail-closed: `set -e` + `curl -o` makes a failed download (network/DNS/HTTP +// error) abort with a non-zero status instead of silently running nothing. +// (`curl | bash` swallowed this — bash read empty stdin and exited 0.) +// +// The temp file is removed on exit. The URL comes from installerURL (doctor.go) +// so the automated download can't drift from the manual hint / other bootstrap +// copy (Bugbot #394). +const prepareHostInstallerCmd = `set -e +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT +curl -fsSL ` + installerURL + ` -o "$tmp" +bash "$tmp" prepare-host` + +// prepareHostManualHint is the copy-pasteable command we show if the automated +// run fails. Built from installCmd (doctor.go) — the single shared bootstrap +// idiom — so a URL/idiom change updates every hint at once (Bugbot #394); we +// only append the prepare-host subcommand. installCmd uses process substitution +// (bash <(curl …)), which keeps stdin on the terminal for interactive prompts. +// When a researcher username was given we prefix TB_PREPARE_USER= so a +// copy-pasted retry still grants access — otherwise the manual fallback would +// silently do less than the original request (Bugbot #394). +func prepareHostManualHint(user string) string { + if user != "" { + return "TB_PREPARE_USER=" + user + " " + installCmd + " prepare-host" + } + return installCmd + " prepare-host" +} + +// prepareHostEnv is the child's environment: the parent's, but with any ambient +// TB_PREPARE_USER stripped, then set to user only when a username was given. +// Stripping matters — the no-username path promises it grants no access, so a +// pre-set TB_PREPARE_USER in the admin's shell must not silently make the +// installer grant it anyway (Bugbot #394). +func prepareHostEnv(user string) []string { + parent := os.Environ() + env := make([]string, 0, len(parent)+1) + for _, kv := range parent { + if strings.HasPrefix(kv, "TB_PREPARE_USER=") { + continue + } + env = append(env, kv) + } + if user != "" { + env = append(env, "TB_PREPARE_USER="+user) + } + return env +} + +// prepareHostCmd builds the exec.Cmd that runs the installer. +// +// It deliberately does NOT put the installer in its own process group. The +// installer is interactive (prepare-host may prompt, e.g. which non-admin user +// gets runtime access), and stdin is the TTY — a child in a *background* process +// group that reads the terminal gets SIGTTIN and hangs (Bugbot #394). Staying in +// the CLI's foreground group means prompts work AND a terminal Ctrl-C delivers +// SIGINT to the whole pipeline (the `bash -c`, the `curl`, and the `bash "$tmp"` +// prepare-host child) in one go — no orphaned privileged work. +// +// WaitDelay bounds teardown on a *programmatic* cancel (parent shutdown / a +// SIGTERM to the CLI alone): CommandContext SIGKILLs the process and, after the +// delay, force-closes the I/O pipes so Wait can't block forever behind a child +// that traps signals. We rely on the default SIGKILL rather than a custom +// SIGINT-only Cancel (which a privileged child could ignore, hanging Wait). +func prepareHostCmd(ctx context.Context) *exec.Cmd { + c := exec.CommandContext(ctx, "bash", "-c", prepareHostInstallerCmd) + c.WaitDelay = 5 * time.Second + return c +} + +// prepareHostInterrupted reports whether the installer run ended because the user +// aborted, so the caller can exit quietly (130) instead of framing it as a failed +// install. ctx.Err() catches a cancel the signal handler already propagated — but +// on a terminal Ctrl-C the child can die and c.Run() can return BEFORE +// NotifyContext flips ctx.Err() (a race), so also treat bash's 130 (128+SIGINT) +// exit as an interrupt (Bugbot #394). +func prepareHostInterrupted(ctx context.Context, runErr error) bool { + if ctx.Err() != nil { + return true + } + var ee *exec.ExitError + if errors.As(runErr, &ee) { + return ee.ExitCode() == exitInterrupted + } + return false +} + +// prepareHostUnsupportedOnOS reports whether prepare-host can't run on this OS. +// The step readies a Linux server / HPC login node (container runtime, docker +// group) — a Unix-only concept — and shells out to bash/curl/mktemp. On Windows +// that would fail with a cryptic missing-bash error and a Unix-only retry hint, +// so we stop early with a clear message instead (mirrors upgrade's Windows +// handling; Bugbot #394). +func prepareHostUnsupportedOnOS(goos string) bool { return goos == "windows" } + +// newPrepareHostCmd builds `tracebloc prepare-host` — the one-time administrator +// step that readies a machine so a non-admin user can then install tracebloc +// with no root at all. +func newPrepareHostCmd() *cobra.Command { + return &cobra.Command{ + Use: "prepare-host [researcher-username]", + Short: "Prepare this machine so a non-admin user can install tracebloc (run once, as an administrator)", + Long: `Prepares a host that a non-admin user can't install on directly. + +Run this ONCE, as an administrator, on a machine where the person who will use +tracebloc has no root or sudo — a shared server, an HPC login node. It installs +the container runtime and its prerequisites. + +Pass that person's username to also grant them container-runtime (docker-group) +access, so they can then install tracebloc at Tier 0 with no administrator +rights at all: + + sudo tracebloc prepare-host alice + +Without a username it installs only the runtime + prerequisites and tells you +how to grant a user access afterwards. NOTE: the username is the RESEARCHER who +will use tracebloc — not you, the admin running this. + +It re-runs the official installer's prepare-host step (verified with cosign). It +does NOT create your secure environment or sign you in — it only prepares the +host, so it's safe to run on a shared machine. Safe to re-run.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + p := printerFor(cmd) + p.Newline() + if prepareHostUnsupportedOnOS(runtime.GOOS) { + // Discoverable in --help everywhere, but a no-op-with-explanation + // here rather than a cryptic missing-bash failure (Bugbot #394). + p.Para("prepare-host readies a Linux server or HPC login node so a non-admin user can install tracebloc without root — it doesn't apply to Windows. Run it as an administrator on the Unix host the researcher will use.") + p.Newline() + return nil + } + ctx := cmd.Context() + c := prepareHostCmd(ctx) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + + user := "" + if len(args) == 1 { + user = args[0] + if !prepareHostUserRe.MatchString(user) { + return &exitError{code: exitBadInput, err: fmt.Errorf("invalid username %q — expected a Linux username (letters, digits, '.', '_', '-')", user)} + } + } + // The installer reads TB_PREPARE_USER to pick who gets docker-group + // access. Pass it through the environment (not the command string) so + // it can't be shell-interpreted; the installer quotes it for usermod. + // prepareHostEnv also strips any ambient TB_PREPARE_USER so the + // no-username path never grants access it says it won't. + c.Env = prepareHostEnv(user) + if user != "" { + p.Para(fmt.Sprintf("Preparing this host and granting %s container-runtime access — re-running the installer's prepare-host step (needs administrator rights once).", user)) + } else { + p.Para("Preparing this host — re-running the installer's prepare-host step (installs the container runtime and prerequisites; needs administrator rights once). Pass a researcher's username to also grant them access: tracebloc prepare-host ") + } + p.Newline() + if err := c.Run(); err != nil { + // User aborted (Ctrl-C) or the parent context was cancelled: exit + // quietly with 130 like the other cancellable paths, not a scary + // "prepare-host didn't complete — retry" (Bugbot #394). + if prepareHostInterrupted(ctx, err) { + return &exitError{code: exitInterrupted} + } + return &exitError{code: exitFailure, err: fmt.Errorf("prepare-host didn't complete (%w). You can run the installer directly:\n %s", err, prepareHostManualHint(user))} + } + return nil + }, + } +} diff --git a/internal/cli/prepare_host_test.go b/internal/cli/prepare_host_test.go new file mode 100644 index 0000000..a8ee58e --- /dev/null +++ b/internal/cli/prepare_host_test.go @@ -0,0 +1,171 @@ +package cli + +import ( + "context" + "errors" + "os/exec" + "strings" + "testing" +) + +// A failed download must abort rather than run an empty script: with the old +// `curl | bash`, a curl failure left bash reading empty stdin and exiting 0, so +// the command reported success while prepare-host never ran. `set -e` + `curl +// -o ` makes the failure propagate — guard both against removal (Bugbot +// #394). +func TestPrepareHostCmdFailsClosedOnDownloadError(t *testing.T) { + if !strings.Contains(prepareHostInstallerCmd, "set -e") { + t.Fatalf("prepareHostInstallerCmd must `set -e` so a failed download aborts; got: %q", prepareHostInstallerCmd) + } + if !strings.Contains(prepareHostInstallerCmd, "curl") || !strings.Contains(prepareHostInstallerCmd, "-o ") { + t.Fatalf("prepareHostInstallerCmd must download the installer to a file (curl -o) so curl's exit is checked; got: %q", prepareHostInstallerCmd) + } + if !strings.Contains(prepareHostInstallerCmd, "prepare-host") { + t.Fatalf("prepareHostInstallerCmd should invoke the installer's prepare-host step; got: %q", prepareHostInstallerCmd) + } +} + +// The installer must NOT be fed to bash over a pipe: `curl | bash -s` makes the +// inner bash read its program from the pipe, stealing the installer's stdin so +// any interactive prompt in prepare-host gets EOF. We download and run a file +// instead, leaving stdin on the TTY (Bugbot #394). +func TestPrepareHostCmdDoesNotPipeIntoBash(t *testing.T) { + if strings.Contains(prepareHostInstallerCmd, "| bash") || strings.Contains(prepareHostInstallerCmd, "|bash") { + t.Errorf("prepareHostInstallerCmd must not pipe the script into bash (steals the installer's stdin); got: %q", prepareHostInstallerCmd) + } +} + +// The installer must run in the CLI's foreground process group (NOT its own): +// it's interactive and stdin is the TTY, so a backgrounded group would get +// SIGTTIN and hang on any prompt. And WaitDelay must be positive so a child that +// traps signals can't hang Wait forever after a programmatic cancel (Bugbot +// #394). SysProcAttr==nil is portable (the field is *syscall.SysProcAttr on +// every OS), so this stays a single cross-platform test. +func TestPrepareHostCmdStaysInForegroundGroup(t *testing.T) { + c := prepareHostCmd(context.Background()) + if c.SysProcAttr != nil { + t.Error("prepareHostCmd must not set SysProcAttr — a separate/background process group breaks interactive TTY prompts (SIGTTIN)") + } + if c.WaitDelay <= 0 { + t.Error("prepareHostCmd must set a positive WaitDelay so Wait can't hang forever after a cancel") + } +} + +// A user abort must be detected as an interrupt even when NotifyContext hasn't +// flipped ctx.Err() yet — bash exits 130 on SIGINT and c.Run() can return first +// (Bugbot #394). A genuine failure (exit 1) must NOT be treated as an interrupt. +func TestPrepareHostInterrupted(t *testing.T) { + // Cancelled context → interrupt regardless of the run error. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if !prepareHostInterrupted(ctx, errors.New("boom")) { + t.Error("a cancelled context must be treated as an interrupt") + } + + if _, err := exec.LookPath("bash"); err != nil { + t.Skip("bash not available for the exit-code cases") + } + // Live context + bash exit 130 (128+SIGINT) → interrupt (the Ctrl-C race). + if err := exec.Command("bash", "-c", "exit 130").Run(); err == nil { + t.Fatal("expected a non-nil error from exit 130") + } else if !prepareHostInterrupted(context.Background(), err) { + t.Error("exit 130 with a live context must be treated as an interrupt") + } + // Live context + a normal failure (exit 1) → NOT an interrupt. + if err := exec.Command("bash", "-c", "exit 1").Run(); err == nil { + t.Fatal("expected a non-nil error from exit 1") + } else if prepareHostInterrupted(context.Background(), err) { + t.Error("exit 1 must NOT be treated as an interrupt") + } +} + +func TestPrepareHostCmdMetadata(t *testing.T) { + c := newPrepareHostCmd() + if !strings.HasPrefix(c.Use, "prepare-host") { + t.Errorf("Use = %q, want it to start with prepare-host", c.Use) + } + // MaximumNArgs(1): the optional researcher username. Zero or one arg is fine; + // two is rejected (Bugbot / Divya #377: name the researcher to grant access). + if err := c.Args(c, []string{}); err != nil { + t.Errorf("prepare-host must accept zero args: %v", err) + } + if err := c.Args(c, []string{"alice"}); err != nil { + t.Errorf("prepare-host must accept one username arg: %v", err) + } + if err := c.Args(c, []string{"alice", "bob"}); err == nil { + t.Error("prepare-host should reject more than one positional argument") + } +} + +// The researcher username is passed to the installer as TB_PREPARE_USER, so it +// must be validated: accept real Linux usernames, reject shell-metacharacter / +// empty / overlong input (Divya #377). +func TestPrepareHostUserValidation(t *testing.T) { + valid := []string{"alice", "bob123", "a.b_c-d", "R2D2", "svc_account"} + for _, u := range valid { + if !prepareHostUserRe.MatchString(u) { + t.Errorf("username %q should be valid", u) + } + } + invalid := []string{"", "-leading", ".dot", "has space", "semi;colon", "a/b", "$(whoami)", "a`b`", "toolong_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} + for _, u := range invalid { + if prepareHostUserRe.MatchString(u) { + t.Errorf("username %q should be rejected", u) + } + } +} + +// The no-username path promises it grants no access, so a pre-set ambient +// TB_PREPARE_USER must be stripped from the child env; the username path sets +// exactly that user (replacing any ambient value, not duplicating it) — Bugbot #394. +func TestPrepareHostEnv_StripsAmbientAndSetsUser(t *testing.T) { + t.Setenv("TB_PREPARE_USER", "ambient-attacker") + + for _, kv := range prepareHostEnv("") { + if strings.HasPrefix(kv, "TB_PREPARE_USER=") { + t.Errorf("no-username env must not carry TB_PREPARE_USER, got %q", kv) + } + } + + n, got := 0, "" + for _, kv := range prepareHostEnv("alice") { + if strings.HasPrefix(kv, "TB_PREPARE_USER=") { + n++ + got = strings.TrimPrefix(kv, "TB_PREPARE_USER=") + } + } + if n != 1 || got != "alice" { + t.Errorf("username env should carry exactly TB_PREPARE_USER=alice, got n=%d val=%q", n, got) + } +} + +// On failure after `prepare-host `, the manual retry must still grant +// access (carry TB_PREPARE_USER=) — otherwise a copy-pasted retry silently +// does less than the original request. The no-username hint carries no such var +// (Bugbot #394). +func TestPrepareHostManualHint_CarriesUser(t *testing.T) { + if h := prepareHostManualHint(""); strings.Contains(h, "TB_PREPARE_USER") { + t.Errorf("no-username hint must not set TB_PREPARE_USER: %q", h) + } + h := prepareHostManualHint("alice") + if !strings.Contains(h, "TB_PREPARE_USER=alice") { + t.Errorf("username hint must carry TB_PREPARE_USER=alice so the retry still grants access: %q", h) + } + if !strings.Contains(h, "prepare-host") { + t.Errorf("hint must invoke prepare-host: %q", h) + } +} + +// prepare-host shells out to bash/curl and readies a Unix host, so it must be +// guarded on Windows (a no-op-with-explanation, not a cryptic missing-bash +// failure) — mirrors upgrade's Windows handling (Bugbot #394). +func TestPrepareHostUnsupportedOnWindows(t *testing.T) { + if !prepareHostUnsupportedOnOS("windows") { + t.Error("prepare-host must be guarded on windows") + } + for _, goos := range []string{"linux", "darwin"} { + if prepareHostUnsupportedOnOS(goos) { + t.Errorf("prepare-host must run on %s", goos) + } + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index b2801ae..06e896c 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -104,6 +104,7 @@ Helm, no YAML, no kubectl needed.`, // `client` and NOT `client delete --uninstall`: one machine owns one client, // so this removes tracebloc from the host and avoids colliding with `data delete`. root.AddCommand(newDeleteCmd()) + root.AddCommand(newPrepareHostCmd()) // F1: apply an update (re-runs the verified installer). The update-check // nudge (update_check.go) and the 426 "too old" error both point here. root.AddCommand(newUpgradeCmd()) diff --git a/internal/cli/testdata/golden/00-home.golden b/internal/cli/testdata/golden/00-home.golden index 1d288e7..b0c0be2 100644 --- a/internal/cli/testdata/golden/00-home.golden +++ b/internal/cli/testdata/golden/00-home.golden @@ -236,19 +236,20 @@ Usage: tracebloc [command] Available Commands: - auth Inspect tracebloc authentication state - client Provision this machine's tracebloc client - cluster Inspect the cluster the CLI is currently targeting - completion Generate the autocompletion script for the specified shell - data Manage the datasets in your secure environment - delete Offboard this machine from tracebloc (revoke, uninstall, reclaim disk) - doctor Check your secure environment is connected and ready to run training - help Help about any command - login Sign in to tracebloc in your browser (device flow) - logout Sign out (revoke the token server-side and clear it locally) - resources Show how much of this machine tracebloc may use - upgrade Update tracebloc to the latest release - version Print the tracebloc CLI version, git SHA, and build date + auth Inspect tracebloc authentication state + client Provision this machine's tracebloc client + cluster Inspect the cluster the CLI is currently targeting + completion Generate the autocompletion script for the specified shell + data Manage the datasets in your secure environment + delete Offboard this machine from tracebloc (revoke, uninstall, reclaim disk) + doctor Check your secure environment is connected and ready to run training + help Help about any command + login Sign in to tracebloc in your browser (device flow) + logout Sign out (revoke the token server-side and clear it locally) + prepare-host Prepare this machine so a non-admin user can install tracebloc (run once, as an administrator) + resources Show how much of this machine tracebloc may use + upgrade Update tracebloc to the latest release + version Print the tracebloc CLI version, git SHA, and build date Flags: -h, --help help for tracebloc diff --git a/internal/cli/testdata/golden/12-prepare-host.golden b/internal/cli/testdata/golden/12-prepare-host.golden new file mode 100644 index 0000000..ec5fcda --- /dev/null +++ b/internal/cli/testdata/golden/12-prepare-host.golden @@ -0,0 +1,42 @@ +tb prepare-host — one-time admin step so a non-admin can install +================================================================ +What you see when you run `tb prepare-host` — the one-time administrator step +that readies a shared / HPC host so a non-admin user can then install tracebloc +with no root. It re-runs the installer's verified prepare-host step; the +privileged prep + its progress stream from the installer (not CLI copy). Only the +--help is byte-exact below. + + +------------------------------------------------------------ +--help +------------------------------------------------------------ +$ tracebloc prepare-host --help +Prepares a host that a non-admin user can't install on directly. + +Run this ONCE, as an administrator, on a machine where the person who will use +tracebloc has no root or sudo — a shared server, an HPC login node. It installs +the container runtime and its prerequisites. + +Pass that person's username to also grant them container-runtime (docker-group) +access, so they can then install tracebloc at Tier 0 with no administrator +rights at all: + + sudo tracebloc prepare-host alice + +Without a username it installs only the runtime + prerequisites and tells you +how to grant a user access afterwards. NOTE: the username is the RESEARCHER who +will use tracebloc — not you, the admin running this. + +It re-runs the official installer's prepare-host step (verified with cosign). It +does NOT create your secure environment or sign you in — it only prepares the +host, so it's safe to run on a shared machine. Safe to re-run. + +Usage: + tracebloc prepare-host [researcher-username] [flags] + +Flags: + -h, --help help for prepare-host + +Global Flags: + --plain disable color and decorative output (also honors $NO_COLOR) + --verbose stream detailed step-by-step progress (also via $TRACEBLOC_LOG_LEVEL=debug) diff --git a/internal/cli/testdata/golden/zz-all-strings.golden b/internal/cli/testdata/golden/zz-all-strings.golden index fe79319..50841a5 100644 --- a/internal/cli/testdata/golden/zz-all-strings.golden +++ b/internal/cli/testdata/golden/zz-all-strings.golden @@ -220,6 +220,8 @@ screen. %s/%d are runtime placeholders. "Pending > %s: %v" "Pick this dataset when you set it up." "Please name the dataset." +"Preparing this host and granting %s container-runtime access — re-running the installer's prepare-host step (needs administrator rights once)." +"Preparing this host — re-running the installer's prepare-host step (installs the container runtime and prerequisites; needs administrator rights once). Pass a researcher's username to also grant them access: tracebloc prepare-host " "Private image pulls will ImagePullBackOff. Reinstall the chart with valid registry credentials." "Proceed with the ingest?" "Provision this client?" @@ -416,6 +418,7 @@ screen. %s/%d are runtime placeholders. "interactive setup: %w" "internal: re-parsing synthesized spec: %w\n%s" "invalid table name %q: %w" +"invalid username %q — expected a Linux username (letters, digits, '.', '_', '-')" "jobs-manager" "jobs-manager %s returned HTTP %d: %s" "jobs-manager has no literal REQUESTS_PROXY_URL (chart too old, or it's set via a configMap/secret ref)" @@ -479,6 +482,8 @@ screen. %s/%d are runtime placeholders. "pod %s container %s restarted %d times" "port-forward allocated zero ports" "port-forward to %s/%s failed during startup: %w" +"prepare-host didn't complete (%w). You can run the installer directly:\n %s" +"prepare-host readies a Linux server or HPC login node so a non-admin user can install tracebloc without root — it doesn't apply to Windows. Run it as an administrator on the Unix host the researcher will use." "pvc path" "querying datasets: %w%s" "reading %q: %w"