feat: clusev host CLI wrapper for short stack commands

feat/v1-foundation
boban 2026-06-19 22:49:10 +02:00
parent 503f4d7fdf
commit 580c72a59d
3 changed files with 110 additions and 2 deletions

48
docker/clusev/clusev Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Clusev host CLI — short wrappers around the production stack.
# Generated by install.sh (it substitutes __CLUSEV_DIR__ with the install directory) and
# installed to /usr/local/bin/clusev. Operators run `clusev <command>` from anywhere instead
# of long `docker compose -f docker-compose.prod.yml ...` invocations. The compose filename is
# referenced internally only — it is never printed to the operator.
set -euo pipefail
CLUSEV_DIR="__CLUSEV_DIR__"
COMPOSE_FILE="${CLUSEV_DIR}/docker-compose.prod.yml"
compose() { docker compose -f "$COMPOSE_FILE" "$@"; }
usage() {
cat <<'EOF'
clusev — Fleet-Control Verwaltung (Host)
sudo clusev update Update holen, Image neu bauen, Migrationen anwenden (root)
clusev reset-admin Admin-Zugang zuruecksetzen (2FA entfernen, Passwort neu setzen)
clusev restart Stack neu starten
clusev logs [dienst] Logs folgen (Strg-C beendet)
clusev ps Dienst-Status (Alias: status)
clusev migrate Datenbank-Migrationen anwenden
clusev artisan <...> beliebiges artisan-Kommando im app-Container
clusev version installierte Version anzeigen
clusev help diese Uebersicht
Was im Hintergrund laeuft:
update -> update.sh (git pull + Image-Rebuild + migrate)
reset-admin -> docker compose exec app php artisan clusev:reset-admin
migrate -> docker compose exec app php artisan migrate --force
EOF
}
cmd="${1:-help}"; shift 2>/dev/null || true
case "$cmd" in
update) exec "${CLUSEV_DIR}/update.sh" "$@" ;;
reset-admin) compose exec app php artisan clusev:reset-admin "$@" ;;
restart) compose up -d "$@" ;;
logs) compose logs -f "$@" ;;
ps|status) compose ps "$@" ;;
migrate) compose exec app php artisan migrate --force "$@" ;;
artisan) compose exec app php artisan "$@" ;;
version) grep -oE "'version' => '[^']+'" "${CLUSEV_DIR}/config/clusev.php" \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true ;;
help|-h|--help) usage ;;
*) printf 'Unbekannter Befehl: %s\n\n' "$cmd" >&2; usage; exit 64 ;;
esac

View File

@ -312,8 +312,19 @@ ADMIN_PW="$(printf '%s\n' "$INSTALL_OUT" | sed -n 's/^CLUSEV_ADMIN_PASSWORD=//
ADMIN_MAIL="$(printf '%s\n' "$INSTALL_OUT" | sed -n 's/^CLUSEV_ADMIN_EMAIL=//p' | head -n1)"
[ -n "$ADMIN_MAIL" ] || ADMIN_MAIL="$ADMIN_EMAIL"
# ── [9/9] MOTD (themed, shown at host login) ─────────────────────────
phase 9/9 "MOTD installieren"
# ── [9/9] Host-CLI + MOTD ────────────────────────────────────────────
phase 9/9 "Host-CLI + MOTD installieren"
# clusev host command: bake the install dir into the template, drop it in PATH. Literal
# (not sed) substitution so any path is safe; best-effort with the whole group's stderr muted
# so a read-only /usr/local/bin never fails the installer (no stray "Permission denied").
cli_template="$(cat docker/clusev/clusev)"
cli_rendered="${cli_template//__CLUSEV_DIR__/$(pwd)}"
if { printf '%s\n' "$cli_rendered" > /usr/local/bin/clusev && chmod 0755 /usr/local/bin/clusev; } 2>/dev/null; then
info "Host-Befehl installiert: clusev (z. B. 'clusev ps', 'sudo clusev update')"
else
info "Host-Befehl uebersprungen (/usr/local/bin nicht beschreibbar)"
fi
if [ -n "$DOMAIN" ] && [ "$DOMAIN_OK" = 1 ]; then
ACCESS_URL="https://${DOMAIN}"
elif [ -n "$DOMAIN" ]; then

View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Verifies the clusev host CLI template renders and behaves. No docker needed — only the
# help / version / unknown-command paths are exercised. Run from the repo root:
# bash tests/scripts/test-clusev-cli.sh
set -euo pipefail
cd "$(dirname "$0")/../.."
REPO="$(pwd)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
# Render the template the way install.sh does.
sed "s|__CLUSEV_DIR__|${REPO}|g" docker/clusev/clusev > "${TMP}/clusev"
chmod +x "${TMP}/clusev"
CLI="${TMP}/clusev"
# 1. Valid bash syntax.
bash -n "$CLI" || fail "syntax error in rendered clusev"
# 2. help lists the key subcommands.
out="$("$CLI" help)"
for needle in "clusev update" "clusev reset-admin" "clusev logs" "clusev ps" "clusev migrate"; do
printf '%s' "$out" | grep -qF "$needle" || fail "help missing: $needle"
done
# 3. bare invocation and --help also show usage.
"$CLI" | grep -qF "clusev reset-admin" || fail "bare invocation did not show usage"
"$CLI" --help | grep -qF "clusev reset-admin" || fail "--help did not show usage"
# 4. unknown command exits 64 and warns on stderr.
set +e
"$CLI" definitely-not-a-command >/dev/null 2>"${TMP}/err"; rc=$?
set -e
[ "$rc" -eq 64 ] || fail "unknown command exit = $rc (want 64)"
grep -qF "Unbekannter Befehl" "${TMP}/err" || fail "unknown command did not warn"
# 5. version reads config/clusev.php (no docker).
want="$(grep -oE "'version' => '[^']+'" config/clusev.php | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)"
got="$("$CLI" version)"
[ "$got" = "$want" ] || fail "version = '$got' (want '$want')"
# 6. install.sh wires the CLI into PATH.
grep -qF "/usr/local/bin/clusev" install.sh || fail "install.sh does not install /usr/local/bin/clusev"
# 7. usage text must NOT leak the long compose filename.
printf '%s' "$out" | grep -qF "docker-compose.prod.yml" && fail "usage leaks docker-compose.prod.yml"
printf 'ok — clusev CLI checks passed\n'