19 Commits
Author SHA1 Message Date
Juan DenisandClaude Opus 4.8 f744106d64 test(scripts): Phase 4 — vm update/uninstall tests + 7-distro CI matrix
Hardening plan §7. Catches distro-specific regressions before release.

- scripts/test/vm-update.sh: install → forced local update (full blue/green
  deploy/migrate/switch/health) → re-run to confirm the "Already up to date"
  version-gate. Same VM harness contract as vm-install.sh (log + status file).
- scripts/test/vm-uninstall.sh: install + seed data → default uninstall asserts
  data preserved (DB snapshot in backups, /var/lib survives) → reinstall →
  --purge asserts data removed. End-to-end proof of the unified uninstaller.
- scripts-ci.yml: now runs all three serverless suites (update+install+lib),
  lints the new lib/ + test/ files (error severity) and keeps scripts/lib/*
  warning-clean, and expands the cross-distro unit matrix to Ubuntu 22.04/24.04,
  Debian 12, Rocky 9, AlmaLinux 9, Fedora 40, and openSUSE Leap 15.5.
- test_install.sh: +choose_pkg_manager graceful-failure case (7.4); a no-op
  docker stub so the uninstall-matrix cases run where docker isn't installed.
- test_lib.sh: neutralize the ambient container=oci env var (RPM/SUSE images)
  and force an empty PATH for the "no package manager" case — both bugs the new
  matrix surfaced (the tests, not the code).
- README: fixed the stale Fedora box, documented the new VM scripts + suites.

Validated by running the exact CI container logic locally across all seven
images: test_update 13/13, test_install 15-16/0, test_lib 13/13 everywhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 01:00:45 -04:00
Juan DenisandClaude Opus 4.8 ff35375b50 feat(install): Phase 3 — multi-distro abstraction, init/env detection, templated unit
Hardening plan §6. Reduces the distro-specific if/else surface and handles
edge environments:

- Dependency manifest (scripts/deps/manifest.yaml): declarative per-family
  package/python/node/docker/init spec. Consumed directly by the Phase 5
  Python bootstrap; install.sh mirrors it inline (it can't parse YAML before
  Python exists in the curl|bash path).

- New sourceable libs (built in parallel, each shellcheck-clean + unit-tested):
  - scripts/lib/pkg.sh  — pkg_detect/refresh/install/is_installed across
    apt/dnf/yum/zypper/pacman/apk (PKG_MGR_OVERRIDE + PKG_DRY_RUN for tests).
  - scripts/lib/init.sh — init_detect + start/stop/enable/disable/reload/
    is_active across systemd/openrc/runit/sysvinit/none (INIT_OVERRIDE/DRY_RUN).
  - scripts/lib/env.sh  — is_container/is_wsl/has_systemd (override-friendly).

- ID_LIKE handling (G5/6.5): install.sh's os_family_from() (now a pure,
  testable function reading ${SERVERKIT_OS_RELEASE}) maps unknown IDs via
  ID_LIKE to debian/rhel/fedora/suse/arch/alpine — rhel before fedora so RHEL
  clones pick the RHEL Docker repo.

- zypper + pacman + apk (G6/6.6): choose_pkg_manager/refresh_pkg_index/pkg_add,
  provision_docker and provision_python gained suse/arch/alpine branches;
  Docker enable/start is now init-aware (systemd OR OpenRC) so it no longer
  aborts on Alpine.

- Templated systemd unit (6.7): serverkit-backend.service → templates/
  serverkit-backend.service.in, rendered (render_service_unit) at install and
  in refresh_config so a custom SERVERKIT_DIR/venv/log path is honored.
  install_service warns-and-skips enable when has_systemd is false (G5).

- nginx TLS floor as a reversible conf.d snippet (6.8): install.sh + update.sh
  prefer /etc/nginx/conf.d/serverkit-tls.conf when nginx.conf doesn't already
  declare ssl_protocols (RHEL/Fedora/SUSE), falling back to the in-place edit
  on Debian/Ubuntu to avoid a 'duplicate ssl_protocols' error. uninstall
  removes only that snippet.

Tests: new scripts/test/test_lib.sh (13) + 4 new test_install.sh cases
(os_family_from, render_service_unit, harden_global_tls A/B). 13+15+10 pass;
shellcheck error+warning clean across all new/changed shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:46:38 -04:00
Juan DenisandClaude Opus 4.8 aa6ee2737f feat(update): Phase 2 — version-aware, dry-run-honest, rollback-safe updater
Updater robustness from the hardening plan §5:

- Version comparison before updating (G3): is_already_current compares the
  local VERSION to the target release tag (release mode) or the local checkout
  HEAD to the remote branch HEAD via `git ls-remote` (branch/main mode). When
  current, both the hybrid and docker flows print "Already up to date" and exit
  0 — unless --force. Offline/indeterminate always proceeds rather than wrongly
  blocking.

- Dry-run fidelity: new run_in_dir reports the working directory a command
  would run in, replacing the `run_or_dry cd X && run_or_dry compose ...` chain
  that printed compose commands with no cwd context.

- Rollback hardening: migrate_database now runs against the NEW slot's DB copy
  (slot-absolute path) instead of the /opt/serverkit symlink that still
  resolves to the live old slot — so a rollback hands the old code a
  byte-for-byte-unchanged database (PostgreSQL/shared DBs migrate directly, as
  before). rollback_docker guards `git reset` on a non-empty SHA with a clear
  warning. Both rollback paths now run a post-rollback health check and say
  plainly when even the rollback is unhealthy.

- Telemetry/logging: halt() always prints the full log path; both summaries
  gained an "old → new", mode (release/branch/main, +docker), and duration
  line, plus the log path.

Tests: test_update.sh gains versions_equal, is_already_current force/offline
short-circuits, and a flask-stub proof that migrate_database targets the new
slot's DB (T10). 10 passed / 2 skipped locally; shellcheck error+warning clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:31:03 -04:00
Juan DenisandClaude Opus 4.8 754bfb4a67 feat(install): Phase 1 hardening — firewall automation, py3.11/3.12, unified uninstall
Critical install/update/uninstall hotfixes from the hardening plan
(docs/plans/INSTALLER_UPDATER_UNINSTALLER_HARDENING_PLAN.md §4):

- Firewall automation (G1): new sourceable scripts/lib/firewall.sh detects
  firewalld/ufw/nftables/iptables and opens 80/443; install.sh runs
  configure_firewall, update.sh re-ensures it idempotently. Exactly what we
  open is recorded in /etc/serverkit/install-state.json (scripts/lib/state.sh)
  so uninstall undoes only our rules. Fixes the #1 "installed but unreachable"
  failure on fresh RHEL-family boxes.

- Accept Python 3.11 OR 3.12 (G2): locate_python now iterates
  python3.12/3.11/3 and provision_python installs the distro package
  (python3.11 on Debian 12, 3.12 on Ubuntu/Fedora, 3.11/3.12 on RHEL) before
  ever falling back to a source compile.

- Use $PYTHON_BIN for secret generation instead of a bare python3.

- Distro-first Node 20 (replacing unconditional NodeSource): install the
  distro nodejs+npm, fall back to NodeSource only when it's missing or < 18,
  and validate node>=18 before continuing.

- Unify uninstall (G4): new scripts/lib/uninstall.sh is the single canonical
  teardown routine shared by uninstall.sh and `serverkit uninstall` (they had
  drifted — one ran `down -v`, one didn't). Default preserves user data
  (volumes kept, DB snapshotted to backups); --purge/--delete-volumes removes
  volumes+DB+data dirs; --keep-data preserves /etc + /var/lib + backups;
  --yes/--dry-run added; honors SERVERKIT_DIR. Removes the apt lock-wait
  drop-in on uninstall.

Tests: new scripts/test/test_install.sh (11 cases, source-able install.sh)
covers python/node detection, firewall dry-run + state, and the uninstall
data-handling matrix. Existing test_update.sh still green. shellcheck clean
(error + warning) on all new/changed shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:24:39 -04:00
Juan DenisandClaude Opus 4.8 33c2483f7e feat(update): self-updating bootstrap + concurrent-run lock
Two reliability gaps remained after the failure cascade:

1. Stale-updater trap. The updater that runs is whatever is installed, so an
   old update.sh fails in ways the current code already handles and can't fix
   itself — exactly what left a box stuck. maybe_reexec_latest_updater fetches
   the newest update.sh for the target ref and re-execs into it (guarded by
   SERVERKIT_UPDATER_REEXECED, skipped offline/dry-run/opt-out, only after a
   bash -n check). From this version on, 'serverkit update' is reliable no
   matter how stale the box is.

2. No concurrency guard. acquire_update_lock takes an flock so two updates
   can't clobber the same blue/green slot or race docker compose; the lock
   auto-releases on exit and degrades gracefully where flock/the path is
   unavailable.

Both are covered by scripts/test/test_update.sh (guard no-ops; lock
refuses a held lock, succeeds when free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:52:09 -04:00
Juan DenisandClaude Opus 4.8 2549d4bbc7 feat(update): failure cascade + make the updater unit-testable
Harden update.sh against the class of failure that left a box silently stuck
on the old version, and make it testable without a real deploy.

Robustness (cascade):
- L5 loud failures: `set -Eeuo pipefail` (the -E/errtrace makes the ERR trap
  fire inside functions too) + a report_failure ERR trap that names the phase,
  exit code, line and command, plus always-on logging to
  /var/log/serverkit/update-*.log. No more silent drop-to-prompt.
- L4 docker-path rollback: snapshot the previous git SHA, :rollback image tags
  and dist.rollback before mutating; if the new backend is not healthy within
  90s, revert to the snapshot and halt (was: warn and continue).
- L6 self-heal: pin COMPOSE_PROJECT_NAME (kills the symlink-rename warning) and
  reclaim a stale inactive blue/green slot left by a prior failed hybrid run.

Testability:
- Parameterize the system dirs (SERVERKIT_NGINX_DIR / _LETSENCRYPT_DIR /
  _SYSTEMD_DIR / _CONFIG_DIR) and add SERVERKIT_NO_LOG, so refresh_config can
  run against fixtures instead of the host /etc.
- Make update.sh source-able via a BASH_SOURCE guard so functions can be
  exercised directly.
- scripts/test/test_update.sh: plain-bash unit tests (no deps, no server) that
  reproduce the missing-serverkit.conf regression and cover the
  detection/slot/report logic.
- .github/workflows/scripts-ci.yml: bash -n + shellcheck + the unit tests on
  changes to scripts/**, install.sh, serverkit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:34:52 -04:00
Juan Denis f9d0610452 chore: remove agent code now living in serverkit-agent repo
The Go agent has moved to its own repository:
https://github.com/jhd3197/serverkit-agent

Changes:
- Delete agent/ directory and all source, build, and packaging files
- Delete .github/workflows/agent-ci.yml and agent-release.yml
- Delete scripts/test/agent-test.ps1 (agent now builds in separate repo)
- Remove committed dist/serverkit-agent-windows-amd64.exe build artifact
- Update docs/INSTALLATION.md and docs/README.md links to point to the new repo
- Add migration notes to docs/AGENT_AUDIT.md, docs/REMOTE_ACCESS_ROADMAP.md,
  SECURITY_AUDIT.md, and docs/plans/AGENT_FLEET_PHASES_3-4.md

Panel agent API endpoints under /api/v1/servers/agent/ and /api/v1/agent/
are retained — they are the control-plane contract with the standalone agent.

Verified:
- backend: pytest (371 passed, 12 skipped)
- frontend: npm run lint (0 errors)
2026-06-21 03:30:57 -04:00
Juan DenisandClaude Opus 4.8 e3bb163221 test: harden Vagrant/RHEL E2E harness; add vm-install overlay script
- Vagrantfile: don't `dnf install rsync/curl` on RHEL boxes — it upgrades
  openssl-libs and breaks the box's sshd (same ABI issue as install.sh).
  rsync/curl ship preinstalled on generic/rocky9; fail loudly if a future
  box lacks them rather than silently corrupting the run.
- full-stack-test.ps1: drop fedora from the default distro set (Vagrant
  Cloud's generic/fedora is EOL at f39; opt in via -Only fedora).
- vm-install.sh (new): run install.sh from uploaded local source, overlay
  the working tree, rebuild, and probe health — so the harness tests the
  exact local code (incl. uncommitted) with sshd/firewall snapshots on every
  exit path for post-mortems. Recovered from a local stash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:45:09 -04:00
Juan DenisandClaude Opus 4.7 ed68cdccb5 Support Rocky/RHEL + fix Docker install on Fedora
install.sh:
- Detect rocky/almalinux/rhel/centos as OS_FAMILY=rhel.
- The Fedora system-deps block now also runs for rhel; auto-install
  epel-release on RHEL-family.
- Docker install on Fedora/RHEL no longer pipes get.docker.com. That
  script recently added docker-model-plugin to its package list, which
  doesn't exist in docker-ce repos and aborts the install on Fedora 39.
  Replace it with explicit `dnf config-manager --add-repo` +
  `dnf install docker-ce docker-ce-cli containerd.io
  docker-compose-plugin docker-buildx-plugin`.
- Node.js and nginx installs route through dnf for rhel too.
- SELinux setsebool runs for rhel too.

scripts/test/full-stack-test.ps1:
- fedora image bumped from fedora39 (EOL Nov 2024) to fedora40 (current).
- Test-VagrantInstall + Test-VmInstall: defensive read of status file.
  Previously `(Get-Content -Raw).Trim()` crashed with InvokeMethodOnNull
  when the status file was missing/empty (which happens when the VM's
  install script crashed before producing it). Now falls back to 'FAIL'
  cleanly so the run summary still works.

Found by full 5-distro suite: Ubuntu 22/24 + Debian 12 passed; Fedora 39
failed on docker-model-plugin; Rocky 9 failed silently due to apt-vs-dnf
+ status NPE. All three fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:34:50 -04:00
Juan DenisandClaude Opus 4.7 e3337f0550 Stop vagrant up output from corrupting test status
The previous commit added Tee-Object so vagrant up output was visible
in the terminal, but Tee-Object's pipeline output then leaked into
Test-VagrantInstall's return stream — the function returned vagrant's
entire log instead of "OK", so the final summary reported every passing
Vagrant VM as a failure.

Append `| Out-Host` after Tee-Object to consume the pipeline. The
operator still sees the live output; the function's return stays clean.

The previous Debian 12 run was actually all-green (10/10 pytest passed,
0 plugin-loader errors) — only the summary line was wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:59:31 -04:00
Juan DenisandClaude Opus 4.7 18d25c6d03 Stream vagrant up output to terminal so prompts are visible
Previously the orchestrator redirected vagrant up's stdout+stderr to a
log file, which meant the box download progress was invisible and any
interactive prompts (like "choose a Hyper-V switch") were silently
swallowed while vagrant blocked on stdin. The operator had no signal
the run was hung.

Use Tee-Object so the operator sees download progress + prompts live
in the terminal, while the log file still gets a copy for later
inspection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:41:53 -04:00
Juan DenisandClaude Opus 4.7 59e2874d5e Pin Hyper-V switch in Vagrantfile to avoid interactive prompt
Without this, vagrant up halts on first boot asking the operator to
choose a Hyper-V switch from a numbered menu, which breaks unattended
test runs. Hard-code "Default Switch" (Hyper-V's built-in NAT switch,
present on every Hyper-V install), overridable via
VAGRANT_HYPERV_SWITCH_NAME env var for users who want a different
network.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:29:53 -04:00
Juan DenisandClaude Opus 4.7 4657483f10 Detect missing admin elevation when Vagrant distros are requested
Vagrant + Hyper-V requires the process itself to run elevated; being in
the Hyper-V Administrators group is necessary but not sufficient. Without
this guard, the script would launch, pack the source, then hit a wall on
'vagrant up' with a permission error mid-run.

Now it checks early: if any requested distro uses the Vagrant backend
and the script isn't running elevated, print exact instructions and exit.
Multipass-only runs (Ubuntu) still work without admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:15:49 -04:00
Juan DenisandClaude Opus 4.7 3b2974177a Add Vagrant + Hyper-V runner for Debian/Fedora/Rocky coverage
Multipass on Windows only ships Ubuntu images. To cover Debian 12,
Fedora, and Rocky 9 too, add a parallel Vagrant runner that uses the
Hyper-V provider (no VirtualBox conflict with Multipass).

scripts/test/
- vagrant/Vagrantfile: parameterized via SK_BOX, SK_VM_NAME env vars;
  Hyper-V provider with linked clones; bootstraps rsync + curl.
- full-stack-test.ps1:
  - DistroMap replaces ImageMap; each entry tags backend (multipass/
    vagrant), image/box name, login user.
  - Default Distros now: ubuntu22, ubuntu24, debian12, fedora, rocky9.
  - Backend auto-detection: if Vagrant isn't installed, vagrant distros
    silently skip rather than failing. Same for Multipass.
  - Test-VagrantInstall mirrors Test-VmInstall using `vagrant upload` /
    `vagrant ssh -c`, per-VM VAGRANT_CWD under OutDir for state.
  - Teardown dispatches by backend (multipass delete vs vagrant destroy).
- README updated with the new prereqs and distro coverage table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:48:39 -04:00
Juan DenisandClaude Opus 4.7 1f7a0a2171 Stop live report flashing FAIL/VMs:0 on first open
Two fixes for the race where the browser opens report.html before any
VM has registered:
- Orchestrator seeds state.json with {"running":true,"vms":{}} the
  instant OutDir is created, so a refresh before the first VM is added
  still has a "running" signal.
- report.py treats missing state.json as RUNNING (not FAIL), and a VM
  with a non-done stage but no install-status file as RUNNING in its
  per-VM badge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:17:27 -04:00
Juan DenisandClaude Opus 4.7 191c6fde94 Fix plugin loader ordering + add live test report
backend: move load_all_plugins(app) inside the with app.app_context()
block, after MigrationService.check_and_prepare(app). The loader queries
the installed_plugins table; previously it ran before migrations created
that table, causing "no such table: installed_plugins" errors in journal
on every cold start. Surfaced by the new E2E harness (visible in the
journalctl pane of the report).

scripts/test: live HTML report.
- Orchestrator writes state.json after each phase + regenerates
  report.html, so opening the page shows real-time progress.
- Browser auto-opens the report when the run starts; page auto-refreshes
  every 8s while a run is in progress.
- Added per-VM 'Stage' field (pending -> transferring source ->
  installing -> running pytest -> done).
- Fixed empty "Details" cells rendering as black boxes; now show "-".
- Pulsing dot indicator in the live banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:04:04 -04:00
Juan DenisandClaude Opus 4.7 099c61d859 Make E2E test harness actually run end-to-end on Windows
Fixes a cascade of PowerShell + Multipass quirks found during the first
real run:

- Rewrote orchestrator sequentially. Start-Job's separate runspace was
  hiding the real errors below behind opaque "method on null" failures.
- Removed `function Mp` helper. PowerShell ships a built-in alias `mp`
  for Move-ItemProperty that silently shadows same-named functions and
  turned multipass calls into Move-ItemProperty calls. Inline `& $MpExe`
  everywhere instead.
- Lowered ErrorActionPreference from Stop to Continue. Multipass writes
  harmless "cannot set permissions" warnings to stderr when transferring
  to NTFS; Stop turned those into fatal aborts.
- Stopped piping `multipass exec` stdout through Tee-Object. The pipe
  could stay open indefinitely after the remote command exited, hanging
  the orchestrator for 45+ min on a finished install. Now redirects to a
  file on the VM and only checks the exit code.
- Tightened tar excludes. Tarball dropped from 305 MB to 20 MB.
- Added `npm ci` in vm-install.sh after the local-code overlay. The
  original installer ran `npm ci` from origin/main, then the overlay
  dropped in a dev package.json that needed different deps, causing a
  spurious frontend build failure.
- Added -ReuseVm flag for re-running just the harness against an
  already-installed VM (skips the 20-min launch cost).

First successful end-to-end run on Ubuntu 24.04: 10/10 tests passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:14:59 -04:00
Juan DenisandClaude Opus 4.7 37d9c63b9e Strip non-ASCII chars from test harness PS scripts
PowerShell 5.1 reads .ps1 files as ANSI by default; em-dashes and check
marks were mangled into bytes that broke the parser. Replaced with ASCII
equivalents so the script runs under stock Windows PowerShell without
needing pwsh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:32:25 -04:00
Juan DenisandClaude Opus 4.7 abb8889c83 Add Multipass-based E2E test harness
One-click PowerShell entry point that spins up Ubuntu/Debian VMs in
parallel, installs ServerKit from the local working tree (not origin/main),
runs a pytest API harness against the live panel, and aggregates results
into a self-contained HTML report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:13:17 -04:00