Repository navigation
feat(backup): per-server backup target, folderless JSON backup commands, quiesced restore (D3) - #46
Merged
Merged
Conversation
…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>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/teployfallback 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-keythen/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;unsetremoves the target.setrewrites teploy-generated backup cron lines (by marker) to thebackup runform; hand-written commands under a teploy marker (the nucleus-backup pattern) are left alone and reported.teploy doctorgains abackup-croncheck: 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 Sdefault to the stored target; legacy--bucketflags still override it.list --jsonreports 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 rawaws s3 lslines.create --json→ id/kind/size/key;restore --json→ the quiesced receipt.drandaccessory backup|restore|verify-backupdefault 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 entrypointRuns 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 lacksbackup 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