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
47 changes: 30 additions & 17 deletions scripts/lib/install-client-helm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ _extract_yaml_value() {
_strip_paste_garbage "$line"
}

# detect_installed_client — report the tracebloc client already installed on this
# cluster, if any, via the globals INSTALLED_CLIENT_ID / INSTALLED_CLIENT_NS
# (both empty when none is found). Enumerate client-chart releases across ALL
# namespaces WITHOUT jq (not a guaranteed prerequisite): helm's NAME/NAMESPACE are
# the first two whitespace-free columns and the CHART column matches
# `client-<ver>`, the same jq-free parse _chart_version uses. Shared by the
# pre-provision ownership pre-flight (#303) and the Helm-step one-client guard so
# the two can never disagree on "what already runs here". Always returns 0 — a
# missing helm / unreadable values just yields the empty (no-client) result.
detect_installed_client() {
INSTALLED_CLIENT_ID=""; INSTALLED_CLIENT_NS=""
local _gvf _rel _ns _id
_gvf="$(mktemp)" || return 0
while read -r _rel _ns; do
[[ -z "$_rel" ]] && continue
if helm get values "$_rel" -n "$_ns" > "$_gvf" 2>/dev/null; then
_id="$(_extract_yaml_value "$_gvf" clientId)"
[[ -n "$_id" ]] && { INSTALLED_CLIENT_ID="$_id"; INSTALLED_CLIENT_NS="$_ns"; break; }
fi
done < <(helm list -A 2>/dev/null | awk '/[[:space:]]client-[0-9]/ { print $1, $2 }')
rm -f "$_gvf"
return 0
}

# Strip ANSI escape sequences and C0 control characters from a value.
# `read -r -s` captures whatever the terminal sends — this can include:
# • bracketed-paste wrappers: ESC[200~ ... ESC[201~
Expand Down Expand Up @@ -430,23 +454,12 @@ install_client_helm() {
# would silently re-point the machine — so we stop and let the operator
# decide. The same clientId is a normal re-run/upgrade and passes through.
# Check ANY namespace: a fresh install lands in `tracebloc`, but an install
# from an older installer version may be in a different namespace. Enumerate
# client-chart releases WITHOUT jq — jq is not a guaranteed prerequisite here,
# and a jq-only enumeration whose fallback checked a single namespace would miss
# an older release under the fixed `tracebloc` namespace once the minted slug
# differs, forking a second release. helm's NAME/NAMESPACE are the first two
# columns and never contain whitespace, and the CHART column matches
# `client-<ver>` — the same jq-free parse _chart_version uses.
local existing_id="" existing_ns="" _gvf _rel _ns _id
_gvf="$(mktemp)"
while read -r _rel _ns; do
[[ -z "$_rel" ]] && continue
if helm get values "$_rel" -n "$_ns" > "$_gvf" 2>/dev/null; then
_id="$(_extract_yaml_value "$_gvf" clientId)"
[[ -n "$_id" ]] && { existing_id="$_id"; existing_ns="$_ns"; break; }
fi
done < <(helm list -A 2>/dev/null | awk '/[[:space:]]client-[0-9]/ { print $1, $2 }')
rm -f "$_gvf"
# from an older installer version may be in a different namespace. The jq-free
# enumeration lives in detect_installed_client (shared with the #303 pre-provision
# pre-flight so the two can't disagree on what's installed here).
local existing_id="" existing_ns=""
detect_installed_client
existing_id="$INSTALLED_CLIENT_ID"; existing_ns="$INSTALLED_CLIENT_NS"
if [[ -n "$existing_id" && "$existing_id" != "$TB_CLIENT_ID" ]]; then
echo ""
warn "This machine already runs the tracebloc client '${existing_id}' (namespace '${existing_ns}')."
Expand Down
61 changes: 61 additions & 0 deletions scripts/lib/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ _cli_supports_provisioning() {
# out as a function so tests can force the non-interactive path deterministically.
_prompt_tty() { [[ -r /dev/tty && -w /dev/tty ]]; }

# _account_owns_namespace NS — does the signed-in account's client list include a
# client whose namespace is NS? `client list` prints "…namespace=<ns> location=…";
# match that field exactly. Namespace is the only stable join key between a local
# Helm release (which stores the UUID clientId + namespace) and the list (which
# shows the numeric dashboard id + namespace) — the two don't share an id.
# Returns: 0 = owned, 1 = list read OK but NS absent, 2 = couldn't read the list.
# Split out so the pre-flight can distinguish "not yours" (refuse) from
# "couldn't tell" (fall through to `client create`'s own idempotent logic).
_account_owns_namespace() {
local ns="$1" out
[[ -n "$ns" ]] || return 1
out="$(tracebloc client list --plain 2>/dev/null)" || return 2
grep -Eq "namespace=${ns}([[:space:]]|$)" <<<"$out"
}

provision_client() {
step 3 5 "Sign in and provision this client"

Expand Down Expand Up @@ -79,6 +94,52 @@ provision_client() {
info "Sign in to tracebloc — approve this machine in your browser when prompted…"
tracebloc login || error "Sign-in didn't complete — re-run the installer to try again."

# ── One-client-per-machine pre-flight (#303) ─────────────────────────────
# `client create` below mints a fresh client whenever the backend can't match
# THIS cluster to a client in the signed-in account — e.g. a pre-anchor client
# whose cluster_id is still null, or one owned by a DIFFERENT account. If a
# client is already installed here and the signed-in account doesn't own it,
# that mint registers a brand-new client that never installs (the Helm-step
# guard then refuses) — an orphan left on the dashboard. Catch it BEFORE minting.
#
# Only fires when a local release exists AND we can positively confirm the
# account does NOT own it. A fresh machine (no release), an account that DOES
# own the local client (create cleanly adopts), or an inconclusive list read all
# fall through to `client create`'s own idempotent adopt/conflict handling — so
# this never regresses the same-account re-run/upgrade path. Guarded on the
# shared probe being present (a stale bootstrap may not have sourced it).
if declare -F detect_installed_client >/dev/null 2>&1; then
detect_installed_client
if [[ -n "$INSTALLED_CLIENT_NS" ]]; then
local _own_rc=0
_account_owns_namespace "$INSTALLED_CLIENT_NS" || _own_rc=$?
# rc 1 = list read OK but the installed client's namespace is absent from the
# account. That's only proof of a FOREIGN client for a slug namespace. A
# client installed under the legacy fixed `tracebloc` namespace is listed on
# the dashboard under its minted slug (§838), so `client list` won't show
# `tracebloc` even for the account's OWN older client — a skew
# install_client_helm reconciles by clientId, the reliable key `client list`
# doesn't expose here. So refuse only for a non-legacy namespace; for
# `tracebloc`, defer to `client create` (adopts if it's yours, 409s if truly
# cross-account) and the Helm-step one-client guard, which key on clientId.
if [[ "$_own_rc" -eq 1 && "$INSTALLED_CLIENT_NS" != "tracebloc" ]]; then
echo ""
warn "This machine already runs a tracebloc client (namespace '${INSTALLED_CLIENT_NS}') that isn't in the account you just signed in as."
hint "tracebloc runs one client per machine. Provisioning now would register a"
hint "second client and strand it (it could never install here). Pick one:"
hint " • Repair / update it → sign in as the account that owns it, or re-run with that client's credentials"
hint " • Switch to this account → remove the current client first:"
hint " k3d cluster delete ${CLUSTER_NAME:-tracebloc} (wipes this client + its local data)"
hint " then re-run this installer"
hint " • Run both → install on a separate machine"
echo ""
error "Refusing to provision a second client on this machine. See the options above."
Comment thread
cursor[bot] marked this conversation as resolved.
elif [[ "$_own_rc" -eq 1 ]]; then
log "installed client is in the legacy 'tracebloc' namespace (not listed by its slug); deferring ownership to client create + the Helm one-client guard, which key on clientId"
fi
fi
fi

# Mint the client + derive the namespace. --credential-file writes the secret to
# a 0600 file (never printed); we source it, hand it to Helm, then delete it (the
# secret's durable home is the Helm/cluster secret — RFC §7.9).
Expand Down
4 changes: 2 additions & 2 deletions scripts/manifest.sha256
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ fd8d37d698c242ab081095625aa430bb91971db646e35393a02bbadd41325898 scripts/lib/gp
2e64a2c47bdd14c861f8b5e86b06d1dfe4eb349ea6f76d9d22ecf3bb1ec12e04 scripts/lib/setup-linux.sh
dd2e1f517b0f6bb665eacebfb7885c6a81443f69c2ebc772433a6baed53c7a8e scripts/lib/cluster.sh
5de1afac8b1062a7f8d0691b46ad39ccdc2287e0e422b53c48628ecfd47d852e scripts/lib/gpu-plugins.sh
24ebf0946aa7e2b63a072e99adfc56acee822fdc5677ac03d4cf7158c252732c scripts/lib/install-client-helm.sh
3e5bc6dac689f269a9ba06e1be5a831465ff74e8d78465ec7f893ea097a5b785 scripts/lib/install-client-helm.sh
f2350fbafbc5ad22d7a87ec55c83b5138e2d039a6eb5580bb8410fd01b3536e2 scripts/lib/install-cli.sh
34a92ac436b4d3590c1aca7c0b0bda2aa6f8b490d451b7b096d8a3cf98b8780c scripts/lib/provision.sh
63ac8ab924a453f2ae0840c2087a97d7c592c8083260a162c6c51322df96876e scripts/lib/provision.sh
58bd83c06ca16e1d9346700a3f6bcafc67f20130f0bc1f160d2faedab19fd2cf scripts/lib/summary.sh
f22b3f57722feaf3a05a8527988c4404c27b772dd8ba8d6d4c6fe08077081a4a scripts/lib/diagnose.sh
8811bd7086b7fffc6a7b158c3261a2e9659bb24490da69d402694f3680fbc2f1 scripts/install-k8s.ps1
84 changes: 84 additions & 0 deletions scripts/tests/provision.bats
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ setup() {
# error() is the real common.sh one (prints + exit 1) — fatal tests assert status.
has() { return 0; } # default: CLI present after install
install_tracebloc_cli() { :; } # stubbed — covered by install-cli.bats
# The #303 pre-flight probes local Helm via detect_installed_client (defined in
# install-client-helm.sh, which this suite doesn't source). Default it to
# "nothing installed here" so the pre-flight is skipped; the #303 tests override.
detect_installed_client() { INSTALLED_CLIENT_ID=""; INSTALLED_CLIENT_NS=""; }
LOG_FILE="$(mktemp)"
HOST_DATA_DIR="$(mktemp -d)"
unset TRACEBLOC_VALUES_FILE TRACEBLOC_CLIENT_ID TRACEBLOC_CLIENT_PASSWORD \
Expand Down Expand Up @@ -156,6 +160,86 @@ _stub_tracebloc() {
[[ "$output" == *"--location DE"* ]]
}

@test "provision_client: refuses BEFORE minting when a foreign client already runs here (#303)" {
# A client is installed locally under a namespace the signed-in account does NOT
# own — the orphan scenario. provision_client must refuse before `client create`.
detect_installed_client() { INSTALLED_CLIENT_ID="uuid-x"; INSTALLED_CLIENT_NS="tracebloc-amazon"; }
tracebloc() {
[[ "$*" == *--help ]] && return 0 # capability probe
[ "$1" = "login" ] && return 0
if [ "$1" = "client" ] && [ "$2" = "list" ]; then
echo "box state=online namespace=some-other-ns location=DE" # NOT tracebloc-amazon
return 0
fi
# any `client create` records its argv so we can assert it never ran
local f="" prev=""; for a in "$@"; do [ "$prev" = "--credential-file" ] && f="$a"; prev="$a"; done
[ -n "$f" ] && printf '%s\n' "$*" >>"${CREATE_ARGS_FILE}"
return 0
}
run provision_client
[ "$status" -ne 0 ]
[[ "$output" == *"isn't in the account you just signed in as"* ]]
[[ "$output" == *"Refusing to provision a second client"* ]]
[ ! -s "$CREATE_ARGS_FILE" ] # create was never called — no orphan minted
}

@test "provision_client: same-account re-run with a local client still provisions (adopt path intact) (#303)" {
# The account owns the local client's namespace → create proceeds and adopts.
detect_installed_client() { INSTALLED_CLIENT_ID="uuid-x"; INSTALLED_CLIENT_NS="my-ns"; }
tracebloc() {
[[ "$*" == *--help ]] && return 0
[ "$1" = "login" ] && return 0
if [ "$1" = "client" ] && [ "$2" = "list" ]; then
echo "box state=online namespace=my-ns location=DE" # owned
return 0
fi
local f="" prev=""; for a in "$@"; do [ "$prev" = "--credential-file" ] && f="$a"; prev="$a"; done
[ -n "$f" ] && printf '%b' 'TRACEBLOC_CLIENT_ID=8\nTB_NAMESPACE=my-ns\nTRACEBLOC_CLIENT_ADOPTED=1\n' > "$f"
return 0
}
provision_client
[ "$TB_NAMESPACE" = "my-ns" ]
[ "$TRACEBLOC_CLIENT_ADOPTED" = "1" ] # adopt path reached, not refused
}

@test "provision_client: an unreadable client list falls through to create, not a refusal (#303)" {
# Ownership inconclusive (list read fails) must NOT block — degrade to create's
# own idempotent adopt/conflict handling, no worse than before the pre-flight.
detect_installed_client() { INSTALLED_CLIENT_ID="uuid-x"; INSTALLED_CLIENT_NS="tracebloc-amazon"; }
tracebloc() {
[[ "$*" == *--help ]] && return 0
[ "$1" = "login" ] && return 0
if [ "$1" = "client" ] && [ "$2" = "list" ]; then return 5; fi # list read fails
local f="" prev=""; for a in "$@"; do [ "$prev" = "--credential-file" ] && f="$a"; prev="$a"; done
[ -n "$f" ] && printf '%b' 'TRACEBLOC_CLIENT_ID=9\nTRACEBLOC_CLIENT_PASSWORD=pw\nTB_NAMESPACE=fresh-ns\n' > "$f"
return 0
}
provision_client
[ "$TRACEBLOC_CLIENT_ID" = "9" ] # create ran despite the inconclusive list
[ "$TB_NAMESPACE" = "fresh-ns" ]
}

@test "provision_client: legacy 'tracebloc' namespace absent from the account is NOT refused — defers to create + guard (#306 Bugbot)" {
# A same-account client installed under the legacy fixed `tracebloc` namespace is
# listed on the dashboard by its minted slug, so `client list` won't show
# `tracebloc`. The old namespace-only check wrongly refused this (blocking a valid
# re-run install_client_helm reconciles by clientId). Must defer to create, not refuse.
detect_installed_client() { INSTALLED_CLIENT_ID="uuid-x"; INSTALLED_CLIENT_NS="tracebloc"; }
tracebloc() {
[[ "$*" == *--help ]] && return 0
[ "$1" = "login" ] && return 0
if [ "$1" = "client" ] && [ "$2" = "list" ]; then
echo "box state=online namespace=slug-ns location=DE" # listed by slug, not 'tracebloc'
return 0
fi
local f="" prev=""; for a in "$@"; do [ "$prev" = "--credential-file" ] && f="$a"; prev="$a"; done
[ -n "$f" ] && printf '%b' 'TRACEBLOC_CLIENT_ID=9\nTRACEBLOC_CLIENT_PASSWORD=pw\nTB_NAMESPACE=slug-ns\n' > "$f"
return 0
}
provision_client # must NOT refuse (a direct call fails the test if error exits)
[ "$TRACEBLOC_CLIENT_ID" = "9" ] # deferred to create, which ran
}

@test "provision_client: no name and no TTY to prompt is fatal (can't provision blind)" {
unset TRACEBLOC_CLIENT_NAME
_prompt_tty() { return 1; } # non-interactive: no terminal to prompt on
Expand Down
Loading