mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 00:01:22 +00:00
feat(backups): scheduled database backups via cron tick
- database_services gains backup_schedule + next_backup_at (goose migration, sqlc schema, new queries). - PUT /databases/:id accepts backup_schedule — validated cron expression, empty string disables; invalid returns 400. - Cron scheduler tick now also folds due databases into the existing snapshot pipeline (createBackupProcess). - Databases page backups section gets a schedule row showing current expr and next run. - resolveBackupConfig reports real next_backup/enabled instead of a fake last+24h estimate. - Verified e2e: '* * * * *' produced three consecutive completed archives containing the seeded payload; invalid schedule rejected. Authored By: TDvorak <[email protected]>
This commit is contained in:
1 parent
aeb91b03e9
commit
08d92fb10f
13 files changed
+387
-99
No files matched your search
@@ -2,8 +2,10 @@ package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"containr/internal/database/sqlcdb"
|
||||
"containr/internal/docker"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -436,6 +438,7 @@ func StartCronScheduler(ctx context.Context, db *database.DB, dockerClient *dock
|
||||
return
|
||||
case <-ticker.C:
|
||||
runDueCronJobs(db, dockerClient)
|
||||
runDueDatabaseBackups(db, dockerClient)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -478,6 +481,51 @@ func runDueCronJobs(db *database.DB, dockerClient *docker.Client) {
|
||||
}
|
||||
}
|
||||
|
||||
// runDueDatabaseBackups snapshots managed databases whose backup_schedule is
|
||||
// due, reusing the same archive pipeline as manual backups.
|
||||
func runDueDatabaseBackups(db *database.DB, dockerClient *docker.Client) {
|
||||
if dockerClient == nil {
|
||||
return
|
||||
}
|
||||
q := sqlcdb.New(db.DB)
|
||||
due, err := q.ListDueDatabaseBackups(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("cron scheduler: failed to list due database backups: %v", err)
|
||||
return
|
||||
}
|
||||
handler := NewDatabaseHandler(db.DB, dockerClient)
|
||||
for _, row := range due {
|
||||
schedule := row.BackupSchedule.String
|
||||
next, err := calculateNextRun(schedule, "UTC")
|
||||
if err != nil {
|
||||
log.Printf("cron scheduler: invalid backup schedule %q on %s: %v", schedule, row.ID, err)
|
||||
continue
|
||||
}
|
||||
now := time.Now()
|
||||
if err := q.SetDatabaseNextBackupAt(context.Background(), sqlcdb.SetDatabaseNextBackupAtParams{
|
||||
NextBackupAt: sql.NullTime{Time: *next, Valid: true},
|
||||
ID: row.ID,
|
||||
}); err != nil {
|
||||
log.Printf("cron scheduler: failed to advance backup schedule on %s: %v", row.ID, err)
|
||||
continue
|
||||
}
|
||||
backupID := generateDatabaseBackupID(row.ID)
|
||||
archivePath := sanitizeBackupArchivePath(managedDatabaseBackupArchivePath(backupID))
|
||||
if err := q.CreateDatabaseBackup(context.Background(), sqlcdb.CreateDatabaseBackupParams{
|
||||
ID: backupID,
|
||||
DatabaseID: row.ID,
|
||||
Size: "pending",
|
||||
Status: "in_progress",
|
||||
BackupPath: sql.NullString{String: archivePath, Valid: true},
|
||||
CreatedAt: sql.NullTime{Time: now, Valid: true},
|
||||
}); err != nil {
|
||||
log.Printf("cron scheduler: failed to record backup for %s: %v", row.ID, err)
|
||||
continue
|
||||
}
|
||||
go handler.createBackupProcess(row.ID, backupID, archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
// findServiceContainer returns the first running container whose name matches
|
||||
// the deployment naming convention containr-<serviceID>-*.
|
||||
func findServiceContainer(ctx context.Context, dockerClient *docker.Client, serviceID string) (string, error) {
|
||||
|
||||
Reference in new issue
Block a user