fix(installer): execute-gate installed tools so a broken binary fails Step 1 (#411) - #451
fix(installer): execute-gate installed tools so a broken binary fails Step 1 (#411)#451shujaatTracebloc wants to merge 4 commits into
Conversation
… Step 1 (#411) The only post-install "verification" was a log-only, failure-masking interpolation, so a corrupt or wrong-architecture binary (winget shims / partial installs skip checksum verify; brew delivers with no checksum of ours) still printed "System tools" and only died at cluster-create in Step 2. All three OSes had the gap. - New shared execute-gate helpers: assert_tool_runs (common.sh, bash) and Assert-ToolRuns (install-k8s.ps1, PowerShell). Each runs the tool's self-check; on non-zero exit or an exception it removes the located binary and fails loudly with an arch-aware remedy, so the tool step fails instead of the cluster step. - Gate kubectl / k3d / helm after install on Linux (setup-linux.sh), macOS (setup-macos.sh), and Windows (install-k8s.ps1), on both the fresh-install and already-present paths. "System tools" success only prints once all gates pass. - kubectl is gated with `version --client` (NOT --short — removed in kubectl 1.28+, which would false-fail the gate). - New check-drift.sh parity check (_drift_execute_gates) so no installer can silently drop a gate for a tool. Tests: +2 bats (common — working tool passes / broken tool errors + removes the binary), +2 bats (drift parity), +7 Pester (exit-nonzero / exception / binary-removal / arch remedy / static gates). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-gate (#411) The execute-gate (#411) runs `<tool> version` after install, but the setup-linux.bats harness only marked tools "present" via has() — it never provided a RUNNABLE k3d/kubectl/helm. On the toolless CI runner the gate's probe found no binary and failed 4 install_k3d tests; locally it false-passed because a real k3d was on PATH. Add silent (non-recording) runnable stubs to setup() so the gate has something to execute and they shadow any real tool on a dev host. Test-only; no production change.
…te-tools # Conflicts: # scripts/manifest.sha256 # scripts/tests/install-k8s.Tests.ps1
…te-tools # Conflicts: # scripts/manifest.sha256 # scripts/tests/check-drift.bats # scripts/tests/check-drift.sh # scripts/tests/install-k8s.Tests.ps1
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 328b78b. Configure here.
| if [[ ! -f "$DRIFT_ROOT/$ps1" ]]; then _note "$ps1 is missing"; fi | ||
| for t in kubectl k3d helm; do | ||
| grep -qE "Assert-ToolRuns -Name \"$t\"" "$DRIFT_ROOT/$ps1" || \ | ||
| _note "$ps1: no execute-gate for '$t' (Assert-ToolRuns -Name \"$t\") — #411" |
There was a problem hiding this comment.
Drift gate uses whole-file grep
Medium Severity
_drift_execute_gates scans entire installer files for assert_tool_runs / Assert-ToolRuns tokens. That can match comments or other non-call text, so deleting a real gate can still pass if the token lingers elsewhere — the same fail-open pattern that broke preflight host parity in #450. The next check in this file already documents why probe-shaped extractors are required.
Triggered by learned rule: Drift/parity checks must extract structured entries — no whole-file grep
Reviewed by Cursor Bugbot for commit 328b78b. Configure here.
| if (-not (Has "helm")) { Err "Helm could not be installed. Install manually from https://helm.sh/docs/intro/install/ and re-run." } | ||
| } | ||
| Log "helm: $(cmd /c 'helm version --short 2>&1')" | ||
| Assert-ToolRuns -Name "helm" -VersionArgs @("version","--short") -BinPath "$TOOL_DIR\helm.exe" |
There was a problem hiding this comment.
Wrong binary removed on gate fail
Medium Severity
Assert-ToolRuns for k3d and helm always passes -BinPath under TOOL_DIR, including already-present and winget paths where PATH may resolve a different binary. On gate failure the helper deletes that TOOL_DIR path, which can remove a good leftover copy while the broken package-manager binary that actually failed stays on PATH. kubectl only passes -BinPath on the fresh direct-download path.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 328b78b. Configure here.
| return 0 | ||
| fi | ||
| local path; path="$(command -v "$name" 2>/dev/null || true)" | ||
| [[ -n "$path" && -w "$path" ]] && rm -f "$path" 2>/dev/null || true |
There was a problem hiding this comment.
This rm fires on the already-present path too (macOS gates all three unconditionally after has), so a broken brew-managed binary gets its symlink deleted — but brew install on re-run sees the formula already installed and won't relink, so the gate fails again → confusing loop. Note the Windows side deliberately omits -BinPath on the already-present calls (815) to avoid removing a tool it didn't install; the bash gate has no such guard. Consider only removing when we placed the binary (i.e. pass the dest path like the fresh-install callers, and skip removal otherwise).
| spin_cmd "Installing system tools…" brew install helm | ||
| fi | ||
| log "helm: $(helm version --short 2>/dev/null || echo installed)" | ||
| assert_tool_runs helm version --short |
There was a problem hiding this comment.
kubectl was moved off --short here because it was removed in 1.28+ and would false-fail the gate — but helm is still gated with version --short on all three OSes. Worth confirming the pinned Helm (v4.x) still supports --short; if it ever drops it the gate false-fails and deletes a perfectly good binary. helm version --template / bare helm version would be immune.
| } | ||
| } | ||
| Log "k3d: $(k3d version | Select-Object -First 1)" | ||
| Assert-ToolRuns -Name "k3d" -VersionArgs @("version") -BinPath "$TOOL_DIR\k3d.exe" |
There was a problem hiding this comment.
This gate runs on the already-present path too, but always points -BinPath at $TOOL_DIR\k3d.exe. If k3d came from winget/choco (or was already on PATH), the broken binary is elsewhere, so Remove-Item is a no-op and Has "k3d" stays true on re-run → same failure every time. The remedy text does tell the user to uninstall the package-manager copy, so this is loud-but-stuck rather than silent; just flagging that the auto-remove can't actually self-heal that path.


Closes #411.
Problem
The only post-install "verification" was a log-only, failure-masking interpolation (
Log "k3d: $(k3d version …)"/… 2>/dev/null || echo present). So a corrupt or wrong-architecture binary — winget shims / partial installs skip the direct path's checksum verify, and brew delivers with no checksum of ours (#429) — still printed✔ System toolsand only died at cluster-create in Step 2. Lukas's audit confirmed it's all three OSes.Fix (both halves + macOS)
assert_tool_runs(common.sh, bash) andAssert-ToolRuns(install-k8s.ps1, PowerShell). Each runs the tool's self-check; on a non-zero exit or an exception it removes the located binary and fails loudly with an arch-aware remedy, so the tool step fails instead of the cluster step.System toolssuccess only prints once all gates pass.version --client, not--short(removed in kubectl 1.28+, which would false-fail the gate).check-drift.shparity check (_drift_execute_gates) so no installer can silently drop a gate.Acceptance criteria
k3d.exefails Step 1 loudly with an actionable message; Step 2 is never reached (Err/errorexits before it).Tests (+11)
common.bats+2 (working tool passes & binary untouched / broken tool errors + removes the binary)check-drift.bats+2 (execute-gate parity)install-k8s.Tests.ps1+7 Pester (exit-nonzero / exception / binary-removal / arch remedy / static gates)Verified locally (CI-identical):
Note
Low Risk
Installer-only hardening with fail-fast behavior before cluster creation; no runtime auth, data, or cluster logic changes.
Overview
Replaces log-only post-install version checks (which could mask corrupt or wrong-arch binaries and still show ✔ System tools until cluster-create) with execute gates that actually run each tool’s self-check during Step 1.
Adds
assert_tool_runsincommon.shandAssert-ToolRunsininstall-k8s.ps1: on failure they remove the located binary when possible and exit with an arch-aware remediation message. kubectl, k3d, and helm are gated on Linux, macOS, and Windows for both fresh installs and already-on-PATH tools; kubectl usesversion --client(not--short, which breaks on kubectl 1.28+).Adds
_drift_execute_gatesincheck-drift.shplus bats/Pester coverage so installers cannot silently drop a gate; updatesmanifest.sha256and setup-linux test mocks so gates see runnable tool stubs.Reviewed by Cursor Bugbot for commit 328b78b. Bugbot is set up for automated code reviews on this repo. Configure here.