ka-install: implement --render/--dry-run mode (issue #38)

Add ka-install verb — the third of the three writing verbs (ka-promote →
ka-build → ka-install). Always read-only: computes and prints the install
plan that ka-install <host> would execute, without touching any host.

New files:
  bin/ka-install                        — Python3, 243 lines
  tests/ka-install/run-tests.sh         — 20 hermetic tests, 337 lines
  tests/ka-install/fixtures/make-sandbox.sh — fixture builder, 275 lines

What --render computes:
  1. Read fleet/<host>.yaml for packaging metadata
  2. Read build/<host>/<baseline>/manifest.lock (from ka-promote)
  3. Detect drift: build receipt missing/empty → exit 3
  4. Determine backup destination from manifest template
  5. Detect Image vs vmlinuz naming, select mkinitcpio hook
  6. Print structured YAML plan, exit 0

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

Test coverage (20/20 PASS):
  - Argument parsing: requires host, --version, --dry-run alias
  - Error paths: missing fleet manifest, missing manifest.lock,
    unknown host
  - Drift detection: no build receipt -> exit 3
  - Happy path: full build receipt -> exit 0, plan printed
  - Plan fields: source package, host, backup path, hook, version
  - Image/vmlinuz detection for ARM and x86
  - mkinitcpio --hook flag for Image (ARM), plain for vmlinuz
  - /boot/firmware/ path (ampere-style boot layout)
  - Grep-implementation: no scp/ssh/pacman/reboot calls

Existing suites unaffected (ka-promote: 6/6 PASS).
This commit is contained in:
Markus Fritsche
2026-07-25 15:50:49 +00:00
parent 84cdb5b4ee
commit d9dc8bacd4
3 changed files with 855 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
#!/usr/bin/env bash
# make-sandbox.sh — Build a hermetic kernel-agent sandbox for ka-install tests.
#
# Usage: make-sandbox.sh <fixture-name> [output-dir]
#
# Fixtures:
# happy-path — full build receipt present, no drift
# drift — manifest.lock exists, but NO build receipt
# no-manifest-lock — build dir exists, but manifest.lock missing
# no-host-manifest — fleet/<host>.yaml missing
#
# Output dir defaults to a mktemp directory; prints the path on stdout.
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
fixture_name="${1:?fixture name required}"
scratch="${2:-$(mktemp -d -t ka-install-test.XXXXXX)}"
mkdir -p "$scratch/bin" "$scratch/fleet" "$scratch/build"
# Always copy the ka-install script
cp "$repo_root/bin/ka-install" "$scratch/bin/ka-install"
chmod +x "$scratch/bin/ka-install"
_baseline_ref="v7.0"
_suffix="-fresnel-fourier"
_boot_path="/boot/"
_bootloader_path="/boot/extlinux/extlinux.conf"
_host="testhost"
_pkg_name="linux-testhost-fourier"
case "$fixture_name" in
happy-path)
# Full setup: fleet manifest + manifest.lock + build receipt
cat > "$scratch/fleet/${_host}.yaml" <<YAML
host: ${_host}
arch: arm64
soc: rockchip/rk3399
board: pinebook-pro
distro: archlinux-arm
baseline:
ref: ${_baseline_ref}
package:
name: ${_pkg_name}
versioning: "\${baseline_ref}.kafr\${pkgrel}"
kernel_suffix: ${_suffix}
boot_path: ${_boot_path}
bootloader: extlinux
bootloader_path: ${_bootloader_path}
install_mode: alongside
backup:
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
YAML
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
host: ${_host}
baseline:
ref: ${_baseline_ref}
resolved_patches:
- apply_order: 1
include: board/test/0001-test.patch
sha256: abc123
size: 100
cumulative:
path: cumulative.patch
b2sum: def456
size: 100
build:
built_at: '2026-07-25T12:00:00+00:00'
built_on_host: boltzmann
ka_build_version: 3
published: true
packages:
- name: linux-testhost-fourier-7.0.kafr3-aarch64.pkg.tar.zst
size: 12345678
b2sum: b2mock
YAML
;;
drift)
# Fleet manifest + manifest.lock but NO build receipt
cat > "$scratch/fleet/${_host}.yaml" <<YAML
host: ${_host}
arch: arm64
soc: rockchip/rk3399
board: pinebook-pro
distro: archlinux-arm
baseline:
ref: ${_baseline_ref}
package:
name: ${_pkg_name}
versioning: "\${baseline_ref}.kafr\${pkgrel}"
kernel_suffix: ${_suffix}
boot_path: ${_boot_path}
bootloader: extlinux
bootloader_path: ${_bootloader_path}
install_mode: alongside
backup:
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
YAML
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
host: ${_host}
baseline:
ref: ${_baseline_ref}
resolved_patches:
- apply_order: 1
include: board/test/0001-test.patch
sha256: abc123
size: 100
cumulative:
path: cumulative.patch
b2sum: def456
size: 100
YAML
;;
no-manifest-lock)
# Fleet manifest present, but build dir has no manifest.lock
cat > "$scratch/fleet/${_host}.yaml" <<YAML
host: ${_host}
arch: arm64
soc: rockchip/rk3399
board: pinebook-pro
distro: archlinux-arm
baseline:
ref: ${_baseline_ref}
package:
name: ${_pkg_name}
versioning: "\${baseline_ref}.kafr\${pkgrel}"
kernel_suffix: ${_suffix}
boot_path: ${_boot_path}
bootloader: extlinux
bootloader_path: ${_bootloader_path}
backup:
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
YAML
# No build dir at all — ka-promote not run
;;
no-host-manifest)
# build receipt present but fleet/<host>.yaml missing
# Don't create fleet/testhost.yaml
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
host: ${_host}
baseline:
ref: ${_baseline_ref}
resolved_patches: []
build:
built_at: '2026-07-25T12:00:00+00:00'
built_on_host: boltzmann
ka_build_version: 1
published: true
packages:
- name: linux-testhost-fourier-7.0.kafr1-aarch64.pkg.tar.zst
size: 12345678
b2sum: b2mock
YAML
;;
image-detection)
# Test x86-style kernel_suffix to verify vmlinuz detection
cat > "$scratch/fleet/${_host}.yaml" <<YAML
host: ${_host}
arch: x86_64
baseline:
ref: ${_baseline_ref}
package:
name: linux-testhost-x86
versioning: "\${baseline_ref}.kafr\${pkgrel}"
kernel_suffix: -x86-fourier
boot_path: /boot/
bootloader: extlinux
bootloader_path: /boot/extlinux/extlinux.conf
install_mode: alongside
backup:
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
YAML
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
host: ${_host}
baseline:
ref: ${_baseline_ref}
resolved_patches:
- apply_order: 1
include: board/test/0001-test.patch
sha256: abc123
size: 100
cumulative:
path: cumulative.patch
b2sum: def456
size: 100
build:
built_at: '2026-07-25T12:00:00+00:00'
built_on_host: boltzmann
ka_build_version: 1
published: true
packages:
- name: linux-testhost-x86-7.0.kafr1-x86_64.pkg.tar.zst
size: 23456789
b2sum: b2x86
YAML
;;
ampere-boot-firmware)
# Test /boot/firmware/ path (like ampere/genbook)
_baseline_ref="v7.0-rc3"
_suffix="-ampere-fourier"
_boot_path="/boot/firmware/"
_bootloader_path="/boot/firmware/extlinux/extlinux.conf"
_pkg_name="linux-ampere-fourier"
cat > "$scratch/fleet/${_host}.yaml" <<YAML
host: ${_host}
arch: arm64
soc: rockchip/rk3588
board: coolpi-cm5-genbook
distro: archlinux-arm
baseline:
ref: ${_baseline_ref}
package:
name: ${_pkg_name}
versioning: "\${baseline_ref}.kafr\${pkgrel}"
kernel_suffix: ${_suffix}
boot_path: ${_boot_path}
bootloader: extlinux
bootloader_path: ${_bootloader_path}
install_mode: alongside
backup:
pre_install: hertz:/sparfuxdata/kernel-agent-backups/testhost/\${replaced_version}/
YAML
mkdir -p "$scratch/build/${_host}/${_baseline_ref}"
cat > "$scratch/build/${_host}/${_baseline_ref}/manifest.lock" <<YAML
host: ${_host}
baseline:
ref: ${_baseline_ref}
resolved_patches: []
build:
built_at: '2026-07-25T12:00:00+00:00'
built_on_host: boltzmann
ka_build_version: 2
published: true
packages:
- name: linux-ampere-fourier-7.0.rc3.kafr2-aarch64.pkg.tar.zst
size: 34567890
b2sum: b2ampere
YAML
;;
*)
echo "unknown fixture: $fixture_name" >&2
echo "valid: happy-path drift no-manifest-lock no-host-manifest image-detection ampere-boot-firmware" >&2
exit 1
;;
esac
echo "$scratch"