docs: v0.85.0 self-update rework — README/CHANGELOG/REPORT (live-validated)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
This commit is contained in:
@@ -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`),
|
||||
|
||||
@@ -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 <c> cat <file>`; 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.
|
||||
|
||||
+18
-6
@@ -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 <image>
|
||||
→ 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.
|
||||
|
||||
Reference in New Issue
Block a user