Skip to content

feat(backup): per-server backup target, folderless JSON backup commands, quiesced restore (D3) - #46

Merged
im-tyler merged 5 commits into
mainfrom
feat/backup-target
Oct 20, 2026
Merged

im-tyler merged 5 commits into
mainfrom
feat/backup-target

Conversation

@im-tyler

Copy link
Copy Markdown
Contributor

Implements decision D3 of the server-authority plan (PLAN_P3_SCREENS_2026-10-20): one backup target per server, read at run time; folderless backup commands with parsed JSON; quiesced restore.

One backup target per server (backup-target-v1)

  • teploy server backup-target set --host S --bucket B [--region] [--endpoint] [--prefix] stores it at /etc/teploy/backup-target.json (0600, outside /deployments; ~/.config/teploy fallback for a non-root deploy user, said on output). Credentials arrive on stdin as two lines (access key, then secret; prompted echo-off on a terminal) and are stored age-encrypted with the server identity — identity try-order /etc/teploy/age-key then /deployments/.age-key, coordinating with W2's move. Stored creds decrypt inside the aws invocation's own shell and fail closed; ambient AWS chain only when none stored. show [--json] never emits secrets; unset removes the target.
  • set rewrites teploy-generated backup cron lines (by marker) to the backup run form; hand-written commands under a teploy marker (the nucleus-backup pattern) are left alone and reported. teploy doctor gains a backup-cron check: embedded credentials = fail, legacy bucket lines = warn; raw line text never reported.

Folderless JSON commands (backup-json-v1)

  • backup create|list|restore|prune|schedule|unschedule --app X --host S default to the stored target; legacy --bucket flags still override it. list --json reports parsed rows {id, kind: volumes|accessory|teploy, accessory?, size_bytes, created_at, key} plus schedules discovered from teploy cron markers (retention joined from the per-app store) — was raw aws s3 ls lines. create --json → id/kind/size/key; restore --json → the quiesced receipt. dr and accessory backup|restore|verify-backup default to the target too.

Quiesced restore

Under the app's fenced lock: stop the app's containers (and a generic accessory's), promote with the two-phase recovery discipline, start again, health-check. On failure — or an unhealthy app afterwards — the pre-restore recovery copy is kept and named. Non-TTY runs require --yes.

backup run — the cron entrypoint

Runs on the server (like autodeploy redeploy), reads target + per-app retention at run time. Cron lines are a single argument-free command; retention lives in a per-app store; failure alerting moved into the run (same alert script, same signatures). Schedule/install refuses when the server binary lacks backup run; an existing binary that has it is never reinstalled.

Events / contracts / docs

Events backup.run, backup.restore, backup_target.set (allowlist id/kind/accessory/error; the server-level set fans out to every app on the server). Contracts corpus rev 14: schemas + generated fixtures for all four new envelopes, version-handshake regenerated. CHANGELOG/README updated; backup: app-config key deferred (cron reconciliation does not fit this change — documented in CHANGELOG).

Tests

Unit tests with the ssh mock for target round-trip, stored-creds invocation, marker classification, cron rewrite (incl. hand-written lines and %-preservation), doctor check, schedule discovery, target resolution and backup run's no-target refusal. go test -race ./..., go vet ./..., gofmt clean.

🤖 Generated with Claude Code

im-tyler and others added 5 commits October 19, 2026 21:08
…esced restore (D3)

One backup target per server, stored on the server and read at run time:

- internal/backup/target.go: /etc/teploy/backup-target.json (0600, outside
  /deployments; ~/.config/teploy fallback for a non-root deploy user),
  credentials age-encrypted with the server identity (try-order
  /etc/teploy/age-key then /deployments/.age-key, matching W2's move).
  Stored creds decrypt inside the aws invocation's own shell and fail
  closed; the ambient AWS chain applies only when none are stored.
- teploy server backup-target set/show/unset: set takes credentials on
  stdin as two lines (prompted with echo off on a terminal), rewrites
  teploy-generated backup cron lines to the run form (hand-written
  commands under a teploy marker are left alone and reported), and fans a
  backup_target.set event out to every app on the server.
- backup create/list/restore/prune/schedule/unschedule are folderless
  (--app X --host S) and default to the stored target; legacy flags still
  override it. list --json reports parsed rows (id, kind
  volumes|accessory|teploy, accessory, size_bytes, created_at, key) plus
  schedules discovered from teploy cron markers, joined with the per-app
  retention store; create/restore --json report the new envelopes.
- restore quiesces: the app's containers (and a generic accessory's) are
  stopped under the fenced lock, the backup is promoted with the
  two-phase recovery discipline, containers restart and health-check; a
  failure (or an unhealthy app afterwards) keeps and names the recovery
  copy. Non-TTY runs require --yes.
- teploy backup run --app X [--accessory Y]: the cron entrypoint, runs on
  the server and reads target + retention at run time; schedules and
  accessory backup --schedule write a single argument-free cron line and
  store retention per app; failure alerting moved into the run (same
  backup-alert.sh, same signatures). dr and accessory verify-backup
  default to the stored target.
- doctor gains a backup-cron check (embedded credentials = fail, legacy
  bucket lines = warn; raw line text never reported). Capability tokens
  backup-target-v1 and backup-json-v1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schemas + generated fixtures for server backup-target show --json and the
backup list/create/restore --json envelopes; version-handshake regenerated
with backup-target-v1 and backup-json-v1. The old bare-array-of-raw-
aws-s3-ls-lines shape of backup list --json was never a corpus artifact
and pinned no consumer; MANIFEST records the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README backups section rewritten around the per-server target; CHANGELOG
Unreleased entry including the deferred backup: app-config key (cron
reconciliation does not fit this change; schedules stay manageable with
backup schedule --app/--host). AUDIT_OPEN notes the partial progress on
A33 (S3 creds out of command text and crontabs) and TCL-45 (one engine
for scheduled and manual backups).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keys

Two defects found by the infra-home live check:

- the stored-credentials payload eval'd bare AWS_* assignments, which set
  unexported shell variables — the exec'd aws child never saw them and
  fell back to the ambient chain (MinIO answered 'Unable to locate
  credentials'). The payload now uses export assignments.
- ListBackupsRecursive built listed keys as listing-prefix + key, but
  recursive keys are bucket-root-relative, doubling the app prefix
  (s3://bucket/app/app/volumes/...). Keys are rebuilt from the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bered rev 15; doctor counts include both new checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@im-tyler
im-tyler merged commit 20a98e0 into main Oct 20, 2026
1 check passed
@im-tyler im-tyler mentioned this pull request Oct 20, 2026
@im-tyler
im-tyler deleted the feat/backup-target branch October 21, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant