6cf63cb064
Verification layers: Structural: PASS (all scripts executable, syntax clean, Dockerfile valid) Behavioral: PASS (121 bats, 77 pytest, docker build succeeds, compose valid) Security: PASS (no secrets committed, .dockerignore excludes .env*, env_file pattern) Quality: PASS (coreci patterns followed, no coreci refs, G-104/G-105/G-106 verified) P0 issues found and auto-fixed: 1. docker-compose.yml: removed invalid restart_policy key, fixed env_file syntax 2. pyproject.toml: added fastapi + uvicorn deps (v0.1 gap exposed by Dockerfile) 3. timing.sh: renamed coreci_deploy_timing → praxis_deploy_timing (TASK-03-07) 4. firstboot-hook.sh: fixed idempotency check (/opt/praxis/.git not /usr/local/bin/praxis-deploy) P1+ issues: 8 (1 fixed: lxc-config.sh default alignment, 7 noted for post-hoc review) REQ coverage: 18/20 covered, 2 deferred (live first-boot timing + live E2E require cluster) Must-haves: 25/28 pass, 2 partial (comment-only diffs, no Makefile), 1 deferred ---ci--- project: praxis phase: 1 milestone: v0.2 status: verify ---/ci---
194 lines
7.3 KiB
Bash
194 lines
7.3 KiB
Bash
#!/usr/bin/env bats
|
|
# Bats tests for scripts/proxmox/firstboot-hook.sh (praxis first-boot hookscript).
|
|
#
|
|
# Run: bats scripts/proxmox/test/firstboot-hook.bats
|
|
#
|
|
# firstboot-hook.sh is invoked by Proxmox at CT lifecycle phases on the
|
|
# PVE HOST. Only the `post-start` phase does work (other phases exit 0).
|
|
# In post-start it:
|
|
# 1. Idempotency check: skip if /opt/praxis/.git exists + praxis
|
|
# service is active (via pct exec).
|
|
# 2. Install Docker + docker-compose-v2 + git + curl inside the CT.
|
|
# 3. Clone the praxis repo from Gitea into /opt/praxis (with branch
|
|
# fallback to main).
|
|
# 4. Run scripts/install-service.sh inside the CT.
|
|
#
|
|
# These tests exercise the real firstboot-hook.sh with a mocked `pct`
|
|
# on PATH (records exec invocations + returns controllable exit codes)
|
|
# so the phase-gating, idempotency skip, Docker-install, and git-clone
|
|
# steps are verified without a live PVE host or CT.
|
|
#
|
|
# G-101: GITEA_TOKEN is baked into this snippet by stage-snippet.sh
|
|
# (the hookscript runs on the PVE host where lxc.environment is
|
|
# invisible). The tests set GITEA_TOKEN in the env to model the baked-in
|
|
# value (stage-snippet.bats verifies the sed bake itself).
|
|
|
|
setup() {
|
|
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
HOOK="${SCRIPT_DIR}/firstboot-hook.sh"
|
|
|
|
STUB_DIR="$(mktemp -d)"
|
|
export STUB_DIR
|
|
LOG="${STUB_DIR}/calls.log"
|
|
export CALL_LOG="$LOG"
|
|
: > "$LOG" 2>/dev/null || true
|
|
|
|
ROOT="${STUB_DIR}/root"
|
|
mkdir -p "$ROOT"
|
|
cp "$HOOK" "${ROOT}/firstboot-hook.sh"
|
|
|
|
# Mocked pct — `pct exec <vmid> -- <cmd...>` records the full
|
|
# invocation to $CALL_LOG and exits with STUB_PCT_EXIT (default 0).
|
|
# Per-call exit overrides via STUB_PCT_EXIT_<n> (1-based call number)
|
|
# let the idempotency-check test make call 1 fail (not-yet-installed)
|
|
# while subsequent calls succeed.
|
|
cat > "${ROOT}/pct" <<'PSTUB'
|
|
#!/bin/sh
|
|
# pct exec <vmid> -- <cmd...>
|
|
count_file="${STUB_DIR}/pct.count"
|
|
n=$(cat "$count_file" 2>/dev/null || echo 0)
|
|
n=$((n + 1))
|
|
echo "$n" > "$count_file"
|
|
# Record the full invocation (vmid + cmd).
|
|
shift # drop `exec`
|
|
vmid="$1"; shift
|
|
if [ "$1" = "--" ]; then shift; fi
|
|
printf 'pct:%s exec:%s cmd:%s\n' "$n" "$vmid" "$*" >> "$CALL_LOG"
|
|
# Per-call exit override.
|
|
eval "exit \${STUB_PCT_EXIT_${n}:-${STUB_PCT_EXIT:-0}}"
|
|
PSTUB
|
|
chmod +x "${ROOT}/pct"
|
|
|
|
export PATH="${ROOT}:${PATH}"
|
|
|
|
# GITEA_TOKEN is baked in by stage-snippet.sh; model it as an env var
|
|
# the baked snippet would carry.
|
|
export GITEA_TOKEN="gitea-test-token"
|
|
export PRAXIS_VERSION="v0.2"
|
|
export GITEA_HOST="git.cloudinit.dev"
|
|
# Reset the pct call counter between tests.
|
|
: > "${STUB_DIR}/pct.count" 2>/dev/null || true
|
|
}
|
|
|
|
teardown() {
|
|
[ -n "${STUB_DIR:-}" ] && rm -rf "$STUB_DIR"
|
|
}
|
|
|
|
@test "hook: non-post-start phase (pre-start) → exit 0 immediately, NO pct exec" {
|
|
run "${ROOT}/firstboot-hook.sh" 200 pre-start
|
|
[ "$status" -eq 0 ]
|
|
# No pct exec invocations (the phase gate exits before any work).
|
|
! grep -q '^pct:' "$LOG"
|
|
}
|
|
|
|
@test "hook: empty phase → exit 0 immediately, NO pct exec (defensive)" {
|
|
run "${ROOT}/firstboot-hook.sh" 200
|
|
[ "$status" -eq 0 ]
|
|
! grep -q '^pct:' "$LOG"
|
|
}
|
|
|
|
@test "hook: post-start phase — runs the idempotency check via pct exec" {
|
|
# Idempotency check (call 1) fails (not yet installed) → proceeds to
|
|
# Docker install (call 2) + git clone (call 3) + install-service (call 4).
|
|
# All subsequent calls succeed.
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
# The idempotency check ran (pct call 1).
|
|
[ "$(cat "${STUB_DIR}/pct.count")" -ge 1 ]
|
|
grep -q 'praxis already installed and active — skipping\|installing Docker inside CT' <<< "$output"
|
|
}
|
|
|
|
@test "hook: post-start + praxis already installed → idempotency skip, NO Docker install" {
|
|
# Idempotency check (call 1) succeeds (already installed + active) →
|
|
# the hook logs "already installed" + exits 0 WITHOUT running Docker
|
|
# install / git clone / install-service.
|
|
STUB_PCT_EXIT_1=0
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q 'praxis already installed and active — skipping' <<< "$output"
|
|
# Only ONE pct exec call (the idempotency probe).
|
|
[ "$(cat "${STUB_DIR}/pct.count")" -eq 1 ]
|
|
! grep -q 'installing Docker inside CT' <<< "$output"
|
|
! grep -q 'cloning praxis repo' <<< "$output"
|
|
}
|
|
|
|
@test "hook: post-start + not installed → Docker install step runs (apt-get docker.io)" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q 'installing Docker inside CT' <<< "$output"
|
|
# The pct exec log records the apt-get install docker.io invocation.
|
|
grep -q 'apt-get install' "$LOG"
|
|
grep -q 'docker.io' "$LOG"
|
|
grep -q 'docker-compose-v2' "$LOG"
|
|
grep -q 'git' "$LOG"
|
|
grep -q 'curl' "$LOG"
|
|
}
|
|
|
|
@test "hook: post-start + not installed → git clone step runs with CLONE_URL containing the baked GITEA_TOKEN" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q 'cloning praxis repo' <<< "$output"
|
|
# The git clone invocation records the CLONE_URL with the token.
|
|
grep -q 'git clone' "$LOG"
|
|
grep -q 'gitea-test-token@git.cloudinit.dev/coreci/praxis.git' "$LOG"
|
|
}
|
|
|
|
@test "hook: post-start + not installed → install-service.sh runs inside the CT" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q 'running install-service inside CT' <<< "$output"
|
|
# The pct exec log records the install-service.sh invocation.
|
|
grep -q 'scripts/install-service.sh' "$LOG"
|
|
}
|
|
|
|
@test "hook: PRAXIS_VERSION flows into the git clone --branch flag" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
PRAXIS_VERSION="feature-xyz" run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q "git clone --depth 1 --branch 'feature-xyz'" "$LOG"
|
|
}
|
|
|
|
@test "hook: GITEA_HOST override flows into the CLONE_URL" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
GITEA_HOST="git.staging.test" run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -eq 0 ]
|
|
grep -q 'gitea-test-token@git.staging.test/coreci/praxis.git' "$LOG"
|
|
}
|
|
|
|
@test "hook: post-start + Docker install fails (pct exit 1) → hook exits non-zero (set -e)" {
|
|
# Idempotency check (call 1) fails (not installed) → proceeds to Docker
|
|
# install (call 2) which ALSO fails → set -e propagates → hook exits 1.
|
|
STUB_PCT_EXIT_1=1
|
|
STUB_PCT_EXIT_2=1
|
|
export STUB_PCT_EXIT_1 STUB_PCT_EXIT_2
|
|
run "${ROOT}/firstboot-hook.sh" 200 post-start
|
|
[ "$status" -ne 0 ]
|
|
grep -q 'installing Docker inside CT' <<< "$output"
|
|
# git clone + install-service NOT reached.
|
|
! grep -q 'cloning praxis repo' <<< "$output"
|
|
! grep -q 'running install-service' <<< "$output"
|
|
}
|
|
|
|
@test "hook: VMID is passed through to every pct exec invocation" {
|
|
STUB_PCT_EXIT_1=1
|
|
export STUB_PCT_EXIT_1
|
|
run "${ROOT}/firstboot-hook.sh" 300 post-start
|
|
[ "$status" -eq 0 ]
|
|
# Every pct exec line records vmid=300.
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
pct:*) echo "$line" | grep -q 'exec:300 ' ;;
|
|
esac
|
|
done < "$LOG"
|
|
} |