Skip to content

fix(installer): harden the Windows install→first-experiment path — checksum-driven downloads, reliable cluster-create, writable ingest & training volumes, local-chart support (#611) - #612

Merged
shujaatTracebloc merged 11 commits into
developfrom
fix/609-checksum-driven-download
Aug 6, 2026

Conversation

@shujaatTracebloc

@shujaatTracebloc shujaatTracebloc commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What & why

Hardens the Windows install → first-dataset-ingest → first-experiment path end to end, driven by real field failures on a Windows machine behind a filtering corporate/AV network. Follow-on to the resilient multi-transport download landed in #607/#608. Each fix below is a place a real user got stuck; together they take the flow from "dies at step 2" to "installs, provisions, ingests, and runs an experiment" on a hostPath (Windows/WSL2) install.

Closes #611. Contributes to #578.

Fixes

1. Checksum-driven tool download (k3d / kubectl / helm) — install-k8s.ps1

#607's multi-transport download validated by size + magic bytes only, so a binary truncated mid-transfer to between the size floor and the real size still passed and dead-ended at a no-retry checksum step. Now the checksum is the completeness test:

  • Get-VerifiedDownload -Sha256 — after a transport lands a size/magic-valid file, its SHA-256 must match or the next transport (curl.exe → BITS) is tried. A truncated/altered copy self-heals.
  • The checksum-list files (checksums.txt, *.sha256) are fetched first and gated on a hash-anchored -MatchPattern (a 64-hex hash adjacent to the asset / at the start of the body), so a proxy error page echoing the URL can't fail the gate open (Bugbot).
  • helm gains checksum verification on the PS path for the first time (parity with bash).
  • Field symptom fixed: System tool checksum verification failed on a correct-but-truncated k3d download.

2. Reliable cluster-create — install-k8s.ps1

Wait-ProcessWithDeadline polled HasExited but never called WaitForExit(), so with Start-Process -RedirectStandardOutput the k3d process's ExitCode came back null and $null -ne 0 misread a successful create as a failure. Now it calls WaitForExit() before returning (reliable exit code for every caller), plus a defense-in-depth fallback that trusts k3d's created successfully marker — checking both stdout and stderr (k3d logs success via logrus to stderr) (Bugbot).

  • Field symptom fixed: Failed to create compute environment even though the cluster was up.

3. Writable hostPath volumes for ingest and training/inference — chart (jobs-manager-deployment.yaml)

Two permission failures, same root cause. On hostPath installs kubelet doesn't apply fsGroup (kubernetes/kubernetes#138411), so a hostPath dir is created root-owned and the non-root pods that mount it can't write:

  • Ingest: tb data ingest failed at the copy step — mkdir: can't create directory '/data/shared/.tracebloc-staging/': Permission denied.
  • Training / inference: a spawned pod failed with PermissionError [Errno 13]: '/data/logs/<run>' while creating its per-run log dir (os.makedirs).

mysql-data already had a privileged init-chown; the shared and logs volumes had none. Now, on hostPath only, jobs-manager runs a privileged init-writable-data (root, caps CHOWN+FOWNER+FSETID) that makes both /data/shared and /data/logs chmod 3777 — world-writable (multiple non-root writers whose UIDs the chart doesn't control: jobs-manager, the spawned training/inference pods, the CLI ingest-staging pod) + setgid (GID-1000 inheritance; FSETID is required or the kernel strips setgid after the chown) + sticky (one writer can't unlink another's files). The spawned pods mount the same hostPath dirs, so fixing them once reaches them too. Training and inference share one pod spec (client-runtime job.yaml), so /data/logs covers both; every other write path there is an emptyDir or read-only. Each dir is fixed independently and best-effort, so a chown that can't complete (NFS root_squash) doesn't wedge the edge. No fsGroup is set — it's a no-op on hostPath and would strip the spawned pods' group-0 access on CSI; CSI is untouched (matches develop) and its pods keep their documented posture (docs/SECURITY.md §5.3).

4. Windows local-chart support (TRACEBLOC_CHART_PATH) — install-k8s.ps1

The Windows installer could only install the published chart, so branch-only chart fixes (like #3 above, or #585's global.imageRegistry) were untestable from a Windows install. Added TRACEBLOC_CHART_PATH — parity with the bash installer's _resolve_chart_ref — so a local chart directory can be installed (skipping helm repo add); the published repo remains the default.

Tests

  • Pester source-guards + unit tests for every installer item above (checksum-driven retry, hash-anchored gates, WaitForExit, both-stream fallback, local-chart ref). Full suite green (451).
  • helm-unittest for the writable-volume init: present on hostPath / absent on CSI, no fsGroup on either path, and asserts the init has caps CHOWN+FOWNER+FSETID, runs the per-dir best-effort loop with chmod 3777, and mounts both /data/shared and /data/logs (mounting both is required or the chown is a no-op). Chart bumped 1.9.15 → 1.9.20 across the chart-affecting fixes (chart-content guard).
  • Windows-facing scripts are manifested; manifest.sha256 regenerated. bats/macOS suites unaffected (Linux/macOS paths untouched except the already-merged Installer tool acquisition must survive a proxy/AV-truncated binary download (k3d/kubectl/helm) #607 shared-bash bits, which a full cross-OS audit confirmed safe).

Scope note

This PR spans the installer scripts and the chart because they're the two halves of the same Windows install→ingest→experiment flow being validated together (kept in one PR by request).

The chart change is confined to the hostPath path: the privileged init-writable-data container is gated to hostPath and sets no fsGroup, so managed (EKS/AKS/OC) installs are genuinely untouched — CSI matches develop, and its spawned pods keep their documented posture (docs/SECURITY.md §5.3). (An earlier revision set fsGroup: 1000 on jobs-manager; @saadqbal and Bugbot correctly flagged that it never reaches the spawned writers and its relabel would strip their group-0 access on CSI, so it was removed.)

CI note

If pii-gate / pii-check is red, it is failing closed because the org PII_DENYLIST secret is unavailable — an org-wide infra issue affecting every PR, not this PR's content (tracked separately; the pii-gate-override label doesn't currently bypass it — tracebloc/.github#165). All other checks and Cursor Bugbot are green.

🤖 Generated with Claude Code

@shujaatTracebloc shujaatTracebloc self-assigned this Aug 5, 2026
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 5, 2026 14:20
…ated tool binary self-heals (#611)

Field follow-up to #607/#608. On a Windows machine behind a filtering proxy, the
k3d download kept failing at "System tool checksum verification failed" even with
#608's multi-transport download — because #608 validated a download by SIZE FLOOR
+ magic bytes only. A binary truncated mid-transfer to somewhere between the 10 MB
floor and the real 25.8 MB still passed (it's >10 MB and starts with 'MZ'), so the
fallbacks never fired and it dead-ended at the separate, no-retry checksum step.
(Proven on the box: a manual download produced the correct hash at 25,805,312
bytes, while the installer's copy failed the checksum in ~2s with no fallback.)

Fix — the checksum is the authoritative completeness test:
- Get-VerifiedDownload gains -Sha256: after a transport lands a size/magic-valid
  file, its SHA-256 must equal the expected hash or the transport is treated as
  failed and the NEXT one (curl.exe -> BITS) is tried. A truncated/altered copy
  now self-heals instead of dead-ending.
- Get-VerifiedDownload gains -MustContain for the small checksum-list files, so a
  proxy error page lacking the expected asset line is retried too.
- k3d / kubectl / helm now fetch their checksum FIRST (resiliently) and pass the
  extracted, 64-hex-validated hash as the download gate. helm gains checksum
  verification on the PS path for the first time (parity with the bash path).

Tests: Pester source-guards for the -Sha256/-MustContain gates, the mismatch->retry
path, and per-tool checksum-first wiring; full suite green (444). Manifest
regenerated.

Contributes to #578. Closes #611.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc shujaatTracebloc changed the title fix(installer): make the checksum drive the download retry so a truncated tool binary self-heals (#609) fix(installer): make the checksum drive the download retry so a truncated tool binary self-heals (#611) Aug 5, 2026
@shujaatTracebloc
shujaatTracebloc force-pushed the fix/609-checksum-driven-download branch from 4032a15 to 436ae6e Compare August 5, 2026 14:20
@shujaatTracebloc
shujaatTracebloc marked this pull request as draft August 5, 2026 14:21
Comment thread scripts/install-k8s.ps1
shujaatTracebloc and others added 2 commits August 5, 2026 16:33
…ries transports (Bugbot)

The kubectl .sha256 is a bare 64-hex hash with no fixed substring, so it used
-MinBytes 1 with no content gate -- a proxy error page satisfied the floor, the
first transport 'succeeded', curl.exe/BITS never ran, and the later hex check
aborted. Add -MatchPattern (a regex content gate) to Get-VerifiedDownload and use
'[0-9a-fA-F]{64}' for the kubectl checksum fetch, matching how k3d/helm use
-MustContain. Pester guards updated; manifest regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed (#611)

Field report (same Windows box, past the k3d download fix): Step 3 aborted with
"Failed to create compute environment" even though k3d printed
"Cluster 'tracebloc' created successfully!" with EMPTY stderr and the cluster was
actually up. Cause: Wait-ProcessWithDeadline polled HasExited but never called
WaitForExit(), and Start-Process -RedirectStandardOutput can leave $proc.ExitCode
$null in that window -- so `$null -ne 0` misread an exit-0 success as a failure.
Not machine-specific; a latent race any Windows user can hit.

- Wait-ProcessWithDeadline now calls $Process.WaitForExit() before returning
  success, so the redirected streams drain and ExitCode is reliable for EVERY
  caller (cluster create, partial delete, tracked installs).
- Cluster-create adds defense-in-depth: a still-null exit code falls back to
  k3d's own "created successfully" marker rather than failing a cluster that is up.
- Pester source-guards for both. Windows-only change (install-k8s.ps1); Linux/mac
  paths untouched and their suites remain green.

Contributes to #578.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 6, 2026 06:45
@shujaatTracebloc shujaatTracebloc added the pii-gate-override Bypass the public PII gate (reviewed false positive or gate cannot run); visible in audit label Aug 6, 2026
Comment thread scripts/install-k8s.ps1
Comment thread scripts/install-k8s.ps1
@shujaatTracebloc shujaatTracebloc added pii-gate-override Bypass the public PII gate (reviewed false positive or gate cannot run); visible in audit and removed pii-gate-override Bypass the public PII gate (reviewed false positive or gate cannot run); visible in audit labels Aug 6, 2026
shujaatTracebloc and others added 3 commits August 6, 2026 09:09
…ms in the exit fallback (Bugbot)

Two Bugbot findings on the checksum-driven download work:

1. (High) The checksum-LIST fetch gates were fail-open, so a proxy error page
   "succeeded" on the first transport and skipped the curl.exe/BITS retry — exactly
   the case #611 exists to survive. Helm's -MustContain substring
   (helm-<ver>-windows-<arch>.zip) also appears in the request URL a proxy page can
   echo; kubectl's -MatchPattern was unanchored so any page with a 64-hex run passed;
   k3d gated on the bare asset name. Fix: drop the weak -MustContain entirely and gate
   every checksum-list fetch on the hash STRUCTURE — k3d/helm require a 64-hex hash
   adjacent to the asset, kubectl requires the hash at the start of the body. A
   proxy/HTML error page can't satisfy that, so it retries transports as intended.

2. (Medium) The null-exit-code cluster-create fallback only scanned $k3dStdout, but
   k3d logs its "Cluster created successfully!" line via logrus to STDERR — so a real
   success could be misread as failure. Fix: check both $k3dStdout and $k3dStderr.

Pester source-guards updated: kubectl gate is start-anchored, k3d/helm gates are
hash-anchored, no -MustContain remains, and the fallback inspects both streams. Full
suite green (447). Manifest regenerated.

Contributes to #578.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Path installs

`tb data ingest` failed at the copy step with `mkdir: can't create directory
'/data/shared/.tracebloc-staging/': Permission denied`. On hostPath installs (the
Windows/WSL2 + bare-metal default) kubelet does not apply fsGroup to hostPath
volumes (kubernetes/kubernetes#138411), so /data/shared (client-pvc) is created
root-owned and the non-root ingest-staging pod can't write to it. mysql-data has a
privileged init-chown for exactly this reason; the shared data volume had none.

- jobs-manager gains fsGroup: 1000 (CSI clusters apply it to the shared volume).
- On hostPath, a privileged init-shared-data container (root, CHOWN+FOWNER only)
  chowns /data/shared to 1000:1000 and chmod 2777. World-writable, unlike
  mysql-data's single-UID chown, because the shared volume has multiple non-root
  writers whose UIDs this chart doesn't control -- jobs-manager, the training/
  ingestor pods it spawns, and the CLI's ingest-staging pod. setgid keeps new files
  in GID 1000; the init is gated on hostPath (CSI relies on fsGroup).
- helm-unittest: init present + world-writable on hostPath; absent (fsGroup kept)
  on CSI. Chart bumped 1.9.15 -> 1.9.16.

Client-side companion to the installer fixes on this PR (requested to land here).
Contributes to #578.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… parity with bash)

The Windows installer could only ever install the PUBLISHED chart (helm repo), so a
branch-only chart change (e.g. the #611 /data/shared fix, or #585's global.imageRegistry)
was impossible to test from a Windows install. The bash installer already supports a
local chart via TRACEBLOC_CHART_PATH (_resolve_chart_ref); this brings Windows to parity.

- When $env:TRACEBLOC_CHART_PATH is set, install-k8s.ps1 installs from that local chart
  directory (validated) and skips `helm repo add`; otherwise it uses the published repo
  as before. Applied to both the fresh-install and adopt/reconcile helm upgrades.
- Pester source-guards for the local-chart ref, the repo-add skip, and the not-a-directory
  error. Manifest regenerated. Full suite green (451).

Enables a from-scratch Windows test of the branch chart. Contributes to #578.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc shujaatTracebloc changed the title fix(installer): make the checksum drive the download retry so a truncated tool binary self-heals (#611) fix(installer): harden the Windows install→first-ingest path — checksum-driven downloads, reliable cluster-create, writable ingest volume, local-chart support (#611) Aug 6, 2026

@saadqbal saadqbal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful, well-tested PR — the checksum-as-completeness-test and the both-streams k3d exit fallback are the right calls. A few non-blocking things to weigh, mostly on the chart side (left inline).

Comment thread client/templates/jobs-manager-deployment.yaml Outdated
Comment thread client/templates/jobs-manager-deployment.yaml Outdated
Comment thread scripts/install-k8s.ps1 Outdated
shujaatTracebloc and others added 2 commits August 6, 2026 10:49
#611)

hostPath ignores fsGroup (kubernetes/kubernetes#138411), so /data/logs was
created root-owned and non-root training/inference pods hit
`PermissionError [Errno 13]` creating their per-run log dir
(`os.makedirs('/data/logs/<run>')`). The #611 init-container chowned
/data/shared but not /data/logs — the same class of bug on the logs volume.

Extend the init (renamed init-shared-data -> init-writable-data) to
chown+chmod BOTH hostPath volumes and mount both. Training and inference
pods share one spec (job.yaml), so this covers both; ingestion already
covered by the /data/shared chmod. Bump chart 1.9.16 -> 1.9.17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc shujaatTracebloc changed the title fix(installer): harden the Windows install→first-ingest path — checksum-driven downloads, reliable cluster-create, writable ingest volume, local-chart support (#611) fix(installer): harden the Windows install→first-experiment path — checksum-driven downloads, reliable cluster-create, writable ingest & training volumes, local-chart support (#611) Aug 6, 2026
Comment thread client/templates/jobs-manager-deployment.yaml Outdated
shujaatTracebloc and others added 2 commits August 6, 2026 11:03
…en-reuse parity

Two reviewer follow-ups on #612:

- chart: chmod the writable hostPath dirs 3777 (was 2777) — add the sticky
  bit so one writer can't unlink/rename another writer's files in
  /data/shared // /data/logs (/tmp semantics). setgid is retained. Safe given
  the uid topology (dir owned by 1000; training pods run as 1000; the ingestor
  writes its own subtrees as a stable uid) and no cross-uid filesystem deletes
  exist in client-runtime. Chart 1.9.17 -> 1.9.18.

- install-k8s.ps1: the adopt/reconcile helm upgrade now prefers
  --reset-then-reuse-values when `helm upgrade --help` advertises it (Helm
  >= 3.14), falling back to --reuse-values otherwise — so NEW chart defaults
  reach adopted Windows edges on auto-upgrade (bash parity with
  install-client-helm.sh). manifest.sha256 regenerated.

Tests: helm-unittest updated (3777); new Pester test asserts the
reset-then-reuse preference; full Pester suite 452 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bot)

init-writable-data chowns the shared/logs hostPath dirs to GID 1000 then
chmods 3777. With caps dropped to CHOWN+FOWNER only, the kernel silently
strips S_ISGID on the chmod — after the chown the dir's group no longer
matches the process (fsgid 0), and a root process without CAP_FSETID can't
set setgid on it — so the mount landed at 1777 and new files did NOT inherit
GID 1000 as documented. Add FSETID to the cap set; setgid now sticks.
helm-unittest asserts FSETID present. Chart 1.9.18 -> 1.9.19.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 15e0f37. Configure here.

Comment thread client/templates/jobs-manager-deployment.yaml Outdated
Comment thread client/templates/jobs-manager-deployment.yaml Outdated
… (Bugbot)

Two Bugbot findings on the writable-volume fix:

- HIGH — remove `fsGroup: 1000` / `fsGroupChangePolicy` from jobs-manager. It is
  a no-op on hostPath (kubelet ignores fsGroup — the init does the work) and on
  CSI it only grants jobs-manager's OWN processes GID 1000 while its
  OnRootMismatch relabel flips the shared/logs volumes to group 1000 — stripping
  the group-0 access the spawned training pods (UID 1001 / OpenShift arbitrary
  UID, GID 0) and the host-UID ingestion pods rely on (docs/SECURITY.md §5.3). It
  never reaches those spawned writers, so it was all regression risk and no gain.
  Those pods keep their own documented posture; CSI is untouched (matches develop).

- MEDIUM — the init now fixes each dir INDEPENDENTLY and best-effort:
  `for d in /data/shared /data/logs; do chown && chmod || echo <warn>; done`. A
  chown that can't complete (e.g. /data/shared on an NFS root_squash export) no
  longer aborts the chain and skips /data/logs — the other dir is still repaired
  and jobs-manager still starts; a truly unwritable mount surfaces as a clear
  error at the writer pod instead of wedging the edge in Init.

helm-unittest updated (no fsGroup on either path; per-dir loop; CSI skips init).
Chart 1.9.19 -> 1.9.20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc merged commit a07f76b into develop Aug 6, 2026
45 of 48 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the fix/609-checksum-driven-download branch August 6, 2026 09:43
divyasinghds added a commit that referenced this pull request Aug 6, 2026
After the rebase merged our port-6550 Pester tests with #612, Test-Preflight's
network-reachability block calls Has for kubectl/helm/k3d. The "port 6550 in use
by our running cluster -> ok" case passes ownership, so it now continues into
that block, where a k3d-only -ParameterFilter left those later Has calls with no
matching mock ("No mock for command 'Has' matched"). Use a plain default mock
(k3d + tools present -> only always-critical hosts probed) so every Has call is
covered and the case does not throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
divyasinghds added a commit that referenced this pull request Aug 6, 2026
After the rebase merged our port-6550 Pester tests with #612, Test-Preflight's
network-reachability block calls Has for kubectl/helm/k3d. The "port 6550 in use
by our running cluster -> ok" case passes ownership, so it now continues into
that block, where a k3d-only -ParameterFilter left those later Has calls with no
matching mock ("No mock for command 'Has' matched"). Use a plain default mock
(k3d + tools present -> only always-critical hosts probed) so every Has call is
covered and the case does not throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka pushed a commit that referenced this pull request Aug 6, 2026
…#622)

* fix(#557): preflight port 6550 on Windows so a conflict fails clearly

New-K3dCluster binds the cluster API server to 127.0.0.1:6550, but
Test-Preflight never checked whether 6550 was already in use. On a machine
where a leftover/other k3d cluster or an unrelated service owns 6550,
`k3d cluster create` failed and the installer surfaced k3d's raw stderr
instead of a clear "port already in use" cause.

Add Get-PfPortListening (Get-NetTCPConnection based; $null when
undeterminable) and a port-6550 check to Test-Preflight. A port owned by THIS
installer's own already-running cluster is fine (that run reuses it), so the
check only hard-fails when the listener is NOT our cluster, with actionable
guidance to find and stop the owner. Adds Pester coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#557): harden port-6550 preflight (bounded, fail-closed, running-gated)

Address PR #622 review on the Windows port-6550 preflight:

- Get-PfPortListening no longer fails open: -ErrorAction SilentlyContinue
  swallowed real CIM/access errors into the same empty result as a free
  port, so a busy port green-OK'd. Switch to -ErrorAction Stop and, in the
  catch, treat only Get-NetTCPConnection's ObjectNotFound (no listener) as
  "free"; every other error returns $null ("can't tell"). (Bugbot Med)

- Port-ownership check now reuses Test-ClusterRunning instead of a bare
  `k3d cluster list -o json`: it wraps the call in the same ~15s job
  deadline, so a wedged Docker engine can't hang preflight (Bugbot High),
  and it gates on serversRunning >= 1, so a STOPPED leftover cluster no
  longer masks a foreign listener on 6550 (Bugbot Med).

- Regenerate scripts/manifest.sha256 for the changed install-k8s.ps1.

- Add Pester coverage: probe error -> $null, ObjectNotFound -> free,
  stopped-leftover + foreign listener -> fail, running-owned -> ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#557): give the port-6550 running-cluster case a default Has mock

After the rebase merged our port-6550 Pester tests with #612, Test-Preflight's
network-reachability block calls Has for kubectl/helm/k3d. The "port 6550 in use
by our running cluster -> ok" case passes ownership, so it now continues into
that block, where a k3d-only -ParameterFilter left those later Has calls with no
matching mock ("No mock for command 'Has' matched"). Use a plain default mock
(k3d + tools present -> only always-critical hosts probed) so every Has call is
covered and the case does not throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#557): tri-state port-6550 ownership so a slow k3d list can't hard-fail reuse (Bugbot 3728340365)

The port-6550 ownership check decided ownership solely from Test-ClusterRunning,
whose bounded `k3d cluster list` maps BOTH a timeout AND a parse failure to
$false. The ownership block treated that $false as "foreign listener" and
HARD-FAILED preflight with stop/delete hints, so a slow/wedged Docker on a
normal re-run wrongly blocked the install and pointed the operator at their own
cluster -- conflating "can't determine" with "definitely not ours".

Make cluster run-state tri-state:
- Get-ClusterRunStateFromList (pure) and Get-ClusterRunState (bounded) return
  'running' | 'down' | 'unknown'. Get-ClusterRunState reports 'unknown' only
  when the list times out or its output is unparseable.
- Test-ClusterRunningInList / Test-ClusterRunning become thin boolean wrappers
  (=='running'), preserving their existing contracts and callers.

The ownership block now only hard-fails when CONFIDENT the listener is foreign
('down': enumerated and ours is absent/stopped, or no k3d installed). An
'unknown' read downgrades to a warning and proceeds, letting New-K3dCluster's
start/repair path settle it instead of blocking the install.

Tests: existing 3 cases updated to mock Get-ClusterRunState; added an
indeterminate case (list timed out -> warns, does NOT hard-fail). Manifest
regenerated. Pester not run locally (pwsh unavailable); validated by source
analysis. Source-of-truth guards for Test-ClusterRunning still hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#557): classify an absent cluster as 'down', not 'unknown' (Bugbot High 3728714531)

Get-ClusterRunState ran `k3d cluster list <name> -o json`, which fatals with
empty stdout when the named cluster is absent. The classifier read that empty
output as 'unknown' -> warn-and-proceed, so a genuinely-absent cluster with a
FOREIGN listener on 6550 escaped the hard-fail -- exactly the "re-run after the
first create failed on the busy port" case #557's preflight must catch.

List ALL clusters instead (`k3d cluster list -o json`, no name) and look for
$CLUSTER_NAME in the parsed array. A successful full list always emits at least
`[]`, so absent-vs-error stays separable:
- completed list without a running $CLUSTER_NAME (absent, stopped, or empty [])
  -> 'down' (confidently not ours -> hard-fail on a busy port)
- only a TIMED-OUT or unparseable/failed list -> 'unknown' (warn-and-proceed)

Tests: added a Get-ClusterRunState tri-state Describe -- pure classifier cases
(absent/empty-list -> 'down'; empty/garbage -> 'unknown') plus bounded cases
(timeout -> 'unknown'; completed-but-absent -> 'down'). Manifest regenerated.
Pester not run locally (pwsh unavailable); validated by source analysis.
Source-of-truth guards for Test-ClusterRunning still hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#557): drop fragile job-cmdlet-mock tests for bounded Get-ClusterRunState (CI Pester)

The two bounded-wrapper cases mocked Start-Job/Wait-JobWithProgress/Receive-Job,
which is environment-fragile and fails under CI Pester. Coverage is retained via
the pure Get-ClusterRunStateFromList tests + the Test-ClusterRunning source-guard
(bounded-job regex). No product-code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
shujaatTracebloc added a commit that referenced this pull request Aug 6, 2026
Resolve conflicts after today's develop advances (#612 chart bumps → 1.9.21,
#635 build-infra + check-facts k3s-cuda consumers + installer-tests paths):
- scripts/manifest.sha256: the only content conflict — regenerated against the
  merged tree (authoritative for the final install-k8s.ps1).
- docker/k3s-cuda/* and build-k3s-cuda.yaml: took develop's versions (they live
  on develop now via #635; #633 had removed its copies to avoid an add/add).

Verified on the merged tree: install-k8s.ps1 parses clean (GPU funcs intact),
check-facts --check passes, helm-unittest 32/32, full Pester 480 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pii-gate-override Bypass the public PII gate (reviewed false positive or gate cannot run); visible in audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Truncated tool binary passes #607's size floor and dead-ends at the checksum (make the checksum drive retry)

3 participants