From 3aa9777f1c3b819625dc20e12917a43e050ac5be Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 26 Jun 2026 21:43:23 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20v0.85.0=20self-update=20rework=20?= =?UTF-8?q?=E2=80=94=20README/CHANGELOG/REPORT=20(live-validated)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7 --- CHANGELOG.md | 4 ++ REPORT.md | 105 +++++++++++++++++++++++-------------------- controller/README.md | 24 +++++++--- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a59fc..b4f56a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### v0.85.1 — version-only build (live self-update validation target) (2026-06-26) +- No code change vs v0.85.0. Pushed as the registry "latest" so the live e2e self-update path could be + validated via the real Settings button (demo `0.85.0 → 0.85.1`: in-guest pull → agent swap → reload). + ### v0.85.0 — Self-update reworked: in-guest pull + agent swap (Phase 1) (2026-06-26) - **Problem:** the self-update button was dead in the LXC architecture — `selfupdate/updater.go` drove the old bare-metal flow (`docker compose -f /opt/docker/felhom-controller/docker-compose.yml up -d`), diff --git a/REPORT.md b/REPORT.md index c6cf32c..3c622e1 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,57 +1,64 @@ -# REPORT — Catalog-driven initial-login extraction (show an app's auto-generated password) +# REPORT — v0.85.0: self-update reworked — in-guest pull + agent swap (Phase 1) -**Repo:** `felhom-controller` · **Version:** `v0.83.0` → **`v0.84.0`** · **Date:** 2026-06-26 -**Commit:** `1705d71` · **Paired catalog commit:** `app-catalog-felhom.eu` `8fd9c00` -**Deployed:** guest 9201 (`demo-felhom`) on `felhom-pve`, golden/bootstrap mechanism. +**Repo:** `felhom-controller` · **Version:** `v0.84.0` → **`v0.85.0`** (+ `v0.85.1` version-only validation +target) · **Date:** 2026-06-26 -## Problem -Some apps generate a random first-login password into a file at first boot instead of taking it from a -deploy field — Crafty writes one to `/crafty/app/config/default-creds.txt` (its built-in default is -rejected as "too short"). Customers had to read the container logs to find it; the static -`app_info.default_creds` hint can't carry a per-install secret. +## What & why +The self-update button was dead in the LXC architecture: `selfupdate/updater.go` drove the old bare-metal +`docker compose -f /opt/docker/felhom-controller/docker-compose.yml up -d` — a path that doesn't exist in +the guest ("docker-compose.yml nem elérhető"). The stranded 0.77.0 demo could detect 0.84 but not install +it. Phase 1: the controller **pulls** the target image in-guest, then delegates the container **swap to the +host agent** (which owns restart + verify + rollback). The controller never `docker rm`/recreates itself. -## Fix — a general, catalog-driven mechanism (Crafty is the first consumer) -- **`.felhom.yml`** gains an optional `initial_credentials` block: - `{file, format: json|regex|plain, container?, username_key/password_key (json), - username_pattern/password_pattern (regex), note}`. -- **`internal/stacks/metadata.go`** — new `InitialCredentials` struct + `Metadata.InitialCreds` - (deep-copied in `deepCopyStack`). -- **`internal/stacks/initialcreds.go`** — `ReadInitialCredentials(stack)` reads the file **live** from - the running container (`docker exec cat `; path passed as a single arg, no shell) and parses - it via the pure, unit-tested `parseInitialCreds` (json/regex/plain). Container defaults to the stack's - main container (`findProbeContainer`). Returns a non-Available result (card hidden) when the container - is down / file missing / parse fails. **Never persists the secret** to `app.yaml` — read on demand. -- **`internal/web/handlers.go`** — `appDetailHandler` populates `InitialCreds` for deployed apps with a - spec; **`app_info.html`** renders a "Kezdeti belépési adatok" card: username + masked password with - Megjelenítés/Másolás (value read from a hidden element — never inlined into a JS literal), clearly - labelled as the **initial** password (valid only until the customer changes it in-app). -- **Tests:** `parseInitialCreds` json (+ default key) / regex (+ missing-pattern) / plain / unknown-format. - `go build`, `go vet`, `go test ./internal/stacks/...` all green. +## Baseline +felhom-controller `e0cf78b` v0.84.0 → v0.85.0. Cross-repo: felhom-agent v0.42.0 (`POST /controller/swap`). -## Catalog (`8fd9c00`) -crafty-controller `.felhom.yml` gains the `initial_credentials` block (json, username/password keys, a -note) and `first_steps` now points at the app page instead of "find it in the logs". +## Changes +- **`internal/selfupdate/updater.go`** — rewrote the install path. Kept `CheckForUpdate`/`queryRegistry`/ + `VerifyStartup`/state. `performUpdate` now: `pullImage` (docker login `--password-stdin` → pull → logout, + in-guest over the shared socket) → `agent.SwapController(targetImage)`. **Removed** `updateComposeFile` + + `composePath` + the `docker compose up -d` flow. `NewUpdater` takes an **`AgentSwapper`** (nil → update + unavailable) instead of a compose path. `DryRun` reports `agent_reachable` + `pull_capable`. Added small + test seams (`queryFn`/`pullFn`). +- **`internal/agentapi/client.go`** — `SwapController` (POST `/controller/swap`, 202) + `SwapStatus`. +- **`cmd/controller/main.go`** — build a (nil-able) agent client from the provisioned local-API config and + pass it to `NewUpdater`. +- **UI + router unchanged:** the existing button (`triggerUpdate` → POST `/api/selfupdate/update`) + poll + (`pollUntilBack`) already drive it; success/rollback is detected by the existing `VerifyStartup` (running + version vs target). Latest-only. -## Security posture -This surfaces a **live working credential** on the app page — same exposure class as the existing -post-deploy password reveal and the `default_creds` card, and rendered with the same mask/reveal UX. It -relies on the dashboard being **auth-gated in production**; the demo's public-unauth dashboard is a -separate, pre-existing tracked issue (not introduced here). The secret is never written to controller -state — only read live from the volume where it already lives. +## Tests (`internal/selfupdate/updater_test.go`) — all green (was 0 → **4**) +up-to-date → no pull / no agent (Scenario C); pull-fails → agent never called, state failed (Scenario D); +happy → pull then exactly one `SwapController` with the right ref; no-agent → unavailable. +`go build ./... && go vet ./... && go test ./...` — all `ok`. -## Verification (live, guest 9201, controller v0.84.0) -| Check | Result | -|---|---| -| Spec synced + parsed | on-disk `.felhom.yml` carries `initial_credentials`; `/api/stacks/crafty-controller` meta shows it | -| Card renders | `/apps/crafty-controller` HTML contains the "Kezdeti belépési adatok" card | -| Username | `initcred-user` cell renders `admin` | -| Password (live-extracted) | hidden value element carries the exact password from the container's `default-creds.txt` (HTML-escaped `&`→`&`, decoded back by JS `textContent`) | -| Controls | `Felhasználónév` label + `icRevealPw`/`icCopyPw` reveal/copy wired | +## Build / deploy +Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.85.0` (and `:0.85.1`, a version-only build, as +the live update target). The demo was **not** manually deployed — the update feature itself brought 9201 +to latest. -**Method:** fetched the exact HTML the browser receives via the in-guest API (Host: felhom.demo-felhom.eu). -The reveal/copy is client-side JS — wiring confirmed in markup, not browser-clicked this run. +## Live test on demo 9201 (operator-authorized; method per check) +- Demo started at controller 0.77.0 (old dead self-update). Brought to **0.85.0** (new code) via the agent + primitive (`felhom-agent --selftest=controller-swap`), each hop verified healthy. +- **E2E via the real Settings button** (claude-in-chrome on `felhom.demo-felhom.eu/settings`): clicked + "Frissítés keresése" → the new controller's `queryRegistry` detected **0.85.1 • Frissítés elérhető**; + clicked **"Frissítés telepítése"** → the controller pulled 0.85.1 in-guest and called the agent → the page + polled `/api/health` and reloaded showing current **0.85.1** and **"Utolsó frissítés: ✅ Sikeres (0.85.0 → + 0.85.1)"**. `/api/selfupdate/status` → `last_state.status = "success"`. No compose error. +- Host-side (authoritative): running container `…:0.85.1 Up (healthy)`, `/etc/felhom-controller-image` = + 0.85.1, agent log "controller-swap: new controller healthy". +- Hub: the controller's report path shows `0.84.0 → 0.85.0 → 0.85.1`. -## Observation (carry, not acted on) -The whole-dashboard public-unauth exposure on the demo remains the right place to fix credential -visibility broadly — tracked separately; this feature should not be read as endorsing an unauthenticated -dashboard. +## Observations (out of scope — not acted on) +- **Phase 2** (next task): hub per-customer version floor (default global) + operator desired-version + fleet-push. An existing 0.84.0 customer still has the OLD dead button — the first hop onto new-code is + operator/agent-driven (the agent primitive), which Phase 2 formalizes. +- **Golden freshness:** the golden bakes 0.77.0; a golden rebuild at current is wanted so fresh provisions + start current (independent of this feature). +- `DryRun`'s `auto_update` scheduled path is unchanged structurally; this task covered the manual button. + +## Commits +- `3c1e91b` — v0.85.0 self-update rework + agentapi + tests. + +No secrets committed; the registry token is used only for the in-guest `docker login --password-stdin` +(logged out after the pull) and is referenced out-of-band. diff --git a/controller/README.md b/controller/README.md index 6eb777a..2ceb539 100644 --- a/controller/README.md +++ b/controller/README.md @@ -1065,18 +1065,30 @@ Notification preferences (email, enabled events, cooldown hours) are: #### Controller Self-Update (`internal/selfupdate/`) -The controller can update itself — a Watchtower-style pull-and-restart mechanism for a single container. Replaces manual SSH-based `docker pull + sed + docker compose up -d` with a one-click Settings page button or scheduled auto-update. +The controller can update itself to the latest registry version with a one-click Settings button. In the +LXC architecture there is **no in-container compose** to drive (the old `docker compose -f +/opt/docker/felhom-controller/docker-compose.yml up -d` path does not exist in the guest — it produced +"docker-compose.yml nem elérhető"). Instead (Phase 1, v0.85.0) the controller **pulls** the target image +in-guest and delegates the container **swap to the host agent**, which owns the restart + health-verify + +**rollback**. ##### How It Works ``` -1. Check Gitea Docker Registry V2 API for new image tags -2. Compare highest semver tag with current Version (set at build time via ldflags) -3. If newer version exists → pull image → update compose file → docker compose up -d -4. Current container is replaced by Docker → new container starts with new version -5. On startup, new container reads update-state.json → marks update success/failure +1. Check Gitea Docker Registry V2 API for the latest semver tag (queryRegistry, BasicAuth). +2. If newer than current (ldflags Version): docker login --password-stdin → docker pull + → docker logout — IN-GUEST over the shared docker socket (token via stdin, never argv). +3. Delegate to the host agent: agentapi.SwapController → POST /controller/swap {image} (202). The agent + (external to this container) rewrites /etc/felhom-controller-image, restarts + felhom-controller-bootstrap.service, polls the new controller to healthy, and ROLLS BACK to the + previous image if it doesn't come up. The controller never docker-rm/recreates itself. +4. On startup the new container reads update-state.json → VerifyStartup marks success (current==target) + or failure (rollback → version mismatch). The Settings button polls /api/health and reloads. ``` +Latest-only (no version picker). Per-customer version floor + operator desired-version fleet-push are +Phase 2 (hub-side). No host agent wired (un-provisioned guest) → self-update unavailable. + ##### Design Philosophy - **No automatic rollback** — follows the Watchtower pattern (24k+ GitHub stars, no rollback). Docker's `restart: unless-stopped` policy is the crash safety net. The Hub's dead man's switch detects when the controller goes down.