From 5457a2e669ab36ce03d044391c627ee09e759b6a Mon Sep 17 00:00:00 2001 From: "Johannes M. Scheuermann" Date: Wed, 24 Sep 2025 17:48:20 +0200 Subject: [PATCH 1/2] Add support for one time backup --- api/v1beta2/foundationdbbackup_types.go | 47 ++++- api/v1beta2/foundationdbbackup_types_test.go | 48 ++++++ api/v1beta2/foundationdbcluster_types_test.go | 1 + api/v1beta2/zz_generated.deepcopy.go | 27 +-- ....foundationdb.org_foundationdbbackups.yaml | 13 ++ controllers/modify_backup.go | 4 +- controllers/update_backup_status.go | 8 +- docs/backup_spec.md | 12 +- docs/manual/backup.md | 54 +++++- e2e/fixtures/fdb_backup.go | 7 +- e2e/fixtures/fdb_cluster.go | 28 ++- .../operator_backup_test.go | 161 ++++++++++++------ fdbclient/admin_client.go | 9 +- fdbclient/admin_client_test.go | 31 ++++ pkg/fdbadminclient/mock/admin_client_mock.go | 14 +- 15 files changed, 379 insertions(+), 85 deletions(-) diff --git a/api/v1beta2/foundationdbbackup_types.go b/api/v1beta2/foundationdbbackup_types.go index 9bd7993d..489cdb39 100644 --- a/api/v1beta2/foundationdbbackup_types.go +++ b/api/v1beta2/foundationdbbackup_types.go @@ -33,6 +33,7 @@ import ( // +kubebuilder:metadata:annotations="foundationdb.org/release=v2.14.0" // +kubebuilder:printcolumn:name="Generation",type="integer",JSONPath=".metadata.generation",description="Latest generation of the spec",priority=0 // +kubebuilder:printcolumn:name="Reconciled",type="integer",JSONPath=".status.generations.reconciled",description="Last reconciled generation of the spec",priority=0 +// +kubebuilder:printcolumn:name="Restorable",type="boolean",JSONPath=".status.backupDetails.restorable",description="If the backup is restorable",priority=0 // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:storageversion @@ -134,6 +135,15 @@ type FoundationDBBackupSpec struct { // +kubebuilder:validation:Enum=noop;stop;cleanup // +kubebuilder:default:=noop DeletionPolicy *BackupDeletionPolicy `json:"deletionPolicy,omitempty"` + + // BackupMode defines the backup mode that should be used for the backup. When the BackupMode is set to + // BackupModeOneTime, the backup will create a single snapshot and then stop. When set to BackupModeContinuous, + // the backup will run continuously, creating snapshots at regular intervals defined by SnapshotPeriodSeconds. + // Default: "Continuous". + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=continuous;oneTime + // +kubebuilder:default:=continuous + BackupMode *BackupMode `json:"backupMode,omitempty"` } // BackupType defines the backup type that should be used for the backup. @@ -165,6 +175,18 @@ const ( BackupDeletionPolicyCleanup BackupDeletionPolicy = "cleanup" ) +// BackupMode defines the mode of backup operation. +// +kubebuilder:validation:MaxLength=64 +type BackupMode string + +const ( + // BackupModeContinuous indicates that the backup should run continuously, taking snapshots at regular intervals. + BackupModeContinuous BackupMode = "continuous" + + // BackupModeOneTime indicates that the backup should create a single snapshot and then stop. + BackupModeOneTime BackupMode = "oneTime" +) + // FoundationDBBackupStatus describes the current status of the backup for a cluster. type FoundationDBBackupStatus struct { // AgentCount provides the number of agents that are up-to-date, ready, @@ -191,6 +213,7 @@ type FoundationDBBackupStatusBackupDetails struct { Running bool `json:"running,omitempty"` Paused bool `json:"paused,omitempty"` SnapshotPeriodSeconds int `json:"snapshotTime,omitempty"` + Restorable bool `json:"restorable,omitempty"` } // BackupGenerationStatus stores information on which generations have reached @@ -265,6 +288,13 @@ type BlobStoreConfiguration struct { // ShouldRun determines whether a backup should be running. func (backup *FoundationDBBackup) ShouldRun() bool { + // For one-time backups, don't run if already completed + backupDetails := backup.Status.BackupDetails + if backup.GetBackupMode() == BackupModeOneTime && backupDetails != nil && + backupDetails.Restorable { + return false + } + return backup.Spec.BackupState == "" || backup.Spec.BackupState == BackupStateRunning || backup.Spec.BackupState == BackupStatePaused } @@ -318,6 +348,12 @@ type FoundationDBLiveBackupStatus struct { // BackupAgentsPaused describes whether the backup agents are paused. BackupAgentsPaused bool `json:"BackupAgentsPaused,omitempty"` + + // Restorable if true, the backup can be restored + Restorable *bool `json:"Restorable,omitempty"` + + // LatestRestorablePoint contains information about the latest restorable point if any exists. + LatestRestorablePoint *LatestRestorablePoint `json:"LatestRestorablePoint,omitempty"` } // FoundationDBLiveBackupStatusState provides the state of a backup in the @@ -325,12 +361,6 @@ type FoundationDBLiveBackupStatus struct { type FoundationDBLiveBackupStatusState struct { // Running determines whether the backup is currently running. Running bool `json:"Running,omitempty"` - - // Restorable if true, the backup can be restored - Restorable *bool `json:"Restorable,omitempty"` - - // LatestRestorablePoint contains information about the latest restorable point if any exists. - LatestRestorablePoint *LatestRestorablePoint `json:"LatestRestorablePoint,omitempty"` } // LatestRestorablePoint contains information about the latest restorable point if any exists. @@ -429,6 +459,11 @@ func (backup *FoundationDBBackup) GetDeletionPolicy() BackupDeletionPolicy { return ptr.Deref(backup.Spec.DeletionPolicy, BackupDeletionPolicyNoop) } +// GetBackupMode will return the backup mode for this backup. +func (backup *FoundationDBBackup) GetBackupMode() BackupMode { + return ptr.Deref(backup.Spec.BackupMode, BackupModeContinuous) +} + // UseUnifiedImage returns true if the unified image should be used. func (backup *FoundationDBBackup) UseUnifiedImage() bool { imageType := ImageTypeUnified diff --git a/api/v1beta2/foundationdbbackup_types_test.go b/api/v1beta2/foundationdbbackup_types_test.go index 8acd1b38..f2595f9f 100644 --- a/api/v1beta2/foundationdbbackup_types_test.go +++ b/api/v1beta2/foundationdbbackup_types_test.go @@ -471,4 +471,52 @@ var _ = Describe("[api] FoundationDBBackup", func() { ), ) }) + + When("testing backup mode functionality", func() { + Context("GetBackupMode method", func() { + It("should return continuous as default when BackupMode is nil", func() { + backup.Spec.BackupMode = nil + Expect(backup.GetBackupMode()).To(Equal(BackupModeContinuous)) + }) + + It("should return continuous when explicitly set", func() { + mode := BackupModeContinuous + backup.Spec.BackupMode = &mode + Expect(backup.GetBackupMode()).To(Equal(BackupModeContinuous)) + }) + + It("should return one-time when explicitly set", func() { + mode := BackupModeOneTime + backup.Spec.BackupMode = &mode + Expect(backup.GetBackupMode()).To(Equal(BackupModeOneTime)) + }) + }) + + Context("ShouldRun method for different backup modes", func() { + It("should run continuous backup by default", func() { + backup.Spec.BackupState = BackupStateRunning + Expect(backup.ShouldRun()).To(BeTrue()) + }) + + It("should not run completed one-time backup", func() { + mode := BackupModeOneTime + backup.Spec.BackupMode = &mode + backup.Spec.BackupState = BackupStateRunning + backup.Status.BackupDetails = &FoundationDBBackupStatusBackupDetails{ + Restorable: true, + } + Expect(backup.ShouldRun()).To(BeFalse()) + }) + + It("should run uncompleted one-time backup", func() { + mode := BackupModeOneTime + backup.Spec.BackupMode = &mode + backup.Spec.BackupState = BackupStateRunning + backup.Status.BackupDetails = &FoundationDBBackupStatusBackupDetails{ + Restorable: false, + } + Expect(backup.ShouldRun()).To(BeTrue()) + }) + }) + }) }) diff --git a/api/v1beta2/foundationdbcluster_types_test.go b/api/v1beta2/foundationdbcluster_types_test.go index dbe74ee5..d5231363 100644 --- a/api/v1beta2/foundationdbcluster_types_test.go +++ b/api/v1beta2/foundationdbcluster_types_test.go @@ -837,6 +837,7 @@ var _ = Describe("[api] FoundationDBCluster", func() { Status: FoundationDBLiveBackupStatusState{ Running: true, }, + Restorable: ptr.To(false), })) }) }) diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index ce3a8053..04b38bb4 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -480,6 +480,11 @@ func (in *FoundationDBBackupSpec) DeepCopyInto(out *FoundationDBBackupSpec) { *out = new(BackupDeletionPolicy) **out = **in } + if in.BackupMode != nil { + in, out := &in.BackupMode, &out.BackupMode + *out = new(BackupMode) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FoundationDBBackupSpec. @@ -876,7 +881,17 @@ func (in *FoundationDBKeyRange) DeepCopy() *FoundationDBKeyRange { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FoundationDBLiveBackupStatus) DeepCopyInto(out *FoundationDBLiveBackupStatus) { *out = *in - in.Status.DeepCopyInto(&out.Status) + out.Status = in.Status + if in.Restorable != nil { + in, out := &in.Restorable, &out.Restorable + *out = new(bool) + **out = **in + } + if in.LatestRestorablePoint != nil { + in, out := &in.LatestRestorablePoint, &out.LatestRestorablePoint + *out = new(LatestRestorablePoint) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FoundationDBLiveBackupStatus. @@ -892,16 +907,6 @@ func (in *FoundationDBLiveBackupStatus) DeepCopy() *FoundationDBLiveBackupStatus // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FoundationDBLiveBackupStatusState) DeepCopyInto(out *FoundationDBLiveBackupStatusState) { *out = *in - if in.Restorable != nil { - in, out := &in.Restorable, &out.Restorable - *out = new(bool) - **out = **in - } - if in.LatestRestorablePoint != nil { - in, out := &in.LatestRestorablePoint, &out.LatestRestorablePoint - *out = new(LatestRestorablePoint) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FoundationDBLiveBackupStatusState. diff --git a/config/crd/bases/apps.foundationdb.org_foundationdbbackups.yaml b/config/crd/bases/apps.foundationdb.org_foundationdbbackups.yaml index 84804b2f..42baefe5 100644 --- a/config/crd/bases/apps.foundationdb.org_foundationdbbackups.yaml +++ b/config/crd/bases/apps.foundationdb.org_foundationdbbackups.yaml @@ -26,6 +26,10 @@ spec: jsonPath: .status.generations.reconciled name: Reconciled type: integer + - description: If the backup is restorable + jsonPath: .status.backupDetails.restorable + name: Restorable + type: boolean - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -65,6 +69,13 @@ spec: namespace: type: string type: object + backupMode: + default: continuous + enum: + - continuous + - oneTime + maxLength: 64 + type: string backupState: enum: - Running @@ -3869,6 +3880,8 @@ spec: properties: paused: type: boolean + restorable: + type: boolean running: type: boolean snapshotTime: diff --git a/controllers/modify_backup.go b/controllers/modify_backup.go index fcf7c829..2a8d041d 100644 --- a/controllers/modify_backup.go +++ b/controllers/modify_backup.go @@ -41,7 +41,9 @@ func (s modifyBackup) reconcile( return nil } - if backup.NeedsBackupReconfiguration() { + // The modify command is only required for continuous backups. + if backup.NeedsBackupReconfiguration() && + backup.GetBackupMode() == fdbv1beta2.BackupModeContinuous { adminClient, err := r.adminClientForBackup(ctx, backup) if err != nil { return &requeue{curError: err} diff --git a/controllers/update_backup_status.go b/controllers/update_backup_status.go index 2c239739..ed32dfb0 100644 --- a/controllers/update_backup_status.go +++ b/controllers/update_backup_status.go @@ -23,12 +23,13 @@ package controllers import ( "context" - "github.com/FoundationDB/fdb-kubernetes-operator/v2/internal" - "k8s.io/apimachinery/pkg/api/equality" - k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/utils/ptr" fdbv1beta2 "github.com/FoundationDB/fdb-kubernetes-operator/v2/api/v1beta2" + "github.com/FoundationDB/fdb-kubernetes-operator/v2/internal" appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -115,6 +116,7 @@ func (s updateBackupStatus) reconcile( Running: liveStatus.Status.Running, Paused: liveStatus.BackupAgentsPaused, SnapshotPeriodSeconds: liveStatus.SnapshotIntervalSeconds, + Restorable: ptr.Deref(liveStatus.Restorable, false), } originalStatus := backup.Status.DeepCopy() diff --git a/docs/backup_spec.md b/docs/backup_spec.md index 4ddd581f..06afe3c2 100644 --- a/docs/backup_spec.md +++ b/docs/backup_spec.md @@ -38,6 +38,12 @@ BackupGenerationStatus stores information on which generations have reached diff [Back to TOC](#table-of-contents) +## BackupMode + +BackupMode defines the mode of backup operation. + +[Back to TOC](#table-of-contents) + ## BackupState BackupState defines the desired state of a backup @@ -108,6 +114,7 @@ FoundationDBBackupSpec describes the desired state of the backup for a cluster. | imageType | ImageType defines the image type that should be used for the FoundationDBCluster deployment. When the type is set to \"unified\" the deployment will use the new fdb-kubernetes-monitor. Otherwise the main container and the sidecar container will use different images. Default: split | *ImageType | false | | backupType | BackupType defines the backup type that should be used for the backup. When the BackupType is set to BackupTypePartitionedLog, it's expected that the FoundationDBCluster creates and manages the additional backup worker processes. A migration to a different backup type is not yet supported in the operator. Default: \"backup_agent\". | *[BackupType](#backuptype) | false | | deletionPolicy | DeletionPolicy defines the deletion policy for this backup. The BackupDeletionPolicy defines the actions that should be taken when the FoundationDBBackup resource has a deletion timestamp. | *[BackupDeletionPolicy](#backupdeletionpolicy) | false | +| backupMode | BackupMode defines the backup mode that should be used for the backup. When the BackupMode is set to BackupModeOneTime, the backup will create a single snapshot and then stop. When set to BackupModeContinuous, the backup will run continuously, creating snapshots at regular intervals defined by SnapshotPeriodSeconds. Default: \"Continuous\". | *[BackupMode](#backupmode) | false | [Back to TOC](#table-of-contents) @@ -134,6 +141,7 @@ FoundationDBBackupStatusBackupDetails provides information about the state of th | running | | bool | false | | paused | | bool | false | | snapshotTime | | int | false | +| restorable | | bool | false | [Back to TOC](#table-of-contents) @@ -147,6 +155,8 @@ FoundationDBLiveBackupStatus describes the live status of the backup for a clust | SnapshotIntervalSeconds | SnapshotIntervalSeconds provides the interval of the snapshots. | int | false | | Status | Status provides the current state of the backup. | [FoundationDBLiveBackupStatusState](#foundationdblivebackupstatusstate) | false | | BackupAgentsPaused | BackupAgentsPaused describes whether the backup agents are paused. | bool | false | +| Restorable | Restorable if true, the backup can be restored | *bool | false | +| LatestRestorablePoint | LatestRestorablePoint contains information about the latest restorable point if any exists. | *[LatestRestorablePoint](#latestrestorablepoint) | false | [Back to TOC](#table-of-contents) @@ -157,8 +167,6 @@ FoundationDBLiveBackupStatusState provides the state of a backup in the backup s | Field | Description | Scheme | Required | | ----- | ----------- | ------ | -------- | | Running | Running determines whether the backup is currently running. | bool | false | -| Restorable | Restorable if true, the backup can be restored | *bool | false | -| LatestRestorablePoint | LatestRestorablePoint contains information about the latest restorable point if any exists. | *[LatestRestorablePoint](#latestrestorablepoint) | false | [Back to TOC](#table-of-contents) diff --git a/docs/manual/backup.md b/docs/manual/backup.md index 14475d0c..4ebb4757 100644 --- a/docs/manual/backup.md +++ b/docs/manual/backup.md @@ -1,14 +1,58 @@ # Managing Backups through the Operator FoundationDB has out-of-the-box support for backing up to an S3-compatible object store, and the operator supports this through a special resource type for backup and restore. -These backups run continuously, and simultaneously build new snapshots while backing up new mutations. -You can restore to any point in time after the end of the first snapshot. +The operator supports two backup modes: **continuous backups** that run indefinitely, taking snapshots at regular intervals, and **one-time backups** that create a single snapshot and then stop. +You can restore to any point in time after the end of the first snapshot for continuous backups, or to the specific point in time captured by a one-time backup. You can find more information about the backup feature in the [FoundationDB Backup documentation](https://apple.github.io/foundationdb/backups.html). **Warning**: Support for backups in the operator is still in development, and there are some missing features. -## Example Backup +## Backup Modes + +The operator supports two backup modes, controlled by the `backupMode` field in the backup spec: + +### Continuous Backups (Default) + +Continuous backups run indefinitely, creating snapshots at regular intervals defined by `snapshotPeriodSeconds`. +This is the default mode and provides continuous protection with point-in-time recovery capabilities. + +```yaml +apiVersion: apps.foundationdb.org/v1beta2 +kind: FoundationDBBackup +metadata: + name: sample-cluster-continuous +spec: + version: 7.1.26 + clusterName: sample-cluster + backupMode: continuous # Optional - this is the default + snapshotPeriodSeconds: 864000 # 10 days (default) + # ... other configuration +``` + +### One-Time Backups + +One-time backups create a single snapshot and then stop. +They are useful for creating point-in-time backups before major operations or for periodic archival purposes. + +```yaml +apiVersion: apps.foundationdb.org/v1beta2 +kind: FoundationDBBackup +metadata: + name: sample-cluster-snapshot +spec: + version: 7.1.26 + clusterName: sample-cluster + backupMode: oneTime + # Note: snapshotPeriodSeconds should NOT be set for one-time backups + # ... other configuration +``` + +**Important Notes for One-Time Backups:** +- Once completed, one-time backups will not restart automatically +- The backup status will show `restorable: true` when finished + +## Example Continuous Backup This is a sample configuration for running a continuous backup of a cluster. @@ -82,7 +126,9 @@ stringData: Creating this resource will tell the operator to do the following things: 1. Create a `sample-cluster-backup-agents` deployment running FoundationDB backup agent processes connecting to the cluster. -2. Run an `fdbbackup start` command to start a backup at `https://object-store.example:443/sample-cluster` using the bucket name `fdb-backups`. +2. Run an `fdbbackup start` command to start a continuous backup at `https://object-store.example:443/sample-cluster` using the bucket name `fdb-backups`. + +For one-time backups, the `fdbbackup start` command will be run without the continuous flags (`-s` and `-z`), creating a single snapshot. Do note, that if a port is not provided in the `blobStoreConfiguration.accountName`, it will default to `443`, or `80` if `secure_connection` is disabled. diff --git a/e2e/fixtures/fdb_backup.go b/e2e/fixtures/fdb_backup.go index 64d8c152..7a95215f 100644 --- a/e2e/fixtures/fdb_backup.go +++ b/e2e/fixtures/fdb_backup.go @@ -48,6 +48,8 @@ type FdbBackupConfiguration struct { BackupType *fdbv1beta2.BackupType // EncryptionEnabled determines whether backup encryption should be used. EncryptionEnabled bool + //BackupMode defines the backup mode that should be used. + BackupMode *fdbv1beta2.BackupMode } // CreateBackupForCluster will create a FoundationDBBackup for the provided cluster. @@ -87,6 +89,7 @@ func (factory *Factory) CreateBackupForCluster( // Enable if you want to get http debug logs. // "knob_http_verbose_level=10", }, + BackupMode: config.BackupMode, ImageType: fdbCluster.cluster.Spec.ImageType, MainContainer: fdbCluster.cluster.Spec.MainContainer, SidecarContainer: fdbCluster.cluster.Spec.SidecarContainer, @@ -252,7 +255,7 @@ func (fdbBackup *FdbBackup) WaitForRestorableVersion(version uint64) uint64 { ) g.Expect(err).NotTo(gomega.HaveOccurred()) - status := &fdbv1beta2.FoundationDBLiveBackupStatusState{} + status := &fdbv1beta2.FoundationDBLiveBackupStatus{} g.Expect(json.Unmarshal([]byte(out), status)).NotTo(gomega.HaveOccurred()) var latestRestorableVersion uint64 @@ -262,7 +265,7 @@ func (fdbBackup *FdbBackup) WaitForRestorableVersion(version uint64) uint64 { log.Println( "Backup status running:", - status.Running, + status.Status.Running, "restorable:", ptr.Deref(status.Restorable, false), "latestRestorablePoint:", diff --git a/e2e/fixtures/fdb_cluster.go b/e2e/fixtures/fdb_cluster.go index 39581251..7f550a9a 100644 --- a/e2e/fixtures/fdb_cluster.go +++ b/e2e/fixtures/fdb_cluster.go @@ -1191,8 +1191,34 @@ func (fdbCluster *FdbCluster) SetUseDNSInClusterFile(useDNSInClusterFile bool) e // Destroy will remove the underlying cluster. func (fdbCluster *FdbCluster) Destroy() error { - return fdbCluster.getClient(). + return fdbCluster.DestroyWithWaitForTearDown(false) +} + +// DestroyWithWaitForTearDown will remove the underlying cluster and wait for the resources to be removed. +func (fdbCluster *FdbCluster) DestroyWithWaitForTearDown(waitForTearDown bool) error { + err := fdbCluster.getClient(). Delete(context.Background(), fdbCluster.cluster) + if err != nil { + return err + } + + if !waitForTearDown { + return nil + } + + // Wait for all pods to be removed. + gomega.Eventually(func(g gomega.Gomega) { + podList := &corev1.PodList{} + + g.Expect(fdbCluster.getClient().List(context.Background(), podList, + client.InNamespace(fdbCluster.cluster.Namespace), + client.MatchingLabels(fdbCluster.cluster.GetMatchLabels()), + )).To(gomega.Succeed()) + + g.Expect(podList.Items).To(gomega.BeEmpty()) + }).WithTimeout(1 * time.Minute).WithPolling(1 * time.Second).ShouldNot(gomega.Succeed()) + + return nil } // SetIgnoreMissingProcessesSeconds sets the IgnoreMissingProcessesSeconds setting. diff --git a/e2e/test_operator_backups/operator_backup_test.go b/e2e/test_operator_backups/operator_backup_test.go index 6276000c..c4be6d9f 100644 --- a/e2e/test_operator_backups/operator_backup_test.go +++ b/e2e/test_operator_backups/operator_backup_test.go @@ -95,76 +95,141 @@ var _ = Describe("Operator Backup", Label("e2e", "pr"), func() { restore.Destroy() } + namespace := fdbCluster.Namespace() // Delete the FDB cluster to have a clean start. - Expect(fdbCluster.Destroy()).To(Succeed()) + Expect(fdbCluster.DestroyWithWaitForTearDown(true)).To(Succeed()) + // Restart the operator pods. + factory.RecreateOperatorPods(namespace) }) When("the default backup system is used", func() { - var restorableVersion uint64 + var useRestorableVersion bool + var backupConfiguration *fixtures.FdbBackupConfiguration - BeforeEach(func() { + JustBeforeEach(func() { log.Println("creating backup for cluster") - backup = factory.CreateBackupForCluster( - fdbCluster, - &fixtures.FdbBackupConfiguration{ - BackupType: ptr.To(fdbv1beta2.BackupTypeDefault), - }, - ) - keyValues = fdbCluster.GenerateRandomValues(10, prefix) - fdbCluster.WriteKeyValues(keyValues) - restorableVersion = backup.WaitForRestorableVersion( - fdbCluster.GetClusterVersion(), - ) - backup.Stop() + var restorableVersion uint64 + + if ptr.Deref( + backupConfiguration.BackupMode, + fdbv1beta2.BackupModeContinuous, + ) == fdbv1beta2.BackupModeContinuous { + // For the continuous backup we want tpo start the backup first and then write some data. + backup = factory.CreateBackupForCluster(fdbCluster, backupConfiguration) + keyValues = fdbCluster.GenerateRandomValues(10, prefix) + fdbCluster.WriteKeyValues(keyValues) + restorableVersion = backup.WaitForRestorableVersion( + fdbCluster.GetClusterVersion(), + ) + backup.Stop() + } else { + // In case of the one time backup we have to first write the keys and then do the backup. + keyValues = fdbCluster.GenerateRandomValues(10, prefix) + fdbCluster.WriteKeyValues(keyValues) + backup = factory.CreateBackupForCluster(fdbCluster, backupConfiguration) + restorableVersion = backup.WaitForRestorableVersion( + fdbCluster.GetClusterVersion(), + ) + } + + // Delete the data and restore it again. fdbCluster.ClearRange([]byte{prefix}, 60) + var currentRestorableVersion *uint64 + if useRestorableVersion { + currentRestorableVersion = ptr.To(restorableVersion) + } + restore = factory.CreateRestoreForCluster(backup, currentRestorableVersion) }) - When("no restorable version is specified", func() { + When("the continuous backup mode is used", func() { BeforeEach(func() { - restore = factory.CreateRestoreForCluster(backup, nil) + backupConfiguration = &fixtures.FdbBackupConfiguration{ + BackupType: ptr.To(fdbv1beta2.BackupTypeDefault), + BackupMode: ptr.To(fdbv1beta2.BackupModeContinuous), + } }) - It("should restore the cluster successfully with a restorable version", func() { - Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + When("no restorable version is specified", func() { + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) + }) + + When("encryption is enabled", func() { + BeforeEach(func() { + requiredFdbVersion, err := fdbv1beta2.ParseFdbVersion("7.4.5") + Expect(err).NotTo(HaveOccurred()) + + version := factory.GetFDBVersion() + if !version.IsAtLeast(requiredFdbVersion) { + Skip( + "version has a bug in the backup version that prevents tests to succeed", + ) + } + + backupConfiguration.EncryptionEnabled = true + }) + + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) + }) + + // TODO (johscheuer): Enable test once the CRD in CI is updated. + PWhen("using a restorable version", func() { + BeforeEach(func() { + useRestorableVersion = true + }) + + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) }) }) - // TODO (johscheuer): Enable test once the CRD in CI is updated. - PWhen("using a restorable version", func() { + When("the one time backup mode is used", func() { BeforeEach(func() { - factory.CreateRestoreForCluster(backup, ptr.To(restorableVersion)) + backupConfiguration = &fixtures.FdbBackupConfiguration{ + BackupType: ptr.To(fdbv1beta2.BackupTypeDefault), + BackupMode: ptr.To(fdbv1beta2.BackupModeOneTime), + } }) - It("should restore the cluster successfully with a restorable version", func() { - Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + When("no restorable version is specified", func() { + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) }) - }) - }) - When("the default backup system is used with encryption", func() { - BeforeEach(func() { - log.Println("creating backup for cluster") - backup = factory.CreateBackupForCluster( - fdbCluster, - &fixtures.FdbBackupConfiguration{ - BackupType: ptr.To(fdbv1beta2.BackupTypeDefault), - EncryptionEnabled: true, - }, - ) - keyValues = fdbCluster.GenerateRandomValues(10, prefix) - fdbCluster.WriteKeyValues(keyValues) - backup.WaitForRestorableVersion(fdbCluster.GetClusterVersion()) - backup.Stop() - fdbCluster.ClearRange([]byte{prefix}, 60) - }) + When("encryption is enabled", func() { + BeforeEach(func() { + requiredFdbVersion, err := fdbv1beta2.ParseFdbVersion("7.4.5") + Expect(err).NotTo(HaveOccurred()) - When("no restorable version is specified", func() { - BeforeEach(func() { - restore = factory.CreateRestoreForCluster(backup, nil) + version := factory.GetFDBVersion() + if !version.IsAtLeast(requiredFdbVersion) { + Skip( + "version has a bug in the backup version that prevents tests to succeed", + ) + } + + backupConfiguration.EncryptionEnabled = true + }) + + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) }) - It("should restore the cluster successfully with a restorable version", func() { - Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + // TODO (johscheuer): Enable test once the CRD in CI is updated. + PWhen("using a restorable version", func() { + BeforeEach(func() { + useRestorableVersion = true + }) + + It("should restore the cluster successfully with a restorable version", func() { + Expect(fdbCluster.GetRange([]byte{prefix}, 25, 60)).Should(Equal(keyValues)) + }) }) }) }) @@ -172,7 +237,7 @@ var _ = Describe("Operator Backup", Label("e2e", "pr"), func() { When("the partitioned backup system is used", func() { BeforeEach(func() { // Versions before 7.4 have a few issues and will not work properly with the experimental feature. - requiredFdbVersion, err := fdbv1beta2.ParseFdbVersion("7.4.0") + requiredFdbVersion, err := fdbv1beta2.ParseFdbVersion("7.4.5") Expect(err).NotTo(HaveOccurred()) version := factory.GetFDBVersion() diff --git a/fdbclient/admin_client.go b/fdbclient/admin_client.go index 6898a0e1..3fe4c31f 100644 --- a/fdbclient/admin_client.go +++ b/fdbclient/admin_client.go @@ -827,9 +827,11 @@ func (client *cliAdminClient) StartBackup(backup *fdbv1beta2.FoundationDBBackup) "start", "-d", backup.BackupURL(), - "-s", - strconv.Itoa(backup.SnapshotPeriodSeconds()), - "-z", + } + + // Add continuous backup flags only if backup mode is continuous. + if backup.GetBackupMode() == fdbv1beta2.BackupModeContinuous { + args = append(args, "-s", strconv.Itoa(backup.SnapshotPeriodSeconds()), "-z") } encryptionKeyPath, err := backup.GetEncryptionKey() @@ -954,6 +956,7 @@ func (client *cliAdminClient) GetBackupStatus() (*fdbv1beta2.FoundationDBLiveBac status := &fdbv1beta2.FoundationDBLiveBackupStatus{} err = json.Unmarshal(statusBytes, &status) + client.log.V(1).Info("backup status", "status", string(statusBytes), "error", err) if err != nil { return nil, err } diff --git a/fdbclient/admin_client_test.go b/fdbclient/admin_client_test.go index 793d670d..b9778610 100644 --- a/fdbclient/admin_client_test.go +++ b/fdbclient/admin_client_test.go @@ -1105,6 +1105,37 @@ protocol fdb00b071010000`, "-s", "60", "-z", }), + Entry("with continuous backup mode (default)", + &fdbv1beta2.FoundationDBBackup{ + Spec: fdbv1beta2.FoundationDBBackupSpec{ + Version: fdbv1beta2.Versions.SupportsBackupEncryption.String(), + BlobStoreConfiguration: &fdbv1beta2.BlobStoreConfiguration{ + AccountName: "test", + BackupName: "test-backup", + }, + SnapshotPeriodSeconds: ptr.To(60), + BackupMode: ptr.To(fdbv1beta2.BackupModeContinuous), + }, + }, []string{ + "start", + "-d", "blobstore://test:443/test-backup?bucket=fdb-backups", + "-s", "60", + "-z", + }), + Entry("with one-time backup mode", + &fdbv1beta2.FoundationDBBackup{ + Spec: fdbv1beta2.FoundationDBBackupSpec{ + Version: fdbv1beta2.Versions.SupportsBackupEncryption.String(), + BlobStoreConfiguration: &fdbv1beta2.BlobStoreConfiguration{ + AccountName: "test", + BackupName: "test-backup", + }, + BackupMode: ptr.To(fdbv1beta2.BackupModeOneTime), + }, + }, []string{ + "start", + "-d", "blobstore://test:443/test-backup?bucket=fdb-backups", + }), ) DescribeTable( diff --git a/pkg/fdbadminclient/mock/admin_client_mock.go b/pkg/fdbadminclient/mock/admin_client_mock.go index c0641772..60eef73a 100644 --- a/pkg/fdbadminclient/mock/admin_client_mock.go +++ b/pkg/fdbadminclient/mock/admin_client_mock.go @@ -876,11 +876,17 @@ func (client *AdminClient) StartBackup(backup *fdbv1beta2.FoundationDBBackup) er return client.mockError } - client.Backups["default"] = fdbv1beta2.FoundationDBBackupStatusBackupDetails{ - URL: backup.BackupURL(), - Running: true, - SnapshotPeriodSeconds: backup.SnapshotPeriodSeconds(), + backupDetails := fdbv1beta2.FoundationDBBackupStatusBackupDetails{ + URL: backup.BackupURL(), + Running: true, } + + // Only set snapshot period for continuous backups + if backup.GetBackupMode() == fdbv1beta2.BackupModeContinuous { + backupDetails.SnapshotPeriodSeconds = backup.SnapshotPeriodSeconds() + } + + client.Backups["default"] = backupDetails return nil } From 960edc2ec51d883c35ea09e5244895cbffd6192e Mon Sep 17 00:00:00 2001 From: Johannes Scheuermann Date: Tue, 30 Sep 2025 16:57:28 +0200 Subject: [PATCH 2/2] Apply suggestion from @nicmorales9 Co-authored-by: nicmorales9 <153320946+nicmorales9@users.noreply.github.com> --- e2e/test_operator_backups/operator_backup_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/test_operator_backups/operator_backup_test.go b/e2e/test_operator_backups/operator_backup_test.go index c4be6d9f..a88ba90d 100644 --- a/e2e/test_operator_backups/operator_backup_test.go +++ b/e2e/test_operator_backups/operator_backup_test.go @@ -114,7 +114,7 @@ var _ = Describe("Operator Backup", Label("e2e", "pr"), func() { backupConfiguration.BackupMode, fdbv1beta2.BackupModeContinuous, ) == fdbv1beta2.BackupModeContinuous { - // For the continuous backup we want tpo start the backup first and then write some data. + // For the continuous backup we want to start the backup first and then write some data. backup = factory.CreateBackupForCluster(fdbCluster, backupConfiguration) keyValues = fdbCluster.GenerateRandomValues(10, prefix) fdbCluster.WriteKeyValues(keyValues)