Add per-workload capture of host-bind data volumes as downloadable tar.gz archives: a new internal/volsnap engine (enumerate host-bind volumes via the computeMounts merge, archive with archive/tar+gzip skipping symlinks/special files, per-workload retention + startup orphan cleanup), a volume_snapshots table + store CRUD, admin-gated API (list/snapshotable/create/download/delete), and a Snapshots panel on /apps/[id] that shows coverage and which volumes are skipped (and why). Scope: image-source apps, host-bind scopes (absolute/stage/project); Docker named volumes, tmpfs, and instance scope are surfaced as not-yet-supported. Restore is a separate later phase. Download/FilePath are containment-checked; create returns a typed no-data error (400) vs generic 500. Covered by archiver unit tests + full API e2e.
This commit is contained in:
@@ -578,6 +578,57 @@ export function backupDownloadUrl(id: string): string {
|
||||
return `/api/backups/${id}/download`;
|
||||
}
|
||||
|
||||
// ── Volume Snapshots ───────────────────────────────────────────────
|
||||
// Per-workload archives of host-bind data volumes. Capture-only for now
|
||||
// (create/list/delete/download); restore is a separate later phase.
|
||||
|
||||
export interface SnapshotInfo {
|
||||
id: string;
|
||||
workload_id: string;
|
||||
label: string;
|
||||
filename: string;
|
||||
size_bytes: number;
|
||||
manifest: string; // JSON-encoded [{ index, target, scope, source }]
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SnapshotableVolume {
|
||||
target: string;
|
||||
scope: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface SkippedVolume {
|
||||
target: string;
|
||||
scope: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SnapshotableInfo {
|
||||
volumes: SnapshotableVolume[];
|
||||
skipped: SkippedVolume[];
|
||||
}
|
||||
|
||||
export function listWorkloadSnapshots(workloadId: string, signal?: AbortSignal): Promise<SnapshotInfo[]> {
|
||||
return get<SnapshotInfo[]>(`/api/workloads/${workloadId}/snapshots`, signal);
|
||||
}
|
||||
|
||||
export function getWorkloadSnapshotable(workloadId: string, signal?: AbortSignal): Promise<SnapshotableInfo> {
|
||||
return get<SnapshotableInfo>(`/api/workloads/${workloadId}/snapshotable`, signal);
|
||||
}
|
||||
|
||||
export function createWorkloadSnapshot(workloadId: string, label?: string): Promise<SnapshotInfo> {
|
||||
return post<SnapshotInfo>(`/api/workloads/${workloadId}/snapshots`, label ? { label } : {});
|
||||
}
|
||||
|
||||
export function deleteSnapshot(sid: string): Promise<void> {
|
||||
return del<void>(`/api/snapshots/${sid}`);
|
||||
}
|
||||
|
||||
export function snapshotDownloadUrl(sid: string): string {
|
||||
return `/api/snapshots/${sid}/download`;
|
||||
}
|
||||
|
||||
// ── Health ──────────────────────────────────────────────────────────
|
||||
|
||||
export function getHealth(): Promise<{ docker: DockerHealth; proxy?: ProxyHealth }> {
|
||||
|
||||
Reference in New Issue
Block a user