diff --git a/api/v1alpha1/mysqlcluster_types.go b/api/v1alpha1/mysqlcluster_types.go index f4414d66..ed4135d7 100644 --- a/api/v1alpha1/mysqlcluster_types.go +++ b/api/v1alpha1/mysqlcluster_types.go @@ -92,6 +92,11 @@ type MysqlClusterSpec struct { // +optional BackupSchedule string `json:"backupSchedule,omitempty"` + // Specify that crontab job backup both on NFS and S3 storage. + // +optional + // +kubebuilder:default:=false + BothS3NFS bool `json:"bothS3NFS,omitempty"` + // If set keeps last BackupScheduleJobsHistoryLimit Backups // +optional // +kubebuilder:default:=6 diff --git a/api/v1alpha1/mysqlcluster_webhook.go b/api/v1alpha1/mysqlcluster_webhook.go index 61234792..961eb2a4 100644 --- a/api/v1alpha1/mysqlcluster_webhook.go +++ b/api/v1alpha1/mysqlcluster_webhook.go @@ -54,6 +54,11 @@ func (r *MysqlCluster) ValidateCreate() error { if err := r.validateNFSServerAddress(r); err != nil { return err } + + if err := r.validBothS3NFS(); err != nil { + return err + } + if err := r.validateMysqlVersionAndImage(); err != nil { return err } @@ -74,6 +79,11 @@ func (r *MysqlCluster) ValidateUpdate(old runtime.Object) error { if err := r.validateLowTableCase(oldCluster); err != nil { return err } + + if err := r.validBothS3NFS(); err != nil { + return err + } + if err := r.validateMysqlVersionAndImage(); err != nil { return err } @@ -145,3 +155,13 @@ func (r *MysqlCluster) validateMysqlVersionAndImage() error { } return nil } + +// Validate BothS3NFS +func (r *MysqlCluster) validBothS3NFS() error { + if r.Spec.BothS3NFS && + (len(r.Spec.NFSServerAddress) == 0 || len(r.Spec.BackupSchedule) == 0 || + len(r.Spec.BackupSecretName) == 0) { + return apierrors.NewForbidden(schema.GroupResource{}, "", fmt.Errorf("if BothS3NFS is set, backupSchedule/backupSecret/nfsAddress should not empty")) + } + return nil +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bbc85fe9..2bbde94f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,3 @@ -//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/backup/cronbackup.go b/backup/cronbackup.go index ca00b238..9ef05521 100644 --- a/backup/cronbackup.go +++ b/backup/cronbackup.go @@ -24,6 +24,7 @@ type CronJob struct { BackupScheduleJobsHistoryLimit *int Image string NFSServerAddress string + BothS3NFS bool Log logr.Logger } @@ -45,9 +46,9 @@ func (j *CronJob) Run() { log.Info("at least a backup is running", "running_backups_count", j.scheduledBackupsRunningCount()) return } - + //TODO: if BothS3NFS, it need create backup without nfs address. // create the backup - if _, err := j.createBackup(); err != nil { + if err := j.createBackups(); err != nil { log.Error(err, "failed to create backup") } } @@ -117,8 +118,32 @@ func (j *CronJob) backupGC() { } } -func (j *CronJob) createBackup() (*apiv1alpha1.Backup, error) { - backupName := fmt.Sprintf("%s-auto-%s", j.ClusterName, time.Now().Format("2006-01-02t15-04-05")) +func (j *CronJob) createBackups() error { + // Cannot use uppcase, because RFC 1123 subdomain must consist of lower case alphanumeric characters + const nfstype string = "nfs" + const s3type string = "s3" + if j.BothS3NFS { // create both. + if err := j.createOneBackup(nfstype, j.NFSServerAddress); err != nil { + return err + } + if err := j.createOneBackup(s3type, ""); err != nil { + return err + } + } else { + if len(j.NFSServerAddress) > 0 { + if err := j.createOneBackup(nfstype, j.NFSServerAddress); err != nil { + return err + } + } else { + if err := j.createOneBackup(s3type, ""); err != nil { + return err + } + } + } + return nil +} +func (j *CronJob) createOneBackup(BackupType, NFSAddress string) error { + backupName := fmt.Sprintf("%s-%s-%s", j.ClusterName, BackupType, time.Now().Format("2006-01-02t15-04-05")) backup := &apiv1alpha1.Backup{ ObjectMeta: metav1.ObjectMeta{ @@ -131,13 +156,12 @@ func (j *CronJob) createBackup() (*apiv1alpha1.Backup, error) { //TODO modify to cluster sidecar image Image: j.Image, //RemoteDeletePolicy: j.BackupRemoteDeletePolicy, - HostName: fmt.Sprintf("%s-mysql-0", j.ClusterName), + HostName: fmt.Sprintf("%s-mysql-0", j.ClusterName), + NFSServerAddress: NFSAddress, }, } - if len(j.NFSServerAddress) > 0 { - backup.Spec.NFSServerAddress = j.NFSServerAddress - } - return backup, j.Client.Create(context.TODO(), backup) + + return j.Client.Create(context.TODO(), backup) } type byTimestamp []apiv1alpha1.Backup diff --git a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml index 5d56ba83..8c3d3c1c 100644 --- a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml +++ b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml @@ -70,6 +70,10 @@ spec: description: Represents the name of the secret that contains credentials to connect to the storage provider to store backups. type: string + bothS3NFS: + default: false + description: Specify that crontab job backup both on NFS and S3 storage. + type: boolean metricsOpts: default: enabled: false diff --git a/controllers/backupcron_controller.go b/controllers/backupcron_controller.go index ae315e7d..11d10532 100644 --- a/controllers/backupcron_controller.go +++ b/controllers/backupcron_controller.go @@ -144,7 +144,8 @@ func (r *BackupCronReconciler) updateClusterSchedule(ctx context.Context, cluste Image: cluster.Spec.PodPolicy.SidecarImage, BackupScheduleJobsHistoryLimit: cluster.Spec.BackupScheduleJobsHistoryLimit, //BackupRemoteDeletePolicy: cluster.Spec.BackupRemoteDeletePolicy, - + // add both S3 and NFS flag. + BothS3NFS: cluster.Spec.BothS3NFS, NFSServerAddress: cluster.Spec.NFSServerAddress, Log: log, }, cluster.Name)