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:
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/alexei/tinyforge/internal/proxy"
|
||||
"github.com/alexei/tinyforge/internal/stale"
|
||||
"github.com/alexei/tinyforge/internal/store"
|
||||
"github.com/alexei/tinyforge/internal/volsnap"
|
||||
"github.com/alexei/tinyforge/internal/webhook"
|
||||
"github.com/alexei/tinyforge/internal/workload/plugin"
|
||||
)
|
||||
@@ -56,6 +57,7 @@ type Server struct {
|
||||
onDNSProviderChanged DNSProviderChangedFunc
|
||||
|
||||
backupEngine *backup.Engine
|
||||
snapshotEngine *volsnap.Engine
|
||||
sseGate *sseGate
|
||||
logScanReloader LogScanReloader
|
||||
dbPath string
|
||||
@@ -119,6 +121,11 @@ func (s *Server) SetBackupEngine(engine *backup.Engine) {
|
||||
s.backupEngine = engine
|
||||
}
|
||||
|
||||
// SetSnapshotEngine sets the volume-snapshot engine on the server.
|
||||
func (s *Server) SetSnapshotEngine(engine *volsnap.Engine) {
|
||||
s.snapshotEngine = engine
|
||||
}
|
||||
|
||||
// SetDBPath sets the database file path (needed for restore).
|
||||
func (s *Server) SetDBPath(path string) {
|
||||
s.dbPath = path
|
||||
@@ -329,6 +336,13 @@ func (s *Server) Router() chi.Router {
|
||||
r.With(auth.AdminOnly).Post("/start", s.startPluginWorkload)
|
||||
r.With(auth.AdminOnly).Delete("/", s.deletePluginWorkload)
|
||||
|
||||
// Volume snapshots (admin-only). Capture/list a workload's
|
||||
// host-bind data volumes; {sid}-scoped download/delete live
|
||||
// in the global admin group alongside backups.
|
||||
r.With(auth.AdminOnly).Get("/snapshots", s.listWorkloadSnapshots)
|
||||
r.With(auth.AdminOnly).Get("/snapshotable", s.getWorkloadSnapshotable)
|
||||
r.With(auth.AdminOnly).Post("/snapshots", s.createWorkloadSnapshot)
|
||||
|
||||
// Runtime view: per-source persisted state + storage usage.
|
||||
// Read-only; safe for any authenticated user.
|
||||
r.Get("/runtime-state", s.getWorkloadRuntimeState)
|
||||
@@ -519,6 +533,11 @@ func (s *Server) Router() chi.Router {
|
||||
r.Get("/backups/{id}/download", s.downloadBackup)
|
||||
r.Delete("/backups/{id}", s.deleteBackup)
|
||||
r.Post("/backups/{id}/restore", s.restoreBackup)
|
||||
|
||||
// Volume-snapshot download/delete (workload-scoped capture +
|
||||
// list live under /workloads/{id}/snapshots).
|
||||
r.Get("/snapshots/{sid}/download", s.downloadSnapshot)
|
||||
r.Delete("/snapshots/{sid}", s.deleteSnapshot)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user