From 5d1951ad94040bbce4fc8005d972910ebbb0ae2e Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Fri, 1 May 2026 13:49:34 +0100 Subject: [PATCH] P1-34: e2e smoke runbook + redacted GET /repo-credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/e2e-smoke.md — an ~5-minute runbook that walks the full P1 happy path against a sibling restic/rest-server: bootstrap admin, mint token with repo creds, enrol an agent, watch the config.update push land, run a backup, confirm the snapshot, edit creds and watch the second push fire. Per the design discussion this is a runbook (not a Go integration test); the Playwright version lands in P5-06. GET /api/hosts/{id}/repo-credentials returns the redacted view — {repo_url, repo_username, has_password} — so the UI can pre-fill the edit form without ever pulling the password out of the AEAD blob. Marks P1-32 / P1-33 / P1-34 done in tasks.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/e2e-smoke.md | 273 +++++++++++++++++++++++ internal/server/http/host_credentials.go | 47 ++++ internal/server/http/server.go | 2 + tasks.md | 7 +- 4 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 docs/e2e-smoke.md diff --git a/docs/e2e-smoke.md b/docs/e2e-smoke.md new file mode 100644 index 0000000..af711ea --- /dev/null +++ b/docs/e2e-smoke.md @@ -0,0 +1,273 @@ +# End-to-end smoke test (P1-34) + +A runbook for verifying the Phase 1 happy path against a real +`restic/rest-server`. Run this on any Linux host with Docker; nothing +here touches your real Proxmox cluster or Unraid storage. + +The test exercises: + +1. Operator mints an enrollment token **with repo creds** (P1-32). +2. Agent enrols, server burns the token, host_credentials row lands. +3. Agent connects over WS, server pushes `config.update` containing + the decrypted creds **before** the agent sees any command. +4. Agent persists creds into `secrets.enc` (P1-33). +5. Run-now backup against the live `restic/rest-server`. +6. `snapshots.report` updates the per-host projection. +7. `GET /api/hosts/{id}/snapshots` returns the new snapshot. + +Total time: ~5 minutes on a warm machine. + +--- + +## Prereqs + +- Docker + Docker Compose +- `restic` v0.16+ on the host running the agent (the agent does **not** + install it; that's a deliberate design choice — see spec §4.2) +- `curl`, `jq` + +## Layout + +Everything lives under `/tmp/rm-smoke/`. Nothing escapes it; remove the +directory to clean up. + +``` +/tmp/rm-smoke/ +├── compose.yaml # rest-server + control-plane +├── data/ # control-plane SQLite + secret key +│ └── agent-binaries/ # built agent binaries served by /agent/binary +├── rest/ # rest-server data volume +│ └── htpasswd +└── agent/ # this host plays the part of an endpoint + ├── etc/ # → bind-mounted as /etc/restic-manager + └── var-lib/ # → bind-mounted as /var/lib/restic-manager +``` + +## 1. Build the binaries + +```sh +mkdir -p /tmp/rm-smoke/data/agent-binaries +cd ~/src/restic-manager +make build +cp bin/restic-manager-agent /tmp/rm-smoke/data/agent-binaries/restic-manager-agent-linux-amd64 +``` + +The server's `/agent/binary?os=linux&arch=amd64` resolves to that path. + +## 2. Compose the stack + +`/tmp/rm-smoke/compose.yaml`: + +```yaml +services: + rest-server: + image: restic/rest-server:latest + restart: unless-stopped + environment: + - OPTIONS=--no-auth # smoke-test only; real deploys use --append-only + htpasswd + ports: + - "127.0.0.1:8000:8000" + volumes: + - ./rest:/data + + control-plane: + image: ghcr.io/dcglab/restic-manager:dev # or build locally; see §1 + restart: unless-stopped + ports: + - "127.0.0.1:8080:8080" + volumes: + - ./data:/data + environment: + - RM_LISTEN=:8080 + - RM_DATA_DIR=/data + - RM_BASE_URL=http://127.0.0.1:8080 + - RM_SECRET_KEY_FILE=/data/secret.key + - RM_COOKIE_SECURE=false # smoke-test only — we're on plain HTTP +``` + +For local-only smoke: skip the image and run the server straight from +the binary instead, pointing at `/tmp/rm-smoke/data`: + +```sh +RM_LISTEN=:8080 RM_DATA_DIR=/tmp/rm-smoke/data \ +RM_SECRET_KEY_FILE=/tmp/rm-smoke/data/secret.key \ +RM_COOKIE_SECURE=false \ +./bin/restic-manager-server +``` + +Either way, watch stderr for the **bootstrap token** — printed on first +run, used in the next step. + +## 3. Bootstrap the admin account + +```sh +BOOTSTRAP_TOKEN='' +curl -s -X POST http://127.0.0.1:8080/api/bootstrap \ + -H 'content-type: application/json' \ + -d "{\"token\":\"$BOOTSTRAP_TOKEN\",\"username\":\"admin\",\"password\":\"correct horse battery staple\"}" +``` + +## 4. Mint an enrollment token (with repo creds) + +```sh +curl -s -c /tmp/rm-smoke/cookies -X POST http://127.0.0.1:8080/api/auth/login \ + -H 'content-type: application/json' \ + -d '{"username":"admin","password":"correct horse battery staple"}' + +ENROLL=$(curl -s -b /tmp/rm-smoke/cookies -X POST http://127.0.0.1:8080/api/enrollment-tokens \ + -H 'content-type: application/json' \ + -d '{ + "hostname":"smoke-host", + "repo_url":"rest:http://127.0.0.1:8000/smoke", + "repo_username":"", + "repo_password":"smoke-pw" + }') +TOKEN=$(echo "$ENROLL" | jq -r .token) +echo "token: $TOKEN" +``` + +If the server rejects with `missing_field`, you forgot +`repo_url`/`repo_password` — both are required (P1-32). + +## 5. Initialise the rest-server repo + +`restic/rest-server` will lazy-create the path on first write, but +restic itself wants the repo initialised: + +```sh +RESTIC_PASSWORD=smoke-pw \ +restic -r rest:http://127.0.0.1:8000/smoke init +``` + +## 6. Pretend to be a fresh endpoint + +The agent will write `agent.yaml` + `secrets.enc` under +`/tmp/rm-smoke/agent/etc` and `/tmp/rm-smoke/agent/var-lib`. We point +both at those dirs to keep the smoke run isolated from your real +`/etc/restic-manager`. + +```sh +mkdir -p /tmp/rm-smoke/agent/etc /tmp/rm-smoke/agent/var-lib +CONFIG=/tmp/rm-smoke/agent/etc/agent.yaml + +# Pre-write the secrets path so we don't hit the system default. +cat > "$CONFIG" <