Skip to content

feat: Slice 6 — server audit, route remove, releases/server prune - #67

Merged
im-tyler merged 6 commits into
mainfrom
slice6-cleanup
Nov 5, 2026
Merged

im-tyler merged 6 commits into
mainfrom
slice6-cleanup

Conversation

@im-tyler

@im-tyler im-tyler commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Plan: _internal/DASH_AUTOCONFIG_PLAN_2026-11-03.md §9 (P5 cleanup/audit, owner-approved). Dash auto-config Slice 6, CLI side. Contracts corpus rev 19; no MI bump.

teploy server audit <server> --json (read-only)

Every finding from the plan table, each with evidence and a remedy {argv|null, destructive, confirm}:

Finding Remedy
Stale managed route / empty managed region new teploy route remove --app X (CAS)
Managed route, dead upstream teploy redeploy (confirm); report-only when the upstream isn't this app's (caddy_extra proxy) or the app is static
Orphan state, no containers teploy remove --app X --host H -y (keeps volumes; confirm)
Manual block, dead upstream / non-local address report only (argv null)
Versions beyond policy new teploy releases prune --keep N (confirm)
Build cache / dangling images new teploy server prune --build-cache --dangling
Large writable layer (≥5 GB) / unmanaged container (≥1 GB) report only
Dangling volumes report only
Disk pressure with attribution report + links to the reclaimable findings; doctor text no longer names a non-existent releases prune

Design principles held: report before remedy; evidence on every finding; destructive ⇒ confirm; hand-written config report-only; every remote command the audit issues only reads — pinned by a test (no mutating verb, docker subcommand outside the read set, or upload); unreadable sources land in checks[] as unknown and yield no finding — never a clean bill. --json exits 0 whenever the envelope was produced; read state.

Also flags scheduled jobs on a host whose cron daemon is absent (the deploy-ovh lesson); implausibly-small backup outputs were already refused at write time by the Slice 2 engine.

teploy route remove --app X

Removes a managed Caddy block proven dead: refuses a live app, a container still carrying the name, an upstream that still resolves (asked inside Caddy's network) or is unknowable, files the block still serves, unreadable state/containers, and webhook-carrying blocks. Commit is an exact-block CAS on the region judged + the app's fence guard — a concurrent rewrite is refused with no partial edit. Recorded as route.remove.

releases prune --keep N / server prune --build-cache --dangling

Pinned, current, previous and running versions are never pruned whatever --keep says; unreadable pins/state/containers refuse the prune; removals run fenced under the app lock. server prune touches only unused build cache and dangling images — no tagged image, container or volume. Events releases.prune, server.prune (under _fleet).

keep_versions (D3)

*int: unset → 5, pruned by deploys only after a recorded 7-day notice (deploy output and audit warn first); init writes 3; 0 keeps everything (keep_all_versions in the applied manifest — unset and N record what they always did, so existing digests hold).

Contracts (rev 19)

server-audit-envelope schema derived from the real DTOs + fixture generated from the real auditServer (one of each finding incl. null-remedy, one unknown check); version-handshake advertises server-audit-v1 (mi2.json regenerated, capability golden updated). Dash CI should re-pin ci/contracts.pin to rev 19.

Tests

Hermetic (mock SSH executors, the domains check discipline): every finding kind from fixture data with remedy/destructive/confirm flags; unreadable → check-not-finding; the read-only pin over every command the audit issues; route remove success, all refusal paths, and the concurrent-rewrite CAS refusal; prune selections (pins/current/previous/running survive, --keep 0, refusals); the notice-period state machine; manifest/D3 semantics; init writes 3. go build, go vet, GOOS=linux go vet, go test ./... and -race on the touched packages all clean; every commit builds standalone. Docs: README fleet-cleanup section, CHANGELOG unreleased, AUDIT_OPEN entry (report-only surface + deferrals recorded).

…version selection

Unset keep_versions now means DefaultKeepVersions (5), pruned by deploys
only after a 7-day notice recorded server-side (.prune-notice) so the
deploy output and `server audit` can warn before anything goes; `teploy
init` writes 3; an explicit 0 keeps everything and records
keep_all_versions in the applied manifest (unset and N record what they
always did, so existing digests hold; confighome seeding maps it back).

docker.PrunableVersions is extracted from PruneVersions so the audit and
`releases prune` can reason about the selection without running it:
the `keep` most recent versions by newest container creation time,
minus the protected set, accessories never candidates, undatable
versions sorted newest (cleanup never deletes what it cannot date).
Deploy-triggered prunes are recorded as releases.prune events.
…nd remedies

`teploy server audit <server> --json` reports what is left over on a
server: stale and empty managed routes, managed routes with dead
upstreams, orphan state, hand-written blocks (dead upstream, non-local
address), versions beyond keep_versions, reclaimable build cache and
dangling images, large writable layers, unmanaged containers, dangling
volumes, a Caddyfile with no Caddy, scheduled jobs with no cron daemon,
and disk pressure with attribution.

Every finding carries kind:subject (stable), severity, evidence, and a
remedy {argv, destructive, confirm} — argv null where nothing the CLI
does is safe (hand-written config, other people's containers, volumes).
Remedies are offered only where a command exists: route remove for a
proven-dead route, redeploy (confirm) for an app's own dead upstream,
remove -y (confirm) for orphan state, releases prune (confirm) for
versions beyond policy, server prune for build cache/dangling images.

The audit only reads: every remote command is read-only (pinned by a
test over a recording executor: no mutating verb, no docker subcommand
outside the read set, no upload). A source that could not be read shows
as unknown in checks[] and yields no finding — never a clean bill; with
--json the exit code is 0 whenever the envelope was produced, read state.

caddy.ManagedRegions/RegionHasWebhook/RegionRoots expose the managed
region view InspectRoutes already had; domains.ResolveNames is the
extracted in-Caddy-network resolver, shared with domains check; doctor's
disk remediations now point at server audit instead of a releases prune
command that did not exist.
Removes one app's managed Caddy block, reloads Caddy, records it as a
route.remove event (removed/absent; refused/failed in the detail). An
absent block is success.

Only a block proven dead goes. Refuses: a live app (state plus running
non-accessory containers, or a live static app), any container still
carrying the app's name, an upstream that still resolves (asked inside
Caddy's network) or cannot be proven dead, files the block still serves,
state or containers that cannot be read, and a block carrying the
autodeploy webhook route. The commit runs under the app's fence guard
and an exact-block compare-and-swap on the region that was judged, so a
deploy that rewrites the block in between is refused (ErrRouteCAS) with
no partial edit — the staged file is cleaned up and the successor's
block stays byte-exact.
releases prune removes a container app's old versions beyond --keep:
containers and images of everything outside the keep window, never the
pinned, current, previous or running versions whatever --keep says.
Every read happens under the app's lock and each failure refuses the
prune (acting without knowing current, previous or the pins could delete
them); removals run fenced. Recorded as a releases.prune event with
keep/pruned/protected. --keep 0 prunes everything the protections do not
name — deliberately the opposite of keep_versions: 0, documented in the
command help; the audit never emits it.

server prune --build-cache --dangling reclaims docker's unused build
cache and dangling images and nothing else — no tagged image, no
container, no volume. Recorded as a server.prune event under _fleet.

remove: an app whose directory is already gone (an earlier removal
finished) now succeeds instead of failing its state deletion — the
audit's one-click remove remedy must be idempotent against a second
click. The test now models the directory and lock a real remove sees.
New artifact server-audit-envelope: the `teploy server audit <server>
--json` envelope — findings with stable kind:subject ids, severity,
evidence and remedy {argv (null where nothing the CLI does is safe),
destructive, confirm}, per-source checks where unknown means the
findings depending on it are missing rather than clean, and disk
attribution. Schema derived from the real serverAuditEnvelope DTO;
fixture GENERATED from the real auditServer over a mock executor (one of
each finding including null-remedy classes, one unknown check, partial
state).

version-handshake advertises server-audit-v1 (mi2.json regenerated; the
capability registry golden updated to force the addition). MANIFEST
bumped to rev 19 with the artifact row. No MI bump.
README: a Fleet cleanup & audit section (server audit, route remove,
releases prune, server prune, the finding/remedy table, the
never-reported-clean rule) and the keep_versions config comment now
states the D3 semantics. CHANGELOG unreleased entry. AUDIT_OPEN records
what the audit deliberately leaves report-only (hand-written blocks,
writable layers, dangling volumes, unmanaged containers, live/webhook/
file-serving routes, foreign upstreams), the deferred surfaces (backup
output sizes stay in the backup engine, attribution beyond docker +
/deployments), and the decisions worth reviewing.
@im-tyler
im-tyler merged commit ebf8aa4 into main Nov 5, 2026
1 check passed
@im-tyler
im-tyler deleted the slice6-cleanup branch November 6, 2026 00:36
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