[ka:cli-build-out] ka-install: --render/--dry-run mode (compute install plan without executing) #38

Open
opened 2026-07-25 12:45:26 +00:00 by marfrit · 1 comment
Owner

Parent umbrella

Closes a scoped subset of #21. ka-promote (#22) and ka-build (#34) are already closed; this is the third verb's "computation half only."

Scope — render-only, never execute

ka-install <host> --render (or --dry-run, following ka-build's existing --dry-run flag at bin/ka-build:36) computes and prints the install plan that ka-install <host> would execute — without touching any host, SSH, scp, pacman, or reboot.

What --render computes

  1. Read manifest.lock (produced by ka-promote <host>, expected at build/<host>/<baseline_ref>/manifest.lock)
  2. Detect drift: does manifest.lock's resolved hash match the package that was actually built?
  3. Determine backup destination: hertz:/sparfuxdata/kernel-agent-backups/<host>/<replaced_version>/
  4. Detect appropriate post-install hook: mkinitcpio's stock hook watches vmlinuz, not Image (the ARM kernel filename) — detect which trigger path applies
  5. Print the plan (files to copy, backup path, hook command, package version) as structured YAML or text to stdout — and exit 0

Explicitly out of scope (NOT in this issue)

  • Actual scp / rsync of files to any host
  • pacman -U / apt package installation
  • SSH to any real host (other than local file ops in the sandbox)
  • Any reboot, extlinux trigger, or mkinitcpio hook execution
  • Any mutation of host state outside the sandbox

These remain a separate, future, human-gated issue that will extend ka-install with the execution half.

Acceptance

  • ka-install <host> --render prints a plan and exits 0
  • Plan includes: source package, target host, backup path, post-install hook command, package version
  • Detects Image vs vmlinuz naming and selects the correct mkinitcpio trigger
  • Detects drift between manifest.lock and built package
  • Never calls scp/ssh/pacman/reboot (grep-implementation or mock-assert-not-called)
  • Exits non-zero on missing inputs (no manifest.lock, no host argument)
  • All testing via fixtures in tests/ka-install/fixtures/
## Parent umbrella Closes a scoped subset of #21. ka-promote (#22) and ka-build (#34) are already closed; this is the third verb's "computation half only." ## Scope — render-only, never execute `ka-install <host> --render` (or `--dry-run`, following ka-build's existing `--dry-run` flag at `bin/ka-build:36`) computes and prints the install plan that `ka-install <host>` would execute — without touching any host, SSH, scp, pacman, or reboot. ### What --render computes 1. **Read `manifest.lock`** (produced by `ka-promote <host>`, expected at `build/<host>/<baseline_ref>/manifest.lock`) 2. **Detect drift**: does `manifest.lock`'s resolved hash match the package that was actually built? 3. **Determine backup destination**: `hertz:/sparfuxdata/kernel-agent-backups/<host>/<replaced_version>/` 4. **Detect appropriate post-install hook**: mkinitcpio's stock hook watches `vmlinuz`, not `Image` (the ARM kernel filename) — detect which trigger path applies 5. **Print the plan** (files to copy, backup path, hook command, package version) as structured YAML or text to stdout — and exit 0 ### Explicitly out of scope (NOT in this issue) - Actual `scp` / `rsync` of files to any host - `pacman -U` / `apt` package installation - SSH to any real host (other than local file ops in the sandbox) - Any reboot, extlinux trigger, or mkinitcpio hook execution - Any mutation of host state outside the sandbox These remain a separate, future, human-gated issue that will extend ka-install with the execution half. ## Acceptance - [ ] `ka-install <host> --render` prints a plan and exits 0 - [ ] Plan includes: source package, target host, backup path, post-install hook command, package version - [ ] Detects `Image` vs `vmlinuz` naming and selects the correct mkinitcpio trigger - [ ] Detects drift between manifest.lock and built package - [ ] Never calls scp/ssh/pacman/reboot (grep-implementation or mock-assert-not-called) - [ ] Exits non-zero on missing inputs (no manifest.lock, no host argument) - [ ] All testing via fixtures in `tests/ka-install/fixtures/`
Author
Owner

Implementation report

Closes #38.

What was done

New file: bin/ka-install (243 lines, Python3)

Read-only install-plan computation engine. Always --render mode (never executes).

  • Parses fleet/<host>.yaml for package metadata (name, versioning template, kernel suffix, boot path, bootloader, backup target)
  • Reads build/<host>/<baseline>/manifest.lock (produced by ka-promote)
  • Detects drift: whether ka-build has run (build receipt present with build.packages)
  • Determines backup destination from manifest's backup.pre_install template
  • Detects kernel image type: Image (ARM64, default) vs vmlinuz (x86, heuristic via kernel_suffix)
  • Selects correct mkinitcpio hook: mkinitcpio -P --hook vmlinuz=Image for ARM (stock hook watches vmlinuz, not Image), plain mkinitcpio -P for x86
  • Computes install path from boot_path + kernel_suffix
  • Prints structured YAML plan to stdout, exits 0 on success, 3 on drift

Exit codes: 0 (plan ready), 2 (missing input), 3 (drift), 4 (parse error).

New file: tests/ka-install/run-tests.sh (337 lines, Bash)

20 hermetic tests covering every acceptance criterion:

# Test Status
1 Requires host arg (exit non-zero) PASS
2 --version flag PASS
3 --dry-run alias for --render PASS
4 Missing fleet manifest (exit 2) PASS
5 Missing manifest.lock (exit 2) PASS
6 Drift detection: no build receipt (exit 3) PASS
7 Happy-path --render with build receipt (exit 0) PASS
8 Plan includes source package (.pkg.tar.zst) PASS
9 Plan includes target host PASS
10 Plan includes backup path PASS
11 Plan includes post-install hook command PASS
12 Plan includes package version PASS
13 Image detection for ARM (default) PASS
14 vmlinuz detection for x86 PASS
15 mkinitcpio hook for Image uses --hook flag PASS
16 mkinitcpio hook for vmlinuz is plain PASS
17 /boot/firmware/ path (ampere-style) PASS
18 Drift exit 3 / non-drift exit 0 PASS
19 No scp/ssh/pacman/reboot calls (grep check) PASS
20 Unknown host exits with error (exit 2) PASS

New file: tests/ka-install/fixtures/make-sandbox.sh (275 lines, Bash)

Fixture builder for hermetic sandboxed test trees. Supports 6 fixtures:

  • happy-path — full build receipt present
  • drift — manifest.lock without build receipt
  • no-manifest-lock — fleet manifest present, no build output
  • no-host-manifest — build receipt present, fleet yaml missing
  • image-detection — x86-style kernel_suffix for vmlinuz test
  • ampere-boot-firmware — /boot/firmware/ path layout

Existing tests unaffected

tests/ka-promote/run-tests.sh: 6/6 PASS (unchanged)
tests/ka-build/run-tests.sh: 3 PASS, 2 SKIP, 1 pre-existing failure (marfrit-packages not present on pica) — no regression

## Implementation report Closes #38. ### What was done **New file: `bin/ka-install`** (243 lines, Python3) Read-only install-plan computation engine. Always --render mode (never executes). - Parses `fleet/<host>.yaml` for package metadata (name, versioning template, kernel suffix, boot path, bootloader, backup target) - Reads `build/<host>/<baseline>/manifest.lock` (produced by ka-promote) - Detects **drift**: whether ka-build has run (build receipt present with `build.packages`) - Determines **backup destination** from manifest's `backup.pre_install` template - Detects **kernel image type**: `Image` (ARM64, default) vs `vmlinuz` (x86, heuristic via kernel_suffix) - Selects correct **mkinitcpio hook**: `mkinitcpio -P --hook vmlinuz=Image` for ARM (stock hook watches vmlinuz, not Image), plain `mkinitcpio -P` for x86 - Computes **install path** from boot_path + kernel_suffix - Prints structured YAML plan to stdout, exits 0 on success, 3 on drift Exit codes: 0 (plan ready), 2 (missing input), 3 (drift), 4 (parse error). **New file: `tests/ka-install/run-tests.sh`** (337 lines, Bash) 20 hermetic tests covering every acceptance criterion: | # | Test | Status | |---|------|--------| | 1 | Requires host arg (exit non-zero) | PASS | | 2 | --version flag | PASS | | 3 | --dry-run alias for --render | PASS | | 4 | Missing fleet manifest (exit 2) | PASS | | 5 | Missing manifest.lock (exit 2) | PASS | | 6 | Drift detection: no build receipt (exit 3) | PASS | | 7 | Happy-path --render with build receipt (exit 0) | PASS | | 8 | Plan includes source package (.pkg.tar.zst) | PASS | | 9 | Plan includes target host | PASS | | 10 | Plan includes backup path | PASS | | 11 | Plan includes post-install hook command | PASS | | 12 | Plan includes package version | PASS | | 13 | Image detection for ARM (default) | PASS | | 14 | vmlinuz detection for x86 | PASS | | 15 | mkinitcpio hook for Image uses --hook flag | PASS | | 16 | mkinitcpio hook for vmlinuz is plain | PASS | | 17 | /boot/firmware/ path (ampere-style) | PASS | | 18 | Drift exit 3 / non-drift exit 0 | PASS | | 19 | No scp/ssh/pacman/reboot calls (grep check) | PASS | | 20 | Unknown host exits with error (exit 2) | PASS | **New file: `tests/ka-install/fixtures/make-sandbox.sh`** (275 lines, Bash) Fixture builder for hermetic sandboxed test trees. Supports 6 fixtures: - `happy-path` — full build receipt present - `drift` — manifest.lock without build receipt - `no-manifest-lock` — fleet manifest present, no build output - `no-host-manifest` — build receipt present, fleet yaml missing - `image-detection` — x86-style kernel_suffix for vmlinuz test - `ampere-boot-firmware` — /boot/firmware/ path layout ### Existing tests unaffected `tests/ka-promote/run-tests.sh`: 6/6 PASS (unchanged) `tests/ka-build/run-tests.sh`: 3 PASS, 2 SKIP, 1 pre-existing failure (marfrit-packages not present on pica) — no regression
Sign in to join this conversation.