From 97fecb4421df2ad659c443bb4bf5ca8cb0f56481 Mon Sep 17 00:00:00 2001 From: Jack Lin Date: Tue, 19 Mar 2024 17:31:46 +0800 Subject: [PATCH] feat(backup): add option validation for backup creation ref: longhorn/longhorn 7070 Signed-off-by: Jack Lin --- api/model.go | 2 + app/recurring_job.go | 28 ++++++++++ client/generated_backup_volume.go | 2 + controller/backup_volume_controller.go | 3 ++ engineapi/backups.go | 1 + engineapi/types.go | 1 + go.mod | 1 + go.sum | 8 +-- k8s/crds.yaml | 3 ++ k8s/pkg/apis/longhorn/v1beta2/backupvolume.go | 3 ++ .../longhorn/backupstore/backupstore.go | 7 +++ .../longhorn/backupstore/deltablock.go | 17 +++++- .../longhorn/backupstore/inspect.go | 1 + .../github.com/longhorn/backupstore/list.go | 1 + .../longhorn/backupstore/types/types.go | 16 ++++++ vendor/modules.txt | 2 +- webhook/resources/backup/validator.go | 53 +++++++++++++++++++ 17 files changed, 140 insertions(+), 9 deletions(-) diff --git a/api/model.go b/api/model.go index 05fc9f510c..96f2842049 100644 --- a/api/model.go +++ b/api/model.go @@ -140,6 +140,7 @@ type BackupVolume struct { BackingImageName string `json:"backingImageName"` BackingImageChecksum string `json:"backingImageChecksum"` StorageClassName string `json:"storageClassName"` + BackupCount string `json:"backupCount"` } type Backup struct { @@ -1742,6 +1743,7 @@ func toBackupVolumeResource(bv *longhorn.BackupVolume, apiContext *api.ApiContex BackingImageName: bv.Status.BackingImageName, BackingImageChecksum: bv.Status.BackingImageChecksum, StorageClassName: bv.Status.StorageClassName, + BackupCount: bv.Status.BackupCount, } b.Actions = map[string]string{ "backupList": apiContext.UrlBuilder.ActionLink(b.Resource, "backupList"), diff --git a/app/recurring_job.go b/app/recurring_job.go index 6070a5598b..4366f5b693 100644 --- a/app/recurring_job.go +++ b/app/recurring_job.go @@ -15,6 +15,7 @@ import ( "github.com/urfave/cli" "golang.org/x/sync/errgroup" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" @@ -28,6 +29,7 @@ import ( "github.com/longhorn/longhorn-manager/types" "github.com/longhorn/longhorn-manager/util" + btypes "github.com/longhorn/backupstore/types" longhornclient "github.com/longhorn/longhorn-manager/client" longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" lhclientset "github.com/longhorn/longhorn-manager/k8s/pkg/client/clientset/versioned" @@ -611,6 +613,32 @@ func (job *Job) doRecurringBackup() (err error) { return err } + if intervalStr, exists := job.labels[types.GetLonghornLabelKey(btypes.LonghornBackupOptionFullBackupInterval)]; exists { + interval, err := strconv.Atoi(intervalStr) + if err != nil { + return errors.Wrapf(err, "interval %v is not number", intervalStr) + } + + backupVolume, err := job.api.BackupVolume.ById(job.volumeName) + if err != nil { + if !apierrors.IsNotFound(err) { + return errors.Wrapf(err, "failed to get backup volume %v", job.volumeName) + } + } + + backupCount := 0 + if backupVolume != nil && backupVolume.BackupCount != "" { + backupCount, err = strconv.Atoi(backupVolume.BackupCount) + if err != nil { + return errors.Wrapf(err, "backup count %v is not number", backupVolume.BackupCount) + } + } + + if backupCount%interval == 0 { + job.labels[types.GetLonghornLabelKey(btypes.LonghornBackupOptionBackupMode)] = btypes.LonghornBackupModeFull + } + } + if _, err := job.api.Volume.ActionSnapshotBackup(volume, &longhornclient.SnapshotInput{ Labels: job.labels, Name: job.snapshotName, diff --git a/client/generated_backup_volume.go b/client/generated_backup_volume.go index b1936e1988..5e89950989 100644 --- a/client/generated_backup_volume.go +++ b/client/generated_backup_volume.go @@ -28,6 +28,8 @@ type BackupVolume struct { Size string `json:"size,omitempty" yaml:"size,omitempty"` StorageClassName string `json:"storageClassName,omitempty" yaml:"storage_class_name,omitempty"` + + BackupCount string `json:"backupCount,omitempty" yaml:"backup_count,omitempty"` } type BackupVolumeCollection struct { diff --git a/controller/backup_volume_controller.go b/controller/backup_volume_controller.go index f018d7d477..5d7cc518a9 100644 --- a/controller/backup_volume_controller.go +++ b/controller/backup_volume_controller.go @@ -383,6 +383,8 @@ func (bvc *BackupVolumeController) reconcile(backupVolumeName string) (err error return nil } + logrus.Infof("[DEBUG]: backupVolumeInfo: %v", backupVolumeInfo) + // Update the Backup CR spec.syncRequestAt to request the // backup_controller to reconcile the Backup CR if the last backup changed if backupVolume.Status.LastBackupName != backupVolumeInfo.LastBackupName { @@ -408,6 +410,7 @@ func (bvc *BackupVolumeController) reconcile(backupVolumeName string) (err error backupVolume.Status.BackingImageChecksum = backupVolumeInfo.BackingImageChecksum backupVolume.Status.StorageClassName = backupVolumeInfo.StorageClassName backupVolume.Status.LastSyncedAt = syncTime + backupVolume.Status.BackupCount = backupVolumeInfo.BackupCount return nil } diff --git a/engineapi/backups.go b/engineapi/backups.go index 9bece398af..66b684225d 100644 --- a/engineapi/backups.go +++ b/engineapi/backups.go @@ -232,6 +232,7 @@ func (btc *BackupTargetClient) BackupVolumeDelete(destURL, volumeName string, cr // parseBackupVolumeConfig parses a backup volume config func parseBackupVolumeConfig(output string) (*BackupVolume, error) { backupVolume := new(BackupVolume) + logrus.Infof("[DEBUG] parseBackupVolumeConfig output: %v", output) if err := json.Unmarshal([]byte(output), backupVolume); err != nil { return nil, errors.Wrapf(err, "error parsing one backup volume config: \n%s", output) } diff --git a/engineapi/types.go b/engineapi/types.go index 63d6be8a4f..e095469441 100644 --- a/engineapi/types.go +++ b/engineapi/types.go @@ -162,6 +162,7 @@ type BackupVolume struct { BackingImageName string `json:"backingImageName"` BackingImageChecksum string `json:"backingImageChecksum"` StorageClassName string `json:"storageClassName"` + BackupCount string `json:"backupCount"` } type Backup struct { diff --git a/go.mod b/go.mod index b03bdf895f..11f5499eae 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/longhorn/longhorn-manager go 1.21 replace ( + github.com/longhorn/backupstore v0.0.0-20240219094812-3a87ee02df77 => github.com/ChanYiLin/backupstore v0.0.0-20240322024320-ab6fa5ee775f k8s.io/api => k8s.io/api v0.28.5 k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.28.5 k8s.io/apimachinery => k8s.io/apimachinery v0.28.5 diff --git a/go.sum b/go.sum index e0e7ece68e..2bcaea8f78 100644 --- a/go.sum +++ b/go.sum @@ -605,6 +605,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChanYiLin/backupstore v0.0.0-20240322024320-ab6fa5ee775f h1:H3+X1DZVKbxMIqeFQsnVGznAjOHJSEAvFSW8ZdNjfqM= +github.com/ChanYiLin/backupstore v0.0.0-20240322024320-ab6fa5ee775f/go.mod h1:4cbJWtlrD2cGTQxQLtdlPTYopiJiusXH7CpOBrn/s3k= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -1045,14 +1047,10 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/longhorn/backing-image-manager v1.6.0 h1:Jmlc8+W63l0VZoVhPwNLniAk+eBC4CNaadoqpqA51KE= github.com/longhorn/backing-image-manager v1.6.0/go.mod h1:IH0mgbK+Dr13xkY+LhDaufyd9YIpiKqYo1AeRLFYGrk= -github.com/longhorn/backupstore v0.0.0-20240219094812-3a87ee02df77 h1:iJRq59kA22f9HIjFtY/lz5rKCorZJrrYXju70XoWdmE= -github.com/longhorn/backupstore v0.0.0-20240219094812-3a87ee02df77/go.mod h1:4cbJWtlrD2cGTQxQLtdlPTYopiJiusXH7CpOBrn/s3k= github.com/longhorn/go-common-libs v0.0.0-20240307063052-6e77996eda29 h1:tyzIDCMjQGQzhqAtdJaeEMAaNUZJD/sHERXp+tYc+ms= github.com/longhorn/go-common-libs v0.0.0-20240307063052-6e77996eda29/go.mod h1:ePLGb2r/PJBUIVoVhLOt4bLOeu0S72ZB+fWDWwC8H28= github.com/longhorn/go-iscsi-helper v0.0.0-20240308033847-bc3aab599425 h1:koSD52H0VkzJAh3OIZCdgQ9mqoRXklkeuhqmuwQ1WzU= github.com/longhorn/go-iscsi-helper v0.0.0-20240308033847-bc3aab599425/go.mod h1:2aM6KBix3Khd56I4rihOBOOPOm0/YvYMjtr1KNclQsI= -github.com/longhorn/go-spdk-helper v0.0.0-20240301101140-6eb6aa5fc09d h1:vajqcFlGHmyQcqhBbGMRo33GNcrKMgNY8ca87rGuKnU= -github.com/longhorn/go-spdk-helper v0.0.0-20240301101140-6eb6aa5fc09d/go.mod h1:Zv0UpwuqZpijy5/vSW2PvR8zVPBX8CJPF3XeZAuc4Ek= github.com/longhorn/go-spdk-helper v0.0.0-20240308030201-9b252d6f7250 h1:bc9BtfvSuXvmztYiBCebvPWPxakbiCWBvydbeqat+Ek= github.com/longhorn/go-spdk-helper v0.0.0-20240308030201-9b252d6f7250/go.mod h1:re2QHb6FUU9G/CL3AnXDbzFjLNPwmweBvnTfeVpkZ1E= github.com/longhorn/longhorn-engine v1.6.0 h1:6CH2vvwCgFBIGW4TegcI79CL1Ego1nvLZIC3ioRjjdM= @@ -2231,8 +2229,6 @@ k8s.io/mount-utils v0.28.5/go.mod h1:ceMAZ+Nzlk8zOwN205YXXGJRGmf1o0/XIwsKnG44p0I k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= -k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= diff --git a/k8s/crds.yaml b/k8s/crds.yaml index 64851fe1bb..a78c2001f8 100644 --- a/k8s/crds.yaml +++ b/k8s/crds.yaml @@ -1111,6 +1111,9 @@ spec: backingImageName: description: The backing image name. type: string + backupCount: + description: The number of backups that have been created. + type: string createdAt: description: The backup volume creation time. type: string diff --git a/k8s/pkg/apis/longhorn/v1beta2/backupvolume.go b/k8s/pkg/apis/longhorn/v1beta2/backupvolume.go index 47aeeb3a26..99f7492c94 100644 --- a/k8s/pkg/apis/longhorn/v1beta2/backupvolume.go +++ b/k8s/pkg/apis/longhorn/v1beta2/backupvolume.go @@ -55,6 +55,9 @@ type BackupVolumeStatus struct { // +optional // +nullable LastSyncedAt metav1.Time `json:"lastSyncedAt"` + // The number of backups that have been created. + // +optional + BackupCount string `json:"backupCount"` } // +genclient diff --git a/vendor/github.com/longhorn/backupstore/backupstore.go b/vendor/github.com/longhorn/backupstore/backupstore.go index 9e79e8c8e8..fff2d69cb6 100644 --- a/vendor/github.com/longhorn/backupstore/backupstore.go +++ b/vendor/github.com/longhorn/backupstore/backupstore.go @@ -23,6 +23,10 @@ type Volume struct { CompressionMethod string `json:",string"` StorageClassName string `json:",string"` DataEngine string `json:",string"` + + // It is not the count of the current backup on the backupstore, + // but how many backups of the volume have been created before. + BackupCount int64 `json:",string"` } type Snapshot struct { @@ -74,6 +78,9 @@ func addVolume(driver BackupStoreDriver, volume *Volume) error { return fmt.Errorf("invalid volume name %v", volume.Name) } + // first time create backup volume means first time create backup on this backupstore + volume.BackupCount = 0 + if err := saveVolume(driver, volume); err != nil { log.WithError(err).Errorf("Failed to add volume %v", volume.Name) return err diff --git a/vendor/github.com/longhorn/backupstore/deltablock.go b/vendor/github.com/longhorn/backupstore/deltablock.go index 73851e53b5..00a22cdc13 100644 --- a/vendor/github.com/longhorn/backupstore/deltablock.go +++ b/vendor/github.com/longhorn/backupstore/deltablock.go @@ -175,7 +175,7 @@ func CreateDeltaBlockBackup(backupName string, config *DeltaBackupConfig) (isInc } backupRequest := &backupRequest{} - if volume.LastBackupName != "" { + if volume.LastBackupName != "" && !isFullBackup(config) { lastBackupName := volume.LastBackupName var backup, err = loadBackup(bsDriver, lastBackupName, volume.Name) if err != nil { @@ -208,6 +208,9 @@ func CreateDeltaBlockBackup(backupName string, config *DeltaBackupConfig) (isInc } } + logrus.Infof("[DEBUG] isFullBackup(config): %v", isFullBackup(config)) + logrus.Infof("[DEBUG] backupRequest.lastBackup: %v", backupRequest.lastBackup) + log.WithFields(logrus.Fields{ LogFieldReason: LogReasonStart, LogFieldObject: LogObjectSnapshot, @@ -368,7 +371,7 @@ func backupBlock(bsDriver BackupStoreDriver, config *DeltaBackupConfig, }() blkFile := getBlockFilePath(volume.Name, checksum) - if bsDriver.FileExists(blkFile) { + if bsDriver.FileExists(blkFile) && !isFullBackup(config) { log.Debugf("Found existing block matching at %v", blkFile) return nil } @@ -554,6 +557,7 @@ func performBackup(bsDriver BackupStoreDriver, config *DeltaBackupConfig, delta volume.CompressionMethod = config.Volume.CompressionMethod volume.StorageClassName = config.Volume.StorageClassName volume.DataEngine = config.Volume.DataEngine + volume.BackupCount = volume.BackupCount + 1 if err := saveVolume(bsDriver, volume); err != nil { return progress.progress, "", err @@ -1303,3 +1307,12 @@ func getBlockNamesForVolume(driver BackupStoreDriver, volumeName string) ([]stri return util.ExtractNames(names, "", BLK_SUFFIX), nil } + +func isFullBackup(config *DeltaBackupConfig) bool { + if config.Labels != nil { + if backupMode, exist := config.Labels[types.GetLonghornLabelKey(types.LonghornBackupOptionBackupMode)]; exist { + return backupMode == types.LonghornBackupModeFull + } + } + return false +} diff --git a/vendor/github.com/longhorn/backupstore/inspect.go b/vendor/github.com/longhorn/backupstore/inspect.go index 590709aa54..2b221cfdd1 100644 --- a/vendor/github.com/longhorn/backupstore/inspect.go +++ b/vendor/github.com/longhorn/backupstore/inspect.go @@ -77,6 +77,7 @@ func fillVolumeInfo(volume *Volume) *VolumeInfo { BackingImageChecksum: volume.BackingImageChecksum, StorageClassname: volume.StorageClassName, DataEngine: volume.DataEngine, + BackupCount: volume.BackupCount, } } diff --git a/vendor/github.com/longhorn/backupstore/list.go b/vendor/github.com/longhorn/backupstore/list.go index c61967fc18..1fe9e55d38 100644 --- a/vendor/github.com/longhorn/backupstore/list.go +++ b/vendor/github.com/longhorn/backupstore/list.go @@ -29,6 +29,7 @@ type VolumeInfo struct { BackingImageChecksum string StorageClassname string DataEngine string + BackupCount int64 `json:",string"` } type BackupInfo struct { diff --git a/vendor/github.com/longhorn/backupstore/types/types.go b/vendor/github.com/longhorn/backupstore/types/types.go index bc626cc5f4..b71880a020 100644 --- a/vendor/github.com/longhorn/backupstore/types/types.go +++ b/vendor/github.com/longhorn/backupstore/types/types.go @@ -1,5 +1,7 @@ package types +import "fmt" + type ProgressState string const ( @@ -29,6 +31,16 @@ const ( NOProxy = "NO_PROXY" VirtualHostedStyle = "VIRTUAL_HOSTED_STYLE" + + LonghornLabelKeyPrefix = "longhorn.io" +) + +const ( + LonghornBackupOptionBackupMode = "backup-mode" + LonghornBackupModeFull = "full" + LonghornBackupModeIncremental = "incremental" + + LonghornBackupOptionFullBackupInterval = "full-backup-interval" ) type Mapping struct { @@ -65,3 +77,7 @@ const ( const ( ErrorMsgRestoreCancelled = "backup restoration is cancelled" ) + +func GetLonghornLabelKey(name string) string { + return fmt.Sprintf("%s/%s", LonghornLabelKeyPrefix, name) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4a33c35218..e8fc1b6492 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -232,7 +232,7 @@ github.com/longhorn/backing-image-manager/pkg/meta github.com/longhorn/backing-image-manager/pkg/rpc github.com/longhorn/backing-image-manager/pkg/types github.com/longhorn/backing-image-manager/pkg/util -# github.com/longhorn/backupstore v0.0.0-20240219094812-3a87ee02df77 +# github.com/longhorn/backupstore v0.0.0-20240219094812-3a87ee02df77 => github.com/ChanYiLin/backupstore v0.0.0-20240322024320-ab6fa5ee775f ## explicit; go 1.21 github.com/longhorn/backupstore github.com/longhorn/backupstore/backupbackingimage diff --git a/webhook/resources/backup/validator.go b/webhook/resources/backup/validator.go index 03697dee38..ad68fa37d0 100644 --- a/webhook/resources/backup/validator.go +++ b/webhook/resources/backup/validator.go @@ -1,10 +1,19 @@ package backup import ( + "fmt" + "strconv" + "strings" + admissionregv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/runtime" + btypes "github.com/longhorn/backupstore/types" "github.com/longhorn/longhorn-manager/datastore" + "github.com/longhorn/longhorn-manager/types" "github.com/longhorn/longhorn-manager/webhook/admission" + werror "github.com/longhorn/longhorn-manager/webhook/error" + "github.com/pkg/errors" longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" ) @@ -28,3 +37,47 @@ func (b *backupValidator) Resource() admission.Resource { OperationTypes: []admissionregv1.OperationType{}, } } + +func (b *backupValidator) Create(request *admission.Request, newObj runtime.Object) error { + backup, ok := newObj.(*longhorn.Backup) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.Backup", newObj), "") + } + + if backup.Spec.Labels != nil { + for key, value := range backup.Spec.Labels { + if !strings.HasPrefix(key, types.LonghornLabelKeyPrefix) { + continue + } + if err := validateBackupOption(key, value); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + } + } + + return nil + +} + +func validateBackupOption(label, value string) error { + option := label[strings.LastIndex(label, "/")+1:] + + switch option { + case btypes.LonghornBackupOptionBackupMode: + if value != btypes.LonghornBackupModeFull && + value != btypes.LonghornBackupModeIncremental { + return fmt.Errorf("%v:%v is not a valid option", label, value) + } + case btypes.LonghornBackupOptionFullBackupInterval: + _, err := strconv.Atoi(value) + if err != nil { + return errors.Wrapf(err, "%v:%v is not number", label, value) + } + case types.LonghornLabelVolumeAccessMode: + return nil + default: + return fmt.Errorf("%v:%v is not a valid option", label, value) + } + + return nil +}