feat(blueprints): capture compose snapshot before stateful eviction (#957)

Wire the snapshot_then_evict withdraw mode to actually persist the
blueprint's compose YAML to fleet_snapshots before running the
eviction. The mode previously recorded intent only. Capture failure
aborts the eviction with HTTP 500 rather than silently falling
through to a destructive withdraw.

Volume bytes remain out of scope: the snapshot holds the compose
definition only. UI copy and the Blueprints docs (Withdraw note,
Migrating stateful data section, two new Troubleshooting entries)
clarify that operators must move volumes by hand if they need the
data on another node.

Adds 9 route-level tests covering the success path, snapshot DB
write failure, orphan-row cleanup when insertSnapshotFiles fails,
empty compose_content, evict_and_destroy unchanged, stateless
unchanged, evict_blocked gate, omitted confirm field, and bad
confirm value.
This commit is contained in:
Anso
2026-05-06 21:58:45 -04:00
committed by GitHub
parent aa00dc2b89
commit 7fe90d9f3a
6 changed files with 357 additions and 11 deletions
@@ -146,7 +146,10 @@ export function BlueprintDetail({ blueprintId, open, onOpenChange, onChanged, ca
try {
const result = await withdrawDeployment(blueprint.id, nodeId, confirm);
if (result.error) toast.error(result.error);
else toast.success(confirm === 'evict_and_destroy' ? 'Evicted and data removed' : 'Deployment withdrawn');
else if (confirm === 'evict_and_destroy') toast.success('Evicted and data removed');
else if (confirm === 'snapshot_then_evict' && result.snapshotId !== null) {
toast.success(`Compose snapshot #${result.snapshotId} captured. Deployment withdrawn.`);
} else toast.success('Deployment withdrawn');
await refresh();
onChanged();
} catch (err) {
@@ -65,7 +65,7 @@ export function EvictionDialog({
Snapshot, then evict (recommended)
</div>
<p className="text-xs text-stat-subtitle mt-1.5 leading-relaxed">
Captures the compose definition into the existing fleet-snapshot store, then runs the eviction. Note: volume bytes are not shipped; that ships in a future Volume Migration feature.
Captures this stack's compose definition to Fleet → Snapshots, then runs the eviction. Volume bytes stay on this node and are removed by docker compose down. Relocate them by hand if you need them on another node.
</p>
</button>
<div className="space-y-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3">