Skip to content

Commit a392e52

Browse files
authored
Merge pull request noobaa#1726 from dannyzaken/danny-db-backup
2 parents 9968968 + b5825fe commit a392e52

File tree

7 files changed

+300
-4
lines changed

7 files changed

+300
-4
lines changed

deploy/role.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,12 @@ rules:
160160
- '*'
161161
verbs:
162162
- '*'
163+
- apiGroups:
164+
- snapshot.storage.k8s.io
165+
resources:
166+
- volumesnapshots
167+
verbs:
168+
- get
169+
- list
170+
- watch
171+
- delete

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
github.com/google/uuid v1.6.0
2929
github.com/kedacore/keda/v2 v2.7.0
3030
github.com/kube-object-storage/lib-bucket-provisioner v0.0.0-20221122204822-d1a8c34382f1
31+
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0
3132
github.com/libopenstorage/secrets v0.0.0-20240416031220-a17cf7f72c6c
3233
github.com/marstr/randname v0.0.0-20200428202425-99aca53a2176
3334
github.com/onsi/ginkgo/v2 v2.23.4
@@ -103,7 +104,6 @@ require (
103104
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
104105
github.com/hashicorp/vault/api/auth/kubernetes v0.8.0 // indirect
105106
github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 // indirect
106-
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 // indirect
107107
github.com/kylelemons/godebug v1.1.0 // indirect
108108
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
109109
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect

pkg/apis/noobaa/v1alpha1/cnpg_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,25 @@ func init() {
3333
APIVersion: "postgresql.cnpg.noobaa.io/v1",
3434
Kind: "ImageCatalogList",
3535
},
36+
}, &cnpgv1.ScheduledBackup{
37+
TypeMeta: metav1.TypeMeta{
38+
APIVersion: "postgresql.cnpg.noobaa.io/v1",
39+
Kind: "ScheduledBackup",
40+
},
41+
}, &cnpgv1.ScheduledBackupList{
42+
TypeMeta: metav1.TypeMeta{
43+
APIVersion: "postgresql.cnpg.noobaa.io/v1",
44+
Kind: "ScheduledBackupList",
45+
},
46+
}, &cnpgv1.Backup{
47+
TypeMeta: metav1.TypeMeta{
48+
APIVersion: "postgresql.cnpg.noobaa.io/v1",
49+
Kind: "Backup",
50+
},
51+
}, &cnpgv1.BackupList{
52+
TypeMeta: metav1.TypeMeta{
53+
APIVersion: "postgresql.cnpg.noobaa.io/v1",
54+
Kind: "BackupList",
55+
},
3656
})
3757
}

pkg/bundle/deploy.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6509,7 +6509,7 @@ spec:
65096509
# name: socket
65106510
`
65116511

6512-
const Sha256_deploy_role_yaml = "6d98627f4b3c9834710856edf01c6ed71fcefe1e78b15189343423ecc524e18d"
6512+
const Sha256_deploy_role_yaml = "22c739e1b81a9d3c1b167b89c0e8e6757b2981b1434071aa4441332ec4c68d64"
65136513

65146514
const File_deploy_role_yaml = `apiVersion: rbac.authorization.k8s.io/v1
65156515
kind: Role
@@ -6673,6 +6673,15 @@ rules:
66736673
- '*'
66746674
verbs:
66756675
- '*'
6676+
- apiGroups:
6677+
- snapshot.storage.k8s.io
6678+
resources:
6679+
- volumesnapshots
6680+
verbs:
6681+
- get
6682+
- list
6683+
- watch
6684+
- delete
66766685
`
66776686

66786687
const Sha256_deploy_role_binding_yaml = "59a2627156ed3db9cd1a4d9c47e8c1044279c65e84d79c525e51274329cb16ff"

pkg/cnpg/cnpg.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,3 +594,38 @@ func GetCnpgClusterObj(namespace string, name string) *cnpgv1.Cluster {
594594
}
595595
return cnpgCluster
596596
}
597+
598+
func GetCnpgScheduledBackupObj(namespace string, name string) *cnpgv1.ScheduledBackup {
599+
cnpgScheduledBackup := &cnpgv1.ScheduledBackup{
600+
TypeMeta: metav1.TypeMeta{
601+
APIVersion: CnpgAPIVersion,
602+
Kind: "ScheduledBackup",
603+
},
604+
ObjectMeta: metav1.ObjectMeta{
605+
Name: name,
606+
Namespace: namespace,
607+
},
608+
Spec: cnpgv1.ScheduledBackupSpec{},
609+
}
610+
return cnpgScheduledBackup
611+
}
612+
613+
func GetCnpgBackupObj(namespace string, name string) *cnpgv1.Backup {
614+
cnpgBackup := &cnpgv1.Backup{
615+
TypeMeta: metav1.TypeMeta{
616+
APIVersion: CnpgAPIVersion,
617+
Kind: "Backup",
618+
},
619+
}
620+
return cnpgBackup
621+
}
622+
623+
func GetCnpgBackupListObj(namespace string) *cnpgv1.BackupList {
624+
cnpgBackupList := &cnpgv1.BackupList{
625+
TypeMeta: metav1.TypeMeta{
626+
APIVersion: CnpgAPIVersion,
627+
Kind: "BackupList",
628+
},
629+
}
630+
return cnpgBackupList
631+
}

pkg/system/db_reconciler.go

Lines changed: 223 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ import (
44
"fmt"
55
"reflect"
66
"slices"
7+
"strings"
78

89
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
910
"github.com/go-test/deep"
11+
storagesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
1012
nbv1 "github.com/noobaa/noobaa-operator/v5/pkg/apis/noobaa/v1alpha1"
1113
"github.com/noobaa/noobaa-operator/v5/pkg/cnpg"
1214
"github.com/noobaa/noobaa-operator/v5/pkg/options"
1315
"github.com/noobaa/noobaa-operator/v5/pkg/util"
1416
corev1 "k8s.io/api/core/v1"
17+
"k8s.io/apimachinery/pkg/api/errors"
1518
"k8s.io/apimachinery/pkg/api/resource"
1619
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
1721
)
1822

1923
const (
@@ -94,6 +98,12 @@ func (r *Reconciler) ReconcileCNPGCluster() error {
9498
return fmt.Errorf("cnpg cluster is not ready")
9599
}
96100

101+
// Reconcile backup configuration
102+
if err := r.reconcileDBBackup(); err != nil {
103+
r.cnpgLogError("got error reconciling backup. error: %v", err)
104+
return err
105+
}
106+
97107
return nil
98108
}
99109

@@ -268,6 +278,28 @@ func (r *Reconciler) reconcileClusterSpec(dbSpec *nbv1.NooBaaDBSpec) error {
268278

269279
r.setPostgresConfig()
270280

281+
// Configure backup settings if specified
282+
if backupSpec := r.NooBaa.Spec.DBSpec.DBBackup; backupSpec != nil {
283+
if backupSpec.VolumeSnapshot == nil {
284+
r.cnpgLogError("volume snapshot backup configuration is not specified")
285+
return fmt.Errorf("volume snapshot backup configuration is not specified")
286+
}
287+
offlineBackup := false
288+
if r.CNPGCluster.Spec.Backup == nil {
289+
r.CNPGCluster.Spec.Backup = &cnpgv1.BackupConfiguration{
290+
VolumeSnapshot: &cnpgv1.VolumeSnapshotConfiguration{
291+
ClassName: backupSpec.VolumeSnapshot.VolumeSnapshotClass,
292+
Online: &offlineBackup,
293+
},
294+
}
295+
} else {
296+
r.CNPGCluster.Spec.Backup.VolumeSnapshot.ClassName = backupSpec.VolumeSnapshot.VolumeSnapshotClass
297+
}
298+
} else {
299+
// Remove backup configuration if not specified
300+
r.CNPGCluster.Spec.Backup = nil
301+
}
302+
271303
// TODO: consider specifying a separate WAL storage configuration in Spec.WalStorage
272304
// currently, the same storage will be used for both DB and WAL
273305

@@ -374,6 +406,170 @@ func (r *Reconciler) reconcileClusterCreateOrImport() error {
374406

375407
}
376408

409+
// reconcileDBBackup reconciles the backup configuration for the CNPG cluster
410+
func (r *Reconciler) reconcileDBBackup() error {
411+
if r.NooBaa.Spec.DBSpec.DBBackup == nil {
412+
// Clean up existing backup resources if any
413+
if err := r.cleanupDBBackup(); err != nil {
414+
r.cnpgLogError("got error cleaning up existing backup resources. error: %v", err)
415+
return err
416+
}
417+
r.NooBaa.Status.DBStatus.BackupStatus = nil
418+
return nil
419+
}
420+
421+
if r.NooBaa.Status.DBStatus.BackupStatus == nil {
422+
r.NooBaa.Status.DBStatus.BackupStatus = &nbv1.DBBackupStatus{}
423+
}
424+
425+
// currently only volume snapshot backup is supported. VolumeSnapshot is required.
426+
if r.NooBaa.Spec.DBSpec.DBBackup.VolumeSnapshot == nil {
427+
r.cnpgLogError("volume snapshot backup configuration is not specified")
428+
return fmt.Errorf("volume snapshot backup configuration is not specified")
429+
}
430+
431+
// Create or update ScheduledBackup
432+
if err := r.reconcileScheduledBackup(); err != nil {
433+
r.cnpgLogError("got error reconciling scheduled backup. error: %v", err)
434+
return err
435+
}
436+
437+
// reconcile the backup retention
438+
if err := r.reconcileBackupRetention(); err != nil {
439+
r.cnpgLogError("got error reconciling backup retention. error: %v", err)
440+
return err
441+
}
442+
443+
return nil
444+
}
445+
446+
func (r *Reconciler) getBackupResourceName() string {
447+
return r.CNPGCluster.Name + "-backup"
448+
}
449+
450+
func (r *Reconciler) reconcileScheduledBackup() error {
451+
backupSpec := r.NooBaa.Spec.DBSpec.DBBackup
452+
offlineBackup := false
453+
// convert the standard cron schedule to the cnpg cron schedule
454+
cnpgSchedule, err := convertToSixFieldCron(backupSpec.Schedule)
455+
if err != nil {
456+
r.cnpgLogError("failed to convert cron schedule to six-field format. error: %v", err)
457+
return err
458+
}
459+
scheduledBackup := cnpg.GetCnpgScheduledBackupObj(r.CNPGCluster.Namespace, r.getBackupResourceName())
460+
461+
return r.ReconcileObject(scheduledBackup, func() error {
462+
// Update spec if needed
463+
scheduledBackup.Spec.Schedule = cnpgSchedule
464+
scheduledBackup.Spec.Cluster.Name = r.CNPGCluster.Name
465+
scheduledBackup.Spec.Method = cnpgv1.BackupMethodVolumeSnapshot
466+
scheduledBackup.Spec.Online = &offlineBackup
467+
if scheduledBackup.Status.LastScheduleTime != nil {
468+
r.NooBaa.Status.DBStatus.BackupStatus.LastBackupTime = scheduledBackup.Status.LastScheduleTime
469+
}
470+
if scheduledBackup.Status.NextScheduleTime != nil {
471+
r.NooBaa.Status.DBStatus.BackupStatus.NextBackupTime = scheduledBackup.Status.NextScheduleTime
472+
}
473+
return nil
474+
})
475+
}
476+
477+
func (r *Reconciler) reconcileBackupRetention() error {
478+
maxSnapshots := r.NooBaa.Spec.DBSpec.DBBackup.VolumeSnapshot.MaxSnapshots
479+
if maxSnapshots == 0 {
480+
r.cnpgLog("backup retention is not specified, skipping")
481+
return nil
482+
}
483+
484+
// list all backupResources created as part of the scheduled backup
485+
backups, err := r.listBackupResourcesOrderByCreate()
486+
if err != nil {
487+
r.cnpgLogError("got error listing cluster volume snapshots. error: %v", err)
488+
return err
489+
}
490+
totalSnapshots := len(backups.Items)
491+
avaialableItems := backups.Items
492+
if totalSnapshots > maxSnapshots {
493+
numToDelete := totalSnapshots - maxSnapshots
494+
avaialableItems = avaialableItems[numToDelete:]
495+
r.cnpgLog("found %d backups, maxSnapshots is %d, deleting the oldest %d backups", totalSnapshots, maxSnapshots, numToDelete)
496+
// delete the oldest backups
497+
for _, backup := range backups.Items[:numToDelete] {
498+
snapshotName := backup.Name
499+
if len(backup.Status.BackupSnapshotStatus.Elements) > 0 {
500+
// although the expected snapshot name is the same as the backup name, take it from the backup status to be on the safe side
501+
snapshotName = backup.Status.BackupSnapshotStatus.Elements[0].Name
502+
}
503+
504+
r.cnpgLog("deleting snapshot %s", snapshotName)
505+
// if encountering an error we only report it and continue with the reconciliation
506+
if err := r.Client.Delete(r.Ctx, &storagesnapshotv1.VolumeSnapshot{
507+
ObjectMeta: metav1.ObjectMeta{
508+
Name: snapshotName,
509+
Namespace: r.CNPGCluster.Namespace,
510+
},
511+
}); err != nil {
512+
r.cnpgLogError("got error deleting snapshot %s. error: %v", snapshotName, err)
513+
// skipping the deletion of the backup so we can try to delete again next time
514+
continue
515+
}
516+
517+
r.cnpgLog("deleting backup %s", backup.Name)
518+
if err := r.Client.Delete(r.Ctx, &backup); err != nil {
519+
r.cnpgLogError("got error deleting backup %s. error: %v", backup.Name, err)
520+
continue
521+
}
522+
totalSnapshots--
523+
}
524+
}
525+
526+
// update the backup status
527+
r.NooBaa.Status.DBStatus.BackupStatus.TotalSnapshots = totalSnapshots
528+
avaiallableSnapshots := []string{}
529+
for _, item := range avaialableItems {
530+
avaiallableSnapshots = append(avaiallableSnapshots, item.Name)
531+
}
532+
r.NooBaa.Status.DBStatus.BackupStatus.AvailableSnapshots = avaiallableSnapshots
533+
534+
return nil
535+
}
536+
537+
func (r *Reconciler) listBackupResourcesOrderByCreate() (*cnpgv1.BackupList, error) {
538+
backupResources := cnpg.GetCnpgBackupListObj(r.CNPGCluster.Namespace)
539+
// list all backup resources having the label cnpg.io/scheduled-backup: <scheduled-backup-name>
540+
if err := r.Client.List(r.Ctx, backupResources, client.InNamespace(r.CNPGCluster.Namespace), client.MatchingLabels{
541+
"cnpg.io/scheduled-backup": r.getBackupResourceName(),
542+
}); err != nil {
543+
r.cnpgLogError("got error listing backup resources. error: %v", err)
544+
return nil, err
545+
}
546+
// filter to only include completed backups (MatchingFields requires field index which isn't configured)
547+
filteredItems := []cnpgv1.Backup{}
548+
for _, backup := range backupResources.Items {
549+
if backup.Status.Phase == cnpgv1.BackupPhaseCompleted {
550+
filteredItems = append(filteredItems, backup)
551+
}
552+
}
553+
// sort the list by creation timestamp
554+
slices.SortFunc(filteredItems, func(a, b cnpgv1.Backup) int {
555+
return a.CreationTimestamp.Compare(b.CreationTimestamp.Time)
556+
})
557+
backupResources.Items = filteredItems
558+
return backupResources, nil
559+
}
560+
561+
func (r *Reconciler) cleanupDBBackup() error {
562+
scheduledBackup := cnpg.GetCnpgScheduledBackupObj(r.CNPGCluster.Namespace, r.getBackupResourceName())
563+
564+
if util.KubeCheckQuiet(scheduledBackup) {
565+
r.cnpgLog("removing ScheduledBackup %s", scheduledBackup.Name)
566+
if err := r.Client.Delete(r.Ctx, scheduledBackup); err != nil && !errors.IsNotFound(err) {
567+
return err
568+
}
569+
}
570+
return nil
571+
}
572+
377573
func (r *Reconciler) setPostgresConfig() {
378574

379575
// set postgresql configuration
@@ -515,11 +711,35 @@ func (r *Reconciler) cnpgLogError(format string, args ...interface{}) {
515711
r.Logger.Errorf("cnpg:: "+format, args...)
516712
}
517713

714+
// convertToSixFieldCron converts a cron schedule to six-field format.
715+
// cnpg requires the schedule to be in six-field format, so we need to convert the standard cron schedule to six-field format.
716+
func convertToSixFieldCron(schedule string) (string, error) {
717+
// If it starts with @, it's a descriptor or interval - return as-is
718+
if len(schedule) > 0 && schedule[0] == '@' {
719+
return schedule, nil
720+
}
721+
722+
// Count the number of space-separated fields
723+
fields := strings.Fields(schedule)
724+
725+
// If already 6 fields, return as-is
726+
if len(fields) == 6 {
727+
return schedule, nil
728+
}
729+
730+
// If 5 fields (standard cron), prepend "0" for seconds
731+
if len(fields) == 5 {
732+
return "0 " + schedule, nil
733+
}
734+
735+
// If not 5 or 6 fields, return an error
736+
return "", fmt.Errorf("invalid cron schedule %q", schedule)
737+
}
738+
518739
// wasClusterSpecChanged checks if any of the cluster spec fields that matter for us were changed.
519740
// This need to be updated if we change more fields in the cluster spec.
520741
// For some reason reflect.DeepEqual always returns false when comparing the entire spec.
521742
func (r *Reconciler) wasClusterSpecChanged(existingClusterSpec *cnpgv1.ClusterSpec) bool {
522-
523743
return !reflect.DeepEqual(existingClusterSpec.InheritedMetadata, r.CNPGCluster.Spec.InheritedMetadata) ||
524744
!reflect.DeepEqual(existingClusterSpec.ImageCatalogRef, r.CNPGCluster.Spec.ImageCatalogRef) ||
525745
existingClusterSpec.Instances != r.CNPGCluster.Spec.Instances ||
@@ -528,5 +748,6 @@ func (r *Reconciler) wasClusterSpecChanged(existingClusterSpec *cnpgv1.ClusterSp
528748
!reflect.DeepEqual(existingClusterSpec.StorageConfiguration.StorageClass, r.CNPGCluster.Spec.StorageConfiguration.StorageClass) ||
529749
!reflect.DeepEqual(existingClusterSpec.StorageConfiguration.Size, r.CNPGCluster.Spec.StorageConfiguration.Size) ||
530750
!reflect.DeepEqual(existingClusterSpec.Monitoring, r.CNPGCluster.Spec.Monitoring) ||
531-
!reflect.DeepEqual(existingClusterSpec.PostgresConfiguration.Parameters, r.CNPGCluster.Spec.PostgresConfiguration.Parameters)
751+
!reflect.DeepEqual(existingClusterSpec.PostgresConfiguration.Parameters, r.CNPGCluster.Spec.PostgresConfiguration.Parameters) ||
752+
!reflect.DeepEqual(existingClusterSpec.Backup, r.CNPGCluster.Spec.Backup)
532753
}

0 commit comments

Comments
 (0)