feat(backup): 玩过的世界每天在停服后自动打一个定时恢复点,按服按 owner 保留 7 个、90 天过期
This commit is contained in:
29 files changed
+1028
-59
No files matched your search
@@ -205,6 +205,21 @@ func (b *Backuper) BackupThenRestore(ctx context.Context, serverName, formerOwne
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupScheduled enqueues the daily restore point of a world (api.BackupScheduler):
|
||||
// a backup Job like Backup's, recorded as a scheduled backup and pruned to
|
||||
// [archive] scheduled_keep, so the owner's own backups keep their count.
|
||||
func (b *Backuper) BackupScheduled(ctx context.Context, serverName, formerOwner string) error {
|
||||
p := b.jobParams(serverName, formerOwner)
|
||||
p.Scheduled = true
|
||||
if err := b.Jobs.CreateBackupJob(ctx, p); err != nil {
|
||||
if errors.Is(err, ErrAlreadyExists) {
|
||||
return nil // suffix collision — treat as enqueued
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jobNameSuffix is a short random hex tag that makes each backup Job name unique.
|
||||
// 32 bits is ample: collisions only matter within a single Job's TTL window across
|
||||
// a handful of manual backups.
|
||||
|
||||
@@ -59,3 +59,28 @@ func TestBackupThenRestoreChainsTheRestore(t *testing.T) {
|
||||
t.Errorf("JobName %q", p.JobName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupScheduledMarksTheJob(t *testing.T) {
|
||||
jobs := &captureJobs{}
|
||||
b := &Backuper{Jobs: jobs, Config: Config{Image: "img", BackupPVC: "pvc"}}
|
||||
if err := b.BackupScheduled(context.Background(), "survival", "usr-1"); err != nil {
|
||||
t.Fatalf("BackupScheduled: %v", err)
|
||||
}
|
||||
if len(jobs.got) != 1 {
|
||||
t.Fatalf("created %d jobs, want 1", len(jobs.got))
|
||||
}
|
||||
p := jobs.got[0]
|
||||
if !p.Scheduled || p.FormerOwner != "usr-1" || p.RestoreRef != "" {
|
||||
t.Errorf("params = %+v", p)
|
||||
}
|
||||
if !strings.HasPrefix(p.JobName, BackupJobName("survival")+"-") {
|
||||
t.Errorf("JobName %q", p.JobName)
|
||||
}
|
||||
|
||||
if err := b.Backup(context.Background(), "survival", "usr-1"); err != nil {
|
||||
t.Fatalf("Backup: %v", err)
|
||||
}
|
||||
if jobs.got[1].Scheduled {
|
||||
t.Error("an on-demand backup is marked scheduled")
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,12 @@ const (
|
||||
|
||||
// ReasonPreRestore is the world_backups reason of a safety snapshot.
|
||||
ReasonPreRestore = "pre_restore"
|
||||
// ReasonScheduled is the world_backups reason of the daily restore point
|
||||
// felis-api takes of a world played since its last one.
|
||||
ReasonScheduled = "scheduled"
|
||||
// LabelReason marks a scheduled backup Job, so the jobs route can tell it
|
||||
// from one somebody asked for.
|
||||
LabelReason = "felis.lolicon.best/backup-reason"
|
||||
|
||||
worldVolume = "world"
|
||||
backupVolume = "backup"
|
||||
@@ -72,6 +78,10 @@ type JobParams struct {
|
||||
// the restore will extract.
|
||||
RestoreRef string
|
||||
RestoreBackupID string
|
||||
// Scheduled records the backup as ReasonScheduled, pruned to its own
|
||||
// [archive] scheduled_keep, and labels the Job LabelReason. It never carries
|
||||
// a restore.
|
||||
Scheduled bool
|
||||
|
||||
TTLAfterFinished time.Duration
|
||||
}
|
||||
@@ -134,6 +144,9 @@ func BackupJob(p JobParams) (*batchv1.Job, error) {
|
||||
if p.RestoreRef != "" && p.RestoreBackupID == "" {
|
||||
return nil, fmt.Errorf("backup: a chained restore needs the backup id")
|
||||
}
|
||||
if p.RestoreRef != "" && p.Scheduled {
|
||||
return nil, fmt.Errorf("backup: a scheduled backup carries no restore")
|
||||
}
|
||||
limits, err := resourceLimits(p.CPULimit, p.MemLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -160,6 +173,9 @@ func BackupJob(p JobParams) (*batchv1.Job, error) {
|
||||
if p.RestoreRef != "" {
|
||||
args = append(args, "--reason", ReasonPreRestore, "--protect", p.RestoreBackupID)
|
||||
}
|
||||
if p.Scheduled {
|
||||
args = append(args, "--reason", ReasonScheduled)
|
||||
}
|
||||
|
||||
container := corev1.Container{
|
||||
Name: "backup",
|
||||
@@ -212,6 +228,9 @@ func BackupJob(p JobParams) (*batchv1.Job, error) {
|
||||
annotationRestoreBackupID: p.RestoreBackupID,
|
||||
}
|
||||
}
|
||||
if p.Scheduled {
|
||||
meta.Labels[LabelReason] = ReasonScheduled
|
||||
}
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: meta,
|
||||
Spec: batchv1.JobSpec{
|
||||
|
||||
@@ -237,6 +237,41 @@ func TestBackupJobCarriesTheRestoreChain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A scheduled backup records itself as scheduled (so the Job prunes it to its
|
||||
// own keep, not the owner's manual one) and says so on the Job for the jobs
|
||||
// route; it protects nothing and carries no chain.
|
||||
func TestBackupJobRecordsAScheduledBackup(t *testing.T) {
|
||||
p := sampleJobParams()
|
||||
p.Scheduled = true
|
||||
job, err := BackupJob(p)
|
||||
if err != nil {
|
||||
t.Fatalf("BackupJob: %v", err)
|
||||
}
|
||||
args := job.Spec.Template.Spec.Containers[0].Args
|
||||
if !argsContain(args, "--reason", ReasonScheduled) {
|
||||
t.Errorf("args = %v, want --reason %s", args, ReasonScheduled)
|
||||
}
|
||||
for _, a := range args {
|
||||
if a == "--protect" {
|
||||
t.Errorf("a scheduled backup passes --protect: %v", args)
|
||||
}
|
||||
}
|
||||
if job.Labels[LabelReason] != ReasonScheduled {
|
||||
t.Errorf("job labels = %v, want %s=%s", job.Labels, LabelReason, ReasonScheduled)
|
||||
}
|
||||
if _, ok := job.Labels[labelThenRestore]; ok {
|
||||
t.Errorf("a scheduled backup carries a chain: %v", job.Labels)
|
||||
}
|
||||
|
||||
plain, err := BackupJob(sampleJobParams())
|
||||
if err != nil {
|
||||
t.Fatalf("BackupJob(plain): %v", err)
|
||||
}
|
||||
if _, ok := plain.Labels[LabelReason]; ok {
|
||||
t.Errorf("a plain backup is labelled scheduled: %v", plain.Labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupJobRejectsMissingInputs(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
@@ -247,6 +282,9 @@ func TestBackupJobRejectsMissingInputs(t *testing.T) {
|
||||
{"no backup pvc", func(p *JobParams) { p.BackupPVC = "" }},
|
||||
{"no config secret", func(p *JobParams) { p.ConfigSecret = "" }},
|
||||
{"chain without backup id", func(p *JobParams) { p.RestoreRef = "/backups/a.tar.gz" }},
|
||||
{"scheduled with a chain", func(p *JobParams) {
|
||||
p.Scheduled, p.RestoreRef, p.RestoreBackupID = true, "/backups/a.tar.gz", "bk-1"
|
||||
}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := sampleJobParams()
|
||||
|
||||
Reference in new issue
Block a user