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
80 changes: 68 additions & 12 deletions scripts/lib/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,73 @@ _prompt_tty() { [[ -r /dev/tty && -w /dev/tty ]]; }
# terminal. Overridable so tests can feed canned input on stdin (TB_TTY=/dev/stdin).
: "${TB_TTY:=/dev/tty}"

# _login_tty_ok: is /dev/tty openable for the sign-in's redirection? Split out
# as its own function (rather than reusing _prompt_tty) purely so tests can force
# either branch: _prompt_tty gates the PROMPTS below, and several tests force it
# on while having no real /dev/tty to redirect a child process onto.
_login_tty_ok() { { : </dev/tty; } 2>/dev/null; }

# _run_device_login: one `tracebloc login`, given the user's REAL terminal.
#
# This runs after setup_log_file (`exec > >(tee …) 2>&1`), so the shell's
# stdout/stderr are a pipe and — under `curl … | bash` — stdin is the install
# pipe; a bare `tracebloc login` would then have no tty on any stream and can
# misrender or fail (same class assess.sh's hand-off handles). Redirect all three
# to /dev/tty when openable, else </dev/null (unattended reaches the dual-mode
# credential path, not here).
#
# TRACEBLOC_INSTALLER tells the CLI it is not being run by hand, so it suppresses
# its own "run `tracebloc login`" recovery line: correct for someone who typed
# the command, wrong here, where a bare login would leave the client mint and the
# Helm install undone. _device_sign_in below owns the recovery advice (cli#517).
# It is a command prefix, not an export — no later CLI call should inherit it.
_run_device_login() {
if _login_tty_ok; then
TRACEBLOC_INSTALLER=1 tracebloc login </dev/tty >/dev/tty 2>/dev/tty
else
TRACEBLOC_INSTALLER=1 tracebloc login </dev/null
fi
}

# SIGN_IN_ATTEMPTS: how many times the device sign-in may run — the initial
# attempt plus ONE in-place retry. Declared once and used as the loop bound AND
# the "is there a next attempt?" test below, so the two can't drift into
# disagreeing about how many tries there are.
: "${SIGN_IN_ATTEMPTS:=2}"

# _device_sign_in: sign in, with ONE in-place retry for a fresh code.
#
# The device code has a hard ten-minute deadline and the human approving it is
# reading a phone — a lapsed code is an ordinary outcome, not an exceptional one.
# Before cli#517 it cost a full installer re-run: everything already done (the
# prerequisites, the cluster) was thrown away over a missed prompt. This mirrors
# the name prompt's retry loop further down, for the same reason — an interactive
# step that can fail for a benign reason must not abort a long install.
#
# Exactly one retry, and only with a live terminal:
# • a second failure is evidence of something other than a missed code, and a
# retry loop there would just hide it behind a prompt nobody can satisfy;
# • without a terminal there is nobody to hand a fresh code to, and a FAILED
# read (rc != 0 = EOF / no live input) can't be fixed by re-prompting either.
# Both fall through to the same fatal error, whose advice is the ONLY next step
# printed — the CLI's own is suppressed above.
_device_sign_in() {
local attempt
for (( attempt = 1; attempt <= SIGN_IN_ATTEMPTS; attempt++ )); do
_run_device_login && return 0
if [[ "$attempt" -ge "$SIGN_IN_ATTEMPTS" ]] || ! _prompt_tty; then break; fi
echo ""
warn "Sign-in didn't complete — the code lapsed or wasn't approved."
hint "Nothing is lost: the install is paused right here, not restarted."
# The prompt WRITE is guarded (|| true) so a test without a real /dev/tty
# doesn't abort; the read comes from TB_TTY, like every other prompt here.
printf '\n Press Enter for a fresh code (or Ctrl-C to stop): ' >/dev/tty 2>/dev/null || true
IFS= read -r _ <"$TB_TTY" || break
echo ""
done
error "Sign-in didn't complete — re-run the installer to try again."
}

# _detect_location_zone: best-effort ISO country code for where this machine
# physically runs, derived from the system timezone via the OS's OWN zone.tab —
# no network call (privacy-preserving for on-prem installs) and no embedded zone
Expand Down Expand Up @@ -164,18 +231,7 @@ provision_client() {
echo -e " Sign in to approve this machine — open the link in your browser"
echo -e " (on this or any device) and enter the code:"
echo ""
# Give the interactive device-flow sign-in the user's REAL terminal. This runs
# after setup_log_file (`exec > >(tee …) 2>&1`), so the shell's stdout/stderr are
# a pipe and — under `curl … | bash` — stdin is the install pipe; a bare
# `tracebloc login` would then have no tty on any stream and can misrender or
# fail (same class assess.sh's hand-off handles). Redirect all three to /dev/tty
# when openable, else </dev/null (unattended reaches the dual-mode credential
# path above, not here).
if { : </dev/tty; } 2>/dev/null; then
tracebloc login </dev/tty >/dev/tty 2>/dev/tty || error "Sign-in didn't complete — re-run the installer to try again."
else
tracebloc login </dev/null || error "Sign-in didn't complete — re-run the installer to try again."
fi
_device_sign_in

# ── One-client-per-machine pre-flight (#303) ─────────────────────────────
# `client create` below mints a fresh client whenever the backend can't match
Expand Down
2 changes: 1 addition & 1 deletion scripts/manifest.sha256
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ b84109f55930b555efc41b0088f9fe8e9741b3eb47e5e993e1fd32d52c5b0ea8 scripts/lib/se
270520b0f6a647da00d653794b0d1334ceffa3e76907af2051101ecb8eae48da scripts/lib/gpu-plugins.sh
a3d1c8a34e7f355d9d5a632c0cbf4b74044827790c02d41f9917d4554dd0cb58 scripts/lib/install-client-helm.sh
61c1c887d158af52d4da4734b3bfa83205b2600ae7a291bfb3074daf3d9ffb55 scripts/lib/install-cli.sh
b0bf0a4966461e4257b6765dc4f12877762eee1e72f60ed4b2e322cb6291315a scripts/lib/provision.sh
ea2bbd9948ee9e31e93271e235c630ced50d746e51a5629b5622041d8a39df07 scripts/lib/provision.sh
8bd0deb458e7649723b722d28022018eeeff318068fd7c166756cbdcc3d65806 scripts/lib/assess.sh
911fd0714b17357bb205fc8a8fa8e13eedc1a9632a2f63d4ead9f8d8c7ee546f scripts/lib/probe.sh
a9bb43caa3e7d156d48e7f9f4473c22da0b3b42c25d84fd49f35349b25956132 scripts/lib/summary.sh
Expand Down
103 changes: 103 additions & 0 deletions scripts/tests/provision.bats
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,109 @@ _stub_tracebloc() {
[[ "$output" == *"Sign-in didn't complete"* ]] || return 1
}

# ── cli#517: a missed code costs one prompt, not a whole installer run ────────
#
# The retry loop is driven through _run_device_login (the seam that owns the
# /dev/tty redirection): stubbing it lets these tests decide success or failure
# per attempt without needing a real controlling terminal, which CI has not got.
# The seam's OWN behaviour — which env it hands the CLI — is pinned separately
# below, on the non-tty branch, so it behaves the same in CI and on a laptop.

# _stub_sign_in_failing: a _run_device_login stub that records every attempt in
# ATTEMPTS_FILE and fails the first $1 of them (0 = succeed immediately). The
# count lives in a GLOBAL, not a local of this function: the stub runs long after
# this returns, so a local would be out of scope by then and read as empty.
_stub_sign_in_failing() {
SIGN_IN_FAIL_FIRST="$1"
ATTEMPTS_FILE="$(mktemp)"
_run_device_login() {
printf 'x\n' >>"$ATTEMPTS_FILE"
[[ "$(grep -c . "$ATTEMPTS_FILE")" -le "$SIGN_IN_FAIL_FIRST" ]] && return 1
return 0
}
}

_attempts() { grep -c . "$ATTEMPTS_FILE" 2>/dev/null || echo 0; }

@test "_device_sign_in: a lapsed code is retried in place, not fatal" {
# THE cli#517 fix: before it, one missed ten-minute code threw away every step
# the installer had already completed. It must cost a single Enter instead.
_prompt_tty() { return 0; } # a live terminal is available
TB_TTY="$(mktemp)"; printf '\n' >"$TB_TTY" # the human presses Enter
_stub_sign_in_failing 1
run _device_sign_in
[ "$status" -eq 0 ] || return 1
[ "$(_attempts)" -eq 2 ] || return 1
# The "Press Enter" line goes to /dev/tty (like every prompt here), so it is
# not in $output; the warn/hint that explain the pause do go through the log.
[[ "$output" == *"the code lapsed or wasn't approved"* ]] || return 1
[[ "$output" == *"Nothing is lost"* ]] || return 1
}

@test "_device_sign_in: a second failure is fatal, and names the installer" {
# The retry is ONE retry. A second failure is evidence of something other than
# a missed code, and the advice must be the installer — not `tracebloc login`,
# which would leave the client mint and the Helm install undone.
_prompt_tty() { return 0; }
TB_TTY="$(mktemp)"; printf '\n\n\n' >"$TB_TTY"
_stub_sign_in_failing 99
run _device_sign_in
[ "$status" -ne 0 ] || return 1
[ "$(_attempts)" -eq 2 ] || return 1
[[ "$output" == *"re-run the installer"* ]] || return 1
[[ "$output" != *"tracebloc login"* ]] || return 1
# Offered ONCE. The attempt count alone can't see a guard that lets the loop
# prompt after its final try — the user would be asked to press Enter for a
# code that is never fetched — so count the offer, not just the attempts.
[ "$(printf '%s\n' "$output" | grep -c 'Nothing is lost')" -eq 1 ] || return 1
}

@test "_device_sign_in: with no terminal there is no retry to offer" {
# Nobody to hand a fresh code to — re-prompting would just hang or spin.
# TB_TTY is deliberately READABLE here: with a dead one, dropping the
# _prompt_tty gate would stop on the failed read instead and the assertion
# below could not tell the gate from the EOF (the mutation ran green that way).
_prompt_tty() { return 1; }
TB_TTY="$(mktemp)"; printf '\n\n\n' >"$TB_TTY"
_stub_sign_in_failing 99
run _device_sign_in
[ "$status" -ne 0 ] || return 1
[ "$(_attempts)" -eq 1 ] || return 1
[[ "$output" != *"Nothing is lost"* ]] || return 1
}

@test "_device_sign_in: an EOF on the retry prompt stops, it does not spin" {
# A failed read (rc != 0 — EOF, a non-PTY ssh session, a closed pipe) can't be
# fixed by asking again; the loop must fall straight through to the error.
_prompt_tty() { return 0; }
TB_TTY=/dev/null # read returns EOF immediately
_stub_sign_in_failing 99
run _device_sign_in
[ "$status" -ne 0 ] || return 1
[ "$(_attempts)" -eq 1 ] || return 1
[[ "$output" == *"re-run the installer"* ]] || return 1
}

@test "_run_device_login: the CLI is told the installer is driving it" {
# TRACEBLOC_INSTALLER is what stops the CLI printing "run \`tracebloc login\`"
# — advice that is right for a hand-typed login and wrong under the installer.
# Pinned on the no-tty branch so it reads the same in CI and on a laptop.
_login_tty_ok() { return 1; }
local seen="$BATS_TEST_TMPDIR/env"
tracebloc() { printf '%s|%s\n' "${TRACEBLOC_INSTALLER:-unset}" "$*" >"$seen"; return 0; }
_run_device_login
[[ "$(cat "$seen")" == "1|login" ]] || return 1
}

@test "_run_device_login: TRACEBLOC_INSTALLER does not leak past the sign-in" {
# Set as a command prefix, not exported for the rest of the install: `client
# create` and every later CLI call must see the environment they always did.
_login_tty_ok() { return 1; }
tracebloc() { return 0; }
_run_device_login
[ -z "${TRACEBLOC_INSTALLER:-}" ] || return 1
}

@test "provision_client: client create writing no credential file is fatal" {
tracebloc() { return 0; } # login OK, create "succeeds" but writes nothing
run provision_client
Expand Down
Loading