11
"github.com/pkg/errors"
12
"github.com/spf13/cobra"
13
"github.com/wal-g/tracelog"
14
"github.com/wal-g/wal-g/internal/multistorage"
15
"github.com/wal-g/wal-g/pkg/storages/storage"
16
"github.com/wal-g/wal-g/utility"
20
NoDeleteModifier = iota
22
FindFullDeleteModifier
24
ConfirmFlag = "confirm"
25
DeleteShortDescription = "Clears old backups and WALs"
27
DeleteRetainExamples = ` retain 5 keep 5 backups
28
retain FULL 5 keep 5 full backups and all deltas of them
29
retain FIND_FULL 5 find necessary full for 5th and keep everything after it
30
retain 5 --after 2019-12-12T12:12:12 keep 5 most recent backups and backups made after 2019-12-12 12:12:12`
32
DeleteBeforeExamples = ` before base_0123 keep everything after base_0123 including itself
33
before FIND_FULL base_0123 keep everything after the base of base_0123`
35
DeleteEverythingExamples = ` everything
36
delete every backup only if there is no permanent backups
37
everything FORCE delete every backup include permanents`
39
DeleteTargetExamples = ` target base_0000000100000000000000C4 delete base backup by name
40
target --target-user-data "{ \"x\": [3], \"y\": 4 }" delete backup specified by user data
41
target base_0000000100000000000000C9_D_0000000100000000000000C4 delete delta backup and all dependant delta backups
42
target FIND_FULL base_0000000100000000000000C9_D_0000000100000000000000C4 delete delta backup and all delta backups with the same base backup` //nolint:lll
44
DeleteEverythingUsageExample = "everything [FORCE]"
45
DeleteRetainUsageExample = "retain [FULL|FIND_FULL] backup_count"
46
DeleteBeforeUsageExample = "before [FIND_FULL] backup_name|timestamp"
47
DeleteTargetUsageExample = "target [FIND_FULL] backup_name | --target-user-data <data>"
49
DeleteTargetUserDataFlag = "target-user-data"
50
DeleteTargetUserDataDescription = "delete storage backup which has the specified user data"
53
var StringModifiers = []string{"FULL", "FIND_FULL"}
54
var StringModifiersDeleteEverything = []string{"FORCE"}
55
var errNotFound = errors.New("not found")
56
var errIncorrectArguments = errors.New("incorrect arguments")
58
// BackupObject represents
59
// the backup sentinel object uploaded on storage
60
type BackupObject interface {
62
multistorage.StorageTeller
63
GetBackupTime() time.Time
64
GetBackupName() string
66
// TODO: move increment info into separate struct (in backup.go)
68
GetBaseBackupName() string
69
GetIncrementFromName() string
72
type DeleteHandlerOption func(h *DeleteHandler)
74
func IsPermanentFunc(isPermanent func(storage.Object) bool) DeleteHandlerOption {
75
return func(h *DeleteHandler) {
76
h.isPermanent = isPermanent
81
folder storage.Folder,
82
backups []BackupObject,
83
less func(object1, object2 storage.Object) bool,
84
options ...DeleteHandlerOption,
86
deleteHandler := &DeleteHandler{
90
greater: func(object1, object2 storage.Object) bool {
91
return less(object2, object1)
93
// by default, all storage objects are impermanent
94
isPermanent: func(storage.Object) bool { return false },
97
for _, option := range options {
104
type DeleteHandler struct {
105
Folder storage.Folder
106
backups []BackupObject
108
less func(object1, object2 storage.Object) bool
109
greater func(object1, object2 storage.Object) bool
111
isPermanent func(object storage.Object) bool
114
func (h *DeleteHandler) HandleDeleteBefore(args []string, confirmed bool) {
115
modifier, beforeStr := ExtractDeleteModifierFromArgs(args)
117
target, err := h.FindTargetBefore(beforeStr, modifier)
118
tracelog.ErrorLogger.FatalOnError(err)
120
tracelog.InfoLogger.Printf("No backup found for deletion")
124
err = h.DeleteBeforeTarget(target, confirmed)
125
tracelog.ErrorLogger.FatalOnError(err)
128
func (h *DeleteHandler) HandleDeleteRetain(args []string, confirmed bool) {
129
modifier, retentionStr := ExtractDeleteModifierFromArgs(args)
130
retentionCount, err := strconv.Atoi(retentionStr)
131
tracelog.ErrorLogger.FatalOnError(err)
133
target, err := h.FindTargetRetain(retentionCount, modifier)
134
tracelog.ErrorLogger.FatalOnError(err)
136
tracelog.InfoLogger.Printf("No backup found for deletion")
139
err = h.DeleteBeforeTarget(target, confirmed)
140
tracelog.ErrorLogger.FatalOnError(err)
143
func (h *DeleteHandler) HandleDeleteRetainAfter(args []string, confirmed bool) {
144
modifier, retentionSir, afterStr := ExtractDeleteRetainAfterModifierFromArgs(args)
145
retentionCount, err := strconv.Atoi(retentionSir)
146
tracelog.ErrorLogger.FatalOnError(err)
148
target, err := h.FindTargetRetainAfter(retentionCount, afterStr, modifier)
149
tracelog.ErrorLogger.FatalOnError(err)
152
tracelog.InfoLogger.Printf("No backup found for deletion")
156
err = h.DeleteBeforeTarget(target, confirmed)
157
tracelog.ErrorLogger.FatalOnError(err)
160
func (h *DeleteHandler) HandleDeleteTarget(targetSelector BackupSelector, confirmed, findFull bool) {
161
target, err := h.FindTargetBySelector(targetSelector)
162
tracelog.ErrorLogger.FatalOnError(err)
165
// since we want to delete the target backup, we should fail if
166
// we didn't find the requested backup for deletion
167
tracelog.ErrorLogger.Fatal("Requested backup was not found")
170
folderFilter := func(name string) bool { return true }
171
err = h.DeleteTarget(target, confirmed, findFull, folderFilter)
172
tracelog.ErrorLogger.FatalOnError(err)
175
func (h *DeleteHandler) HandleDeleteEverything(args []string, permanentBackups []string, confirmed bool) {
176
forceModifier := false
177
modifier := ExtractDeleteEverythingModifierFromArgs(args)
178
if modifier == ForceDeleteModifier {
182
if len(permanentBackups) > 0 {
184
tracelog.ErrorLogger.Fatalf("Found permanent backups=%v\n", permanentBackups)
186
tracelog.InfoLogger.Printf("Found permanent backups=%v\n", permanentBackups)
188
h.DeleteEverything(confirmed)
192
func (h *DeleteHandler) FindTargetBefore(beforeStr string, modifier int) (BackupObject, error) {
193
timeLine, err := time.Parse(time.RFC3339, beforeStr)
195
return h.FindTargetBeforeTime(timeLine, modifier)
198
return h.FindTargetBeforeName(beforeStr, modifier)
201
func (h *DeleteHandler) FindTargetBeforeName(name string, modifier int) (BackupObject, error) {
202
choiceFunc := getBeforeChoiceFunc(name, modifier)
203
if choiceFunc == nil {
204
return nil, utility.NewForbiddenActionError("Not allowed modifier for 'delete before'")
206
return findTarget(h.backups, h.greater, choiceFunc)
210
func (h *DeleteHandler) FindTargetBeforeTime(timeLine time.Time, modifier int) (BackupObject, error) {
211
potentialTarget, err := findTarget(h.backups, h.less, func(object BackupObject) bool {
212
backupTime := object.GetBackupTime()
213
return timeLine.Before(backupTime) || timeLine.Equal(backupTime)
215
if err != nil && err != errNotFound {
218
if potentialTarget == nil {
222
return h.FindTargetBeforeName(potentialTarget.GetName(), modifier)
225
func (h *DeleteHandler) FindTargetRetain(retentionCount, modifier int) (BackupObject, error) {
226
choiceFunc := getRetainChoiceFunc(retentionCount, modifier)
227
if choiceFunc == nil {
228
return nil, utility.NewForbiddenActionError("Not allowed modifier for 'delete retain'")
230
target, err := findTarget(h.backups, h.greater, choiceFunc)
231
// it is OK to have no backups found outside the specified retain window, skip this error
232
if err != nil && err != errNotFound {
239
func (h *DeleteHandler) FindTargetByName(bname string) (BackupObject, error) {
240
return findTarget(h.backups, h.greater, func(object BackupObject) bool {
241
return strings.HasPrefix(object.GetName(), bname)
246
func (h *DeleteHandler) FindTargetBySelector(targetSelector BackupSelector) (BackupObject, error) {
247
targetBackup, err := targetSelector.Select(h.Folder)
249
if _, ok := err.(NoBackupsFoundError); ok {
250
// it is OK to have no backups found for the provided selector,
251
// just return no target found
257
var target BackupObject
258
for idx := range h.backups {
259
if h.backups[idx].GetBackupName() == targetBackup.Name {
260
target = h.backups[idx]
269
func (h *DeleteHandler) FindTargetRetainAfter(retentionCount int, afterStr string, modifier int) (BackupObject, error) {
270
timeLine, err := time.Parse(time.RFC3339, afterStr)
272
return h.FindTargetRetainAfterTime(retentionCount, timeLine, modifier)
275
return h.FindTargetRetainAfterName(retentionCount, afterStr, modifier)
279
func (h *DeleteHandler) FindTargetRetainAfterName(
280
retentionCount int, name string, modifier int) (BackupObject, error) {
281
choiceFuncRetain := getRetainChoiceFunc(retentionCount, modifier)
282
if choiceFuncRetain == nil {
283
return nil, utility.NewForbiddenActionError("Not allowed modifier for 'delete before'")
286
choiceFuncAfterName := func(object BackupObject) bool {
287
meetName = meetName || strings.HasPrefix(object.GetName(), name)
288
if modifier == NoDeleteModifier {
291
return meetName && object.IsFullBackup()
294
target1, err := findTarget(h.backups, h.greater, choiceFuncRetain)
295
if err != nil && err != errNotFound {
298
target2, err := findTarget(h.backups, h.less, choiceFuncAfterName)
299
if err != nil && err != errNotFound {
303
if h.greater(target2, target1) {
310
func (h *DeleteHandler) FindTargetRetainAfterTime(retentionCount int, timeLine time.Time, modifier int,
311
) (BackupObject, error) {
312
choiceFuncRetain := getRetainChoiceFunc(retentionCount, modifier)
313
if choiceFuncRetain == nil {
314
return nil, utility.NewForbiddenActionError("Not allowed modifier for 'delete retain'")
316
choiceFuncAfter := func(object BackupObject) bool {
317
backupTime := object.GetBackupTime()
318
timeCheck := timeLine.Before(backupTime) || timeLine.Equal(backupTime)
319
if modifier == NoDeleteModifier {
322
return timeCheck && object.IsFullBackup()
325
target1, err := findTarget(h.backups, h.greater, choiceFuncRetain)
326
if err != nil && err != errNotFound {
329
target2, err := findTarget(h.backups, h.less, choiceFuncAfter)
330
if err != nil && err != errNotFound {
341
if h.greater(target2, target1) {
348
func (h *DeleteHandler) DeleteEverything(confirmed bool) {
349
filter := func(object storage.Object) bool { return true }
350
folderFilter := func(path string) bool { return true }
351
err := DeleteObjectsWhere(h.Folder, confirmed, filter, folderFilter)
352
tracelog.ErrorLogger.FatalOnError(err)
355
func (h *DeleteHandler) DeleteBeforeTarget(target BackupObject, confirmed bool) error {
356
objFilter := func(storage.Object) bool { return true }
357
folderFilter := func(string) bool { return true }
359
return h.DeleteBeforeTargetWhere(target, confirmed, objFilter, folderFilter)
362
func (h *DeleteHandler) DeleteBeforeTargetWhere(
365
objSelector func(object storage.Object) bool,
366
folderFilter func(name string) bool,
368
if !target.IsFullBackup() {
369
errorMessage := "%v is incremental and it's predecessors cannot be deleted. Consider FIND_FULL option."
370
return utility.NewForbiddenActionError(fmt.Sprintf(errorMessage, target.GetName()))
372
tracelog.InfoLogger.Println("Start delete")
374
return DeleteObjectsWhere(h.Folder, confirmed, func(object storage.Object) bool {
375
return objSelector(object) && h.less(object, target) && !h.isPermanent(object)
379
func (h *DeleteHandler) DeleteTarget(target BackupObject, confirmed, findFull bool,
380
folderFilter func(name string) bool) error {
381
var backupsToDelete []BackupObject
383
// delete all backups with the same base backup as the target
384
backupsToDelete = h.findRelatedBackups(target)
386
// delete all dependant backups
387
backupsToDelete = h.findDependantBackups(target)
389
tracelog.DebugLogger.Printf("backupsToDelete: %v", backupsToDelete)
391
backupNamesToDelete := make(map[string]bool)
392
for _, bTarget := range backupsToDelete {
393
if h.isPermanent(bTarget) {
394
tracelog.ErrorLogger.Fatalf("Unable to delete permanent backup %s\n", bTarget.GetName())
396
backupNamesToDelete[bTarget.GetBackupName()] = true
399
return DeleteObjectsWhere(h.Folder.GetSubFolder(utility.BaseBackupPath),
400
confirmed, func(object storage.Object) bool {
401
return backupNamesToDelete[utility.StripLeftmostBackupName(object.GetName())] && !h.isPermanent(object)
406
// Find all backups related to the target.
407
// All delta backups with the same base backup are considered as related.
408
func (h *DeleteHandler) findRelatedBackups(target BackupObject) []BackupObject {
409
relatedBackups := make([]BackupObject, 0)
411
var related func(target BackupObject, other BackupObject) bool
412
if target.IsFullBackup() {
413
related = func(target BackupObject, other BackupObject) bool {
414
// remove base backup
415
isBaseBackup := target.GetBackupName() == other.GetBackupName()
416
// remove all increments from the target backup too
417
isIncrement := target.GetBackupName() == other.GetBaseBackupName()
418
return isBaseBackup || isIncrement
421
related = func(target BackupObject, other BackupObject) bool {
422
// remove base backup
423
isBaseBackup := target.GetBaseBackupName() == other.GetBackupName()
424
// remove all other increments from the target base backup
425
hasCommonBaseBackup := target.GetBaseBackupName() == other.GetBaseBackupName()
426
return isBaseBackup || hasCommonBaseBackup
430
for _, backup := range h.backups {
431
if related(target, backup) {
432
relatedBackups = append(relatedBackups, backup)
435
return relatedBackups
439
// Find all backups dependant on the target.
440
// All delta backups which have the target as the ancestor in increment chain
441
// are considered as dependant.
442
func (h *DeleteHandler) findDependantBackups(target BackupObject) []BackupObject {
443
dependantBackups := make([]BackupObject, 0)
445
incrementsByBackup := make(map[string][]BackupObject)
446
for _, backup := range h.backups {
447
if !backup.IsFullBackup() {
448
incrementFrom := backup.GetIncrementFromName()
449
incrementsByBackup[incrementFrom] = append(incrementsByBackup[incrementFrom], backup)
453
queue := []BackupObject{target}
454
var curr BackupObject
456
curr, queue = queue[0], queue[1:]
457
dependantBackups = append(dependantBackups, curr)
458
queue = append(queue, incrementsByBackup[curr.GetBackupName()]...)
460
return dependantBackups
463
func DeleteObjectsWhere(
464
folder storage.Folder,
466
objFilter func(object1 storage.Object) bool,
467
folderFilter func(name string) bool,
469
relativePathObjects, err := multistorage.ListFolderRecursivelyWithFilter(folder, folderFilter)
473
filteredRelativePaths := make([]string, 0)
474
tracelog.InfoLogger.Println("Objects in folder:")
475
for _, object := range relativePathObjects {
476
if objFilter(object) {
477
tracelog.InfoLogger.Printf("\twill be deleted: %s, from storage: %s\n", object.GetName(), multistorage.GetStorage(object))
478
filteredRelativePaths = append(filteredRelativePaths, object.GetName())
480
tracelog.DebugLogger.Printf("\tskipped: %s, in storage: %s\n", object.GetName(), multistorage.GetStorage(object))
483
if len(filteredRelativePaths) == 0 {
487
return folder.DeleteObjects(filteredRelativePaths)
489
tracelog.InfoLogger.Println("Dry run, nothing were deleted")
493
func findTarget(objects []BackupObject,
494
compare func(object1, object2 storage.Object) bool,
495
isTarget func(object BackupObject) bool) (BackupObject, error) {
496
sort.Slice(objects, func(i, j int) bool {
497
return compare(objects[i], objects[j])
499
for _, object := range objects {
500
tracelog.DebugLogger.Printf("processing %s\n", object.GetName())
501
if isTarget(object) {
505
return nil, errNotFound
508
func getBeforeChoiceFunc(name string, modifier int) func(object BackupObject) bool {
511
case NoDeleteModifier:
512
return func(object BackupObject) bool {
513
return strings.HasPrefix(object.GetName(), name)
515
case FindFullDeleteModifier:
516
return func(object BackupObject) bool {
517
meetName = meetName || strings.HasPrefix(object.GetName(), name)
518
return meetName && object.IsFullBackup()
524
func getRetainChoiceFunc(retentionCount, modifier int) func(object BackupObject) bool {
525
uniqueNames := map[string]bool{}
527
case NoDeleteModifier:
528
return func(object BackupObject) bool {
529
uniqueNames[object.GetBackupName()] = true
531
return len(uniqueNames) == retentionCount
533
case FullDeleteModifier:
534
return func(object BackupObject) bool {
535
if object.IsFullBackup() {
536
uniqueNames[object.GetBackupName()] = true
538
if len(uniqueNames) == retentionCount {
543
case FindFullDeleteModifier:
544
return func(object BackupObject) bool {
545
uniqueNames[object.GetBackupName()] = true
546
if len(uniqueNames) >= retentionCount && object.IsFullBackup() {
555
// ExtractDeleteRetainAfterModifierFromArgs extracts the args for the "delete retain --after" command
556
func ExtractDeleteRetainAfterModifierFromArgs(args []string) (int, string, string) {
558
return NoDeleteModifier, args[0], args[1]
559
} else if args[0] == StringModifiers[0] {
560
return FullDeleteModifier, args[1], args[2]
562
return FindFullDeleteModifier, args[1], args[2]
565
// ExtractDeleteEverythingModifierFromArgs extracts the args for the "delete everything" command
566
func ExtractDeleteEverythingModifierFromArgs(args []string) int {
568
return NoDeleteModifier
570
return ForceDeleteModifier
573
// ExtractDeleteTargetModifierFromArgs extracts the args for the "delete target" command
574
func ExtractDeleteTargetModifierFromArgs(args []string) int {
575
if len(args) >= 1 && args[0] == StringModifiers[1] {
576
return FindFullDeleteModifier
579
return NoDeleteModifier
582
// ExtractDeleteModifierFromArgs extracts the delete modifier the "delete retain"/"delete before" commands
583
func ExtractDeleteModifierFromArgs(args []string) (int, string) {
585
return NoDeleteModifier, args[0]
586
} else if args[0] == StringModifiers[0] {
587
return FullDeleteModifier, args[1]
589
return FindFullDeleteModifier, args[1]
592
func DeleteBeforeArgsValidator(cmd *cobra.Command, args []string) error {
593
err := DeleteArgsValidator(args, StringModifiers, 1, 2)
597
modifier, beforeStr := ExtractDeleteModifierFromArgs(args)
598
if modifier == FullDeleteModifier {
599
return fmt.Errorf("unsupported moodifier for delete before command")
601
if before, err := time.Parse(time.RFC3339, beforeStr); err == nil {
602
if before.After(utility.TimeNowCrossPlatformUTC()) {
603
return fmt.Errorf("cannot delete before future date")
609
func DeleteTargetArgsValidator(cmd *cobra.Command, args []string) error {
610
err := cobra.RangeArgs(0, 2)(cmd, args)
616
case len(args) == 0 && !cmd.Flags().Changed(DeleteTargetUserDataFlag):
617
// allow 0 arguments only when target user data flag is set
618
return errIncorrectArguments
620
case len(args) == 2 && args[0] != StringModifiers[1]:
621
return errIncorrectArguments
628
func DeleteEverythingArgsValidator(cmd *cobra.Command, args []string) error {
629
return DeleteArgsValidator(args, StringModifiersDeleteEverything, 0, 1)
632
func DeleteRetainArgsValidator(cmd *cobra.Command, args []string) error {
633
_, retentionStr := ExtractDeleteModifierFromArgs(args)
634
retentionNumber, err := strconv.Atoi(retentionStr)
636
return errors.Wrapf(err, "expected to get a number as retantion count, but got: '%s'", retentionStr)
638
if retentionNumber <= 0 {
639
return fmt.Errorf("cannot retain less than one backup. Check out delete everything")
644
func DeleteRetainAfterArgsValidator(cmd *cobra.Command, args []string) error {
645
err := DeleteArgsValidator(args, StringModifiers, 2, 3)
649
_, retentionStr, afterStr := ExtractDeleteRetainAfterModifierFromArgs(args)
650
retentionNumber, err := strconv.Atoi(retentionStr)
652
return errors.Wrapf(err, "expected to get a number as retantion count, but got: '%s'", retentionStr)
654
if retentionNumber <= 0 {
655
return fmt.Errorf("cannot retain less than one backup. Check out delete everything")
657
if before, err := time.Parse(time.RFC3339, afterStr); err == nil {
658
if before.After(utility.TimeNowCrossPlatformUTC()) {
659
return fmt.Errorf("cannot delete retain future date")
665
func DeleteArgsValidator(args, stringModifiers []string, minArgs int, maxArgs int) error {
666
if len(args) < minArgs || len(args) > maxArgs {
667
return fmt.Errorf("accepts between %d and %d arg(s), received %d", minArgs, maxArgs, len(args))
669
if len(args) == maxArgs {
670
expectedModifier := args[0]
671
isModifierInList := false
672
for _, modifier := range stringModifiers {
673
if isModifierInList = modifier == expectedModifier; isModifierInList {
677
if !isModifierInList {
678
return fmt.Errorf("expected to get one of modifiers: %v as first argument", stringModifiers)