From 000b5ff76ca0d21cdeaa643ceab1adf11a05e8ff Mon Sep 17 00:00:00 2001 From: Timofei Larkin Date: Tue, 3 Feb 2026 21:39:15 +0300 Subject: [PATCH] [backups] Signed-off-by: Timofei Larkin --- api/backups/strategy/v1alpha1/velero_types.go | 5 +- .../v1alpha1/zz_generated.deepcopy.go | 6 + api/backups/v1alpha1/backupjob_types.go | 1 + api/backups/v1alpha1/backupjob_webhook.go | 67 --- .../v1alpha1/backupjob_webhook_test.go | 334 -------------- cmd/backup-controller/main.go | 15 - hack/update-codegen.sh | 2 +- .../backupcontroller/backupjob_controller.go | 5 +- .../backupcontroller/restorejob_controller.go | 3 +- .../velerostrategy_controller.go | 59 +-- .../backups.cozystack.io_backupjobs.yaml | 5 + .../backups.cozystack.io_restorejobs.yaml | 8 +- .../backup-controller/templates/class.yaml | 14 - .../backup-controller/templates/strategy.yaml | 10 - ...trategy.backups.cozystack.io_veleroes.yaml | 422 ++++++++++++++++++ .../cozyrds/backup-strategy.yaml | 6 +- pkg/generated/openapi/zz_generated.openapi.go | 7 + 17 files changed, 481 insertions(+), 488 deletions(-) delete mode 100644 api/backups/v1alpha1/backupjob_webhook.go delete mode 100644 api/backups/v1alpha1/backupjob_webhook_test.go delete mode 100644 packages/system/backup-controller/templates/class.yaml delete mode 100644 packages/system/backup-controller/templates/strategy.yaml diff --git a/api/backups/strategy/v1alpha1/velero_types.go b/api/backups/strategy/v1alpha1/velero_types.go index ae15fa5e..2bfeabb7 100644 --- a/api/backups/strategy/v1alpha1/velero_types.go +++ b/api/backups/strategy/v1alpha1/velero_types.go @@ -55,8 +55,9 @@ type VeleroSpec struct { // VeleroTemplate describes the data a backup.velero.io should have when // templated from a Velero backup strategy. type VeleroTemplate struct { - Spec velerov1.BackupSpec `json:"spec"` - RestoreSpec velerov1.RestoreSpec `json:"restoreSpec"` + Spec velerov1.BackupSpec `json:"spec"` + // +optional + RestoreSpec *velerov1.RestoreSpec `json:"restoreSpec,omitempty"` } type VeleroStatus struct { diff --git a/api/backups/strategy/v1alpha1/zz_generated.deepcopy.go b/api/backups/strategy/v1alpha1/zz_generated.deepcopy.go index c1989ac9..7f109878 100644 --- a/api/backups/strategy/v1alpha1/zz_generated.deepcopy.go +++ b/api/backups/strategy/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -223,6 +224,11 @@ func (in *VeleroStatus) DeepCopy() *VeleroStatus { func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) { *out = *in in.Spec.DeepCopyInto(&out.Spec) + if in.RestoreSpec != nil { + in, out := &in.RestoreSpec, &out.RestoreSpec + *out = new(velerov1.RestoreSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate. diff --git a/api/backups/v1alpha1/backupjob_types.go b/api/backups/v1alpha1/backupjob_types.go index 7b251ceb..111b5f1d 100644 --- a/api/backups/v1alpha1/backupjob_types.go +++ b/api/backups/v1alpha1/backupjob_types.go @@ -57,6 +57,7 @@ type BackupJobSpec struct { // The BackupClass will be resolved to determine the appropriate strategy and storage // based on the ApplicationRef. // This field is immutable once the BackupJob is created. + // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="backupClassName is immutable" BackupClassName string `json:"backupClassName"` } diff --git a/api/backups/v1alpha1/backupjob_webhook.go b/api/backups/v1alpha1/backupjob_webhook.go deleted file mode 100644 index 064605c2..00000000 --- a/api/backups/v1alpha1/backupjob_webhook.go +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package v1alpha1 - -import ( - "context" - "fmt" - "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -// SetupWebhookWithManager registers the BackupJob webhook with the manager. -func SetupBackupJobWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(&BackupJob{}). - Complete() -} - -// +kubebuilder:webhook:path=/mutate-backups-cozystack-io-v1alpha1-backupjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=backups.cozystack.io,resources=backupjobs,verbs=create;update,versions=v1alpha1,name=mbackupjob.kb.io,admissionReviewVersions=v1 - -// Default implements webhook.Defaulter so a webhook will be registered for the type -func (j *BackupJob) Default() { - j.Spec.ApplicationRef = NormalizeApplicationRef(j.Spec.ApplicationRef) -} - -// +kubebuilder:webhook:path=/validate-backups-cozystack-io-v1alpha1-backupjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=backups.cozystack.io,resources=backupjobs,verbs=create;update,versions=v1alpha1,name=vbackupjob.kb.io,admissionReviewVersions=v1 - -// ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (j *BackupJob) ValidateCreate() (admission.Warnings, error) { - logger := log.FromContext(context.Background()) - logger.Info("validating BackupJob creation", "name", j.Name, "namespace", j.Namespace) - - // Validate that backupClassName is set - if strings.TrimSpace(j.Spec.BackupClassName) == "" { - return nil, fmt.Errorf("backupClassName is required and cannot be empty") - } - - return nil, nil -} - -// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (j *BackupJob) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - logger := log.FromContext(context.Background()) - logger.Info("validating BackupJob update", "name", j.Name, "namespace", j.Namespace) - - oldJob, ok := old.(*BackupJob) - if !ok { - return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a BackupJob but got a %T", old)) - } - - // Enforce immutability of backupClassName - if oldJob.Spec.BackupClassName != j.Spec.BackupClassName { - return nil, fmt.Errorf("backupClassName is immutable and cannot be changed from %q to %q", oldJob.Spec.BackupClassName, j.Spec.BackupClassName) - } - - return nil, nil -} - -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (j *BackupJob) ValidateDelete() (admission.Warnings, error) { - // No validation needed for deletion - return nil, nil -} diff --git a/api/backups/v1alpha1/backupjob_webhook_test.go b/api/backups/v1alpha1/backupjob_webhook_test.go deleted file mode 100644 index d143c5be..00000000 --- a/api/backups/v1alpha1/backupjob_webhook_test.go +++ /dev/null @@ -1,334 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -package v1alpha1 - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -func TestBackupJob_ValidateCreate(t *testing.T) { - tests := []struct { - name string - job *BackupJob - wantErr bool - errMsg string - }{ - { - name: "valid BackupJob with backupClassName", - job: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", - }, - }, - wantErr: false, - }, - { - name: "BackupJob with empty backupClassName should be rejected", - job: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "", - }, - }, - wantErr: true, - errMsg: "backupClassName is required and cannot be empty", - }, - { - name: "BackupJob with whitespace-only backupClassName should be rejected", - job: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: " ", - }, - }, - wantErr: true, - errMsg: "backupClassName is required and cannot be empty", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - warnings, err := tt.job.ValidateCreate() - if (err != nil) != tt.wantErr { - t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && err != nil { - if tt.errMsg != "" && err.Error() != tt.errMsg { - t.Errorf("ValidateCreate() error message = %v, want %v", err.Error(), tt.errMsg) - } - } - if warnings != nil && len(warnings) > 0 { - t.Logf("ValidateCreate() warnings = %v", warnings) - } - }) - } -} - -func TestBackupJob_ValidateUpdate(t *testing.T) { - baseJob := &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", - }, - } - - tests := []struct { - name string - old runtime.Object - new *BackupJob - wantErr bool - errMsg string - }{ - { - name: "update with same backupClassName should succeed", - old: baseJob, - new: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", // Same as old - }, - }, - wantErr: false, - }, - { - name: "update changing backupClassName should be rejected", - old: baseJob, - new: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "different-class", // Changed! - }, - }, - wantErr: true, - errMsg: "backupClassName is immutable and cannot be changed from \"velero\" to \"different-class\"", - }, - { - name: "update changing other fields but keeping backupClassName should succeed", - old: baseJob, - new: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - Labels: map[string]string{ - "new-label": "value", - }, - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm2", // Changed application - }, - BackupClassName: "velero", // Same as old - }, - }, - wantErr: false, - }, - { - name: "update when old backupClassName is empty should be rejected", - old: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "", // Empty in old - }, - }, - new: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", // Setting it for the first time - }, - }, - wantErr: true, - errMsg: "backupClassName is immutable", - }, - { - name: "update changing from non-empty to different non-empty should be rejected", - old: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "class-a", - }, - }, - new: &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "class-b", // Changed from class-a - }, - }, - wantErr: true, - errMsg: "backupClassName is immutable and cannot be changed from \"class-a\" to \"class-b\"", - }, - { - name: "update with invalid old object type should be rejected", - old: &corev1.Pod{ // Wrong type - will be cast to runtime.Object in test - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - }, - new: baseJob, - wantErr: true, - errMsg: "expected a BackupJob but got a", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - warnings, err := tt.new.ValidateUpdate(tt.old) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) - if err != nil { - t.Logf("Error message: %v", err.Error()) - } - return - } - if tt.wantErr && err != nil { - if tt.errMsg != "" { - if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) { - t.Errorf("ValidateUpdate() error message = %v, want contains %v", err.Error(), tt.errMsg) - } - } - } - if warnings != nil && len(warnings) > 0 { - t.Logf("ValidateUpdate() warnings = %v", warnings) - } - }) - } -} - -func TestBackupJob_ValidateDelete(t *testing.T) { - job := &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", - }, - } - - warnings, err := job.ValidateDelete() - if err != nil { - t.Errorf("ValidateDelete() should never return an error, got %v", err) - } - if warnings != nil && len(warnings) > 0 { - t.Logf("ValidateDelete() warnings = %v", warnings) - } -} - -func TestBackupJob_Default(t *testing.T) { - job := &BackupJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "default", - }, - Spec: BackupJobSpec{ - ApplicationRef: corev1.TypedLocalObjectReference{ - Kind: "VirtualMachine", - Name: "vm1", - }, - BackupClassName: "velero", - }, - } - - // Default() should not panic and should not modify the object - originalClassName := job.Spec.BackupClassName - job.Default() - if job.Spec.BackupClassName != originalClassName { - t.Errorf("Default() should not modify backupClassName, got %v, want %v", job.Spec.BackupClassName, originalClassName) - } -} - -// Helper function to check if a string contains a substring -func contains(s, substr string) bool { - if len(substr) == 0 { - return true - } - if len(s) < len(substr) { - return false - } - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/cmd/backup-controller/main.go b/cmd/backup-controller/main.go index fcc31d71..16dfc44c 100644 --- a/cmd/backup-controller/main.go +++ b/cmd/backup-controller/main.go @@ -162,21 +162,6 @@ func main() { os.Exit(1) } - if err = (&backupcontroller.BackupJobReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("backup-controller"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "BackupJob") - os.Exit(1) - } - - // Register BackupJob webhook for validation (immutability of backupClassName) - if err = backupsv1alpha1.SetupBackupJobWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "BackupJob") - os.Exit(1) - } - // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 188325e8..b97c2ade 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -19,7 +19,7 @@ set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. -CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} +CODEGEN_PKG=${CODEGEN_PKG:-~/go/pkg/mod/k8s.io/code-generator@v0.34.1} API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}" UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}" CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4" diff --git a/internal/backupcontroller/backupjob_controller.go b/internal/backupcontroller/backupjob_controller.go index c1ac71b5..b926722f 100644 --- a/internal/backupcontroller/backupjob_controller.go +++ b/internal/backupcontroller/backupjob_controller.go @@ -2,7 +2,6 @@ package backupcontroller import ( "context" - "fmt" "net/http" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -22,8 +21,8 @@ import ( backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1" ) -// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing -// Velero.strategy.backups.cozystack.io objects. +// BackupJobReconciler reconciles BackupJob with a strategy from the +// strategy.backups.cozystack.io API group. type BackupJobReconciler struct { client.Client dynamic.Interface diff --git a/internal/backupcontroller/restorejob_controller.go b/internal/backupcontroller/restorejob_controller.go index 19941959..71307ba3 100644 --- a/internal/backupcontroller/restorejob_controller.go +++ b/internal/backupcontroller/restorejob_controller.go @@ -106,9 +106,10 @@ func (r *RestoreJobReconciler) SetupWithManager(mgr ctrl.Manager) error { // getTargetApplicationRef determines the effective target application reference. // According to DESIGN.md, if spec.targetApplicationRef is omitted, drivers SHOULD // restore into backup.spec.applicationRef. +// The returned reference is normalized to ensure APIGroup has a default value. func (r *RestoreJobReconciler) getTargetApplicationRef(restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) corev1.TypedLocalObjectReference { if restoreJob.Spec.TargetApplicationRef != nil { - return *restoreJob.Spec.TargetApplicationRef + return backupsv1alpha1.NormalizeApplicationRef(*restoreJob.Spec.TargetApplicationRef) } return backup.Spec.ApplicationRef } diff --git a/internal/backupcontroller/velerostrategy_controller.go b/internal/backupcontroller/velerostrategy_controller.go index a26aab56..6809bd4f 100644 --- a/internal/backupcontroller/velerostrategy_controller.go +++ b/internal/backupcontroller/velerostrategy_controller.go @@ -208,30 +208,6 @@ func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1a return ctrl.Result{RequeueAfter: 5 * time.Second}, nil } -func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) { - logger := getLogger(ctx) - now := metav1.Now() - backupJob.Status.CompletedAt = &now - backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed - backupJob.Status.Message = message - - // Add condition - backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{ - Type: "Ready", - Status: metav1.ConditionFalse, - Reason: "BackupFailed", - Message: message, - LastTransitionTime: now, - }) - - if err := r.Status().Update(ctx, backupJob); err != nil { - logger.Error(err, "failed to update BackupJob status to Failed") - return ctrl.Result{}, err - } - logger.Debug("BackupJob failed", "message", message) - return ctrl.Result{}, nil -} - func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero, resolved *ResolvedBackupConfig) error { logger := getLogger(ctx) logger.Debug("createVeleroBackup called", "strategy", strategy.Name) @@ -476,39 +452,48 @@ func (r *RestoreJobReconciler) createVeleroRestore(ctx context.Context, restoreJ return fmt.Errorf("failed to get target application: %w", err) } - // Template the restore spec from the strategy - veleroRestoreSpec, err := template.Template(&strategy.Spec.Template.RestoreSpec, app.Object) - if err != nil { - return fmt.Errorf("failed to template Velero Restore spec: %w", err) + // Build template context + templateContext := map[string]interface{}{ + "Application": app.Object, + // TODO: Parameters are not currently stored on Backup, so they're unavailable during restore. + // This is a design limitation that should be addressed by persisting Parameters on the Backup object. + "Parameters": map[string]string{}, + } + + // Template the restore spec from the strategy, or use defaults if not specified + var veleroRestoreSpec velerov1.RestoreSpec + if strategy.Spec.Template.RestoreSpec != nil { + templatedSpec, err := template.Template(strategy.Spec.Template.RestoreSpec, templateContext) + if err != nil { + return fmt.Errorf("failed to template Velero Restore spec: %w", err) + } + veleroRestoreSpec = *templatedSpec } // Set the backupName in the spec (required by Velero) veleroRestoreSpec.BackupName = veleroBackupName + generateName := fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name) veleroRestore := &velerov1.Restore{ ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name), + GenerateName: generateName, Namespace: veleroNamespace, Labels: map[string]string{ backupsv1alpha1.OwningJobNameLabel: restoreJob.Name, backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace, }, }, - Spec: *veleroRestoreSpec, + Spec: veleroRestoreSpec, } - name := veleroRestore.GenerateName if err := r.Create(ctx, veleroRestore); err != nil { - if veleroRestore.Name != "" { - name = veleroRestore.Name - } - logger.Error(err, "failed to create Velero Restore", "name", veleroRestore.Name) + logger.Error(err, "failed to create Velero Restore", "generateName", generateName) r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "VeleroRestoreCreationFailed", - fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, name, err)) + fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, generateName, err)) return err } logger.Debug("created Velero Restore", "name", veleroRestore.Name, "namespace", veleroRestore.Namespace) r.Recorder.Event(restoreJob, corev1.EventTypeNormal, "VeleroRestoreCreated", - fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, name)) + fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, veleroRestore.Name)) return nil } diff --git a/packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml b/packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml index 686c079f..7a0ed6f5 100644 --- a/packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml +++ b/packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml @@ -74,7 +74,12 @@ spec: BackupClassName references a BackupClass that contains strategy and storage configuration. The BackupClass will be resolved to determine the appropriate strategy and storage based on the ApplicationRef. + This field is immutable once the BackupJob is created. + minLength: 1 type: string + x-kubernetes-validations: + - message: backupClassName is immutable + rule: self == oldSelf planRef: description: |- PlanRef refers to the Plan that requested this backup run. diff --git a/packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml b/packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml index 607a69b2..400737fc 100644 --- a/packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml +++ b/packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml @@ -14,7 +14,11 @@ spec: singular: restorejob scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + name: v1alpha1 schema: openAPIV3Schema: description: RestoreJob represents a single execution of a restore from a @@ -166,3 +170,5 @@ spec: type: object served: true storage: true + subresources: + status: {} diff --git a/packages/system/backup-controller/templates/class.yaml b/packages/system/backup-controller/templates/class.yaml deleted file mode 100644 index cb3164f2..00000000 --- a/packages/system/backup-controller/templates/class.yaml +++ /dev/null @@ -1,14 +0,0 @@ -kind: BackupClass -metadata: - name: velero -spec: - strategies: - - strategyRef: - apiGroup: strategy.backups.cozystack.io - kind: Velero - name: velero-backup-strategy-for-virtualmachines - application: - apiVersion: apps.cozystack.io - kind: VirtualMachine - parameters: - backupStorageLocationName: default diff --git a/packages/system/backup-controller/templates/strategy.yaml b/packages/system/backup-controller/templates/strategy.yaml deleted file mode 100644 index a8396d66..00000000 --- a/packages/system/backup-controller/templates/strategy.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: strategy.backups.cozystack.io/v1alpha1 -kind: Velero -metadata: - name: velero-strategy-default -spec: - template: - spec: - ttl: 720h - includedNamespaces: - - "*" diff --git a/packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml b/packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml index e817de23..34254aed 100644 --- a/packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml +++ b/packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml @@ -45,6 +45,428 @@ spec: VeleroTemplate describes the data a backup.velero.io should have when templated from a Velero backup strategy. properties: + restoreSpec: + description: RestoreSpec defines the specification for a Velero + restore. + properties: + backupName: + description: |- + BackupName is the unique name of the Velero backup to restore + from. + type: string + excludedNamespaces: + description: |- + ExcludedNamespaces contains a list of namespaces that are not + included in the restore. + items: + type: string + nullable: true + type: array + excludedResources: + description: |- + ExcludedResources is a slice of resource names that are not + included in the restore. + items: + type: string + nullable: true + type: array + existingResourcePolicy: + description: ExistingResourcePolicy specifies the restore + behavior for the Kubernetes resource to be restored + nullable: true + type: string + hooks: + description: Hooks represent custom behaviors that should + be executed during or post restore. + properties: + resources: + items: + description: |- + RestoreResourceHookSpec defines one or more RestoreResrouceHooks that should be executed based on + the rules defined for namespaces, resources, and label selector. + properties: + excludedNamespaces: + description: ExcludedNamespaces specifies the namespaces + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + excludedResources: + description: ExcludedResources specifies the resources + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + includedNamespaces: + description: |- + IncludedNamespaces specifies the namespaces to which this hook spec applies. If empty, it applies + to all namespaces. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which this hook spec applies. If empty, it applies + to all resources. + items: + type: string + nullable: true + type: array + labelSelector: + description: LabelSelector, if specified, filters + the resources to which this hook spec applies. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: Name is the name of this hook. + type: string + postHooks: + description: PostHooks is a list of RestoreResourceHooks + to execute during and after restoring a resource. + items: + description: RestoreResourceHook defines a restore + hook for a resource. + properties: + exec: + description: Exec defines an exec restore + hook. + properties: + command: + description: Command is the command and + arguments to execute from within a container + after a pod has been restored. + items: + type: string + minItems: 1 + type: array + container: + description: |- + Container is the container in the pod where the command should be executed. If not specified, + the pod's first container is used. + type: string + execTimeout: + description: |- + ExecTimeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + waitForReady: + description: WaitForReady ensures command + will be launched when container is Ready + instead of Running. + nullable: true + type: boolean + waitTimeout: + description: |- + WaitTimeout defines the maximum amount of time Velero should wait for the container to be Ready + before attempting to run the command. + type: string + required: + - command + type: object + init: + description: Init defines an init restore + hook. + properties: + initContainers: + description: InitContainers is list of + init containers to be added to a pod + during its restore. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + x-kubernetes-preserve-unknown-fields: true + timeout: + description: Timeout defines the maximum + amount of time Velero should wait for + the initContainers to complete. + type: string + type: object + type: object + type: array + required: + - name + type: object + type: array + type: object + includeClusterResources: + description: |- + IncludeClusterResources specifies whether cluster-scoped resources + should be included for consideration in the restore. If null, defaults + to true. + nullable: true + type: boolean + includedNamespaces: + description: |- + IncludedNamespaces is a slice of namespace names to include objects + from. If empty, all namespaces are included. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources is a slice of resource names to include + in the restore. If empty, all resources in the backup are included. + items: + type: string + nullable: true + type: array + itemOperationTimeout: + description: |- + ItemOperationTimeout specifies the time used to wait for RestoreItemAction operations + The default value is 4 hour. + type: string + labelSelector: + description: |- + LabelSelector is a metav1.LabelSelector to filter with + when restoring individual objects from the backup. If empty + or nil, all objects are included. Optional. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping is a map of source namespace names + to target namespace names to restore into. Any source + namespaces not included in the map will be restored into + namespaces of the same name. + type: object + orLabelSelectors: + description: |- + OrLabelSelectors is list of metav1.LabelSelector to filter with + when restoring individual objects from the backup. If multiple provided + they will be joined by the OR operator. LabelSelector as well as + OrLabelSelectors cannot co-exist in restore request, only one of them + can be used + items: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + nullable: true + type: array + preserveNodePorts: + description: PreserveNodePorts specifies whether to restore + old nodePorts from backup. + nullable: true + type: boolean + resourceModifier: + description: ResourceModifier specifies the reference to JSON + resource patches that should be applied to resources before + restoration. + nullable: true + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + restorePVs: + description: |- + RestorePVs specifies whether to restore all included + PVs from snapshot + nullable: true + type: boolean + restoreStatus: + description: |- + RestoreStatus specifies which resources we should restore the status + field. If nil, no objects are included. Optional. + nullable: true + properties: + excludedResources: + description: ExcludedResources specifies the resources + to which will not restore the status. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which will restore the status. + If empty, it applies to all resources. + items: + type: string + nullable: true + type: array + type: object + scheduleName: + description: |- + ScheduleName is the unique name of the Velero schedule to restore + from. If specified, and BackupName is empty, Velero will restore + from the most recent successful backup created from this schedule. + type: string + uploaderConfig: + description: UploaderConfig specifies the configuration for + the restore. + nullable: true + properties: + parallelFilesDownload: + description: ParallelFilesDownload is the concurrency + number setting for restore. + type: integer + writeSparseFiles: + description: WriteSparseFiles is a flag to indicate whether + write files sparsely or not. + nullable: true + type: boolean + type: object + type: object spec: description: BackupSpec defines the specification for a Velero backup. diff --git a/packages/system/virtual-machine-rd/cozyrds/backup-strategy.yaml b/packages/system/virtual-machine-rd/cozyrds/backup-strategy.yaml index 1312540b..b84f08e4 100644 --- a/packages/system/virtual-machine-rd/cozyrds/backup-strategy.yaml +++ b/packages/system/virtual-machine-rd/cozyrds/backup-strategy.yaml @@ -3,13 +3,13 @@ kind: Velero metadata: name: velero-backup-strategy-for-virtualmachines spec: - backupTemplate: + template: spec: # see https://velero.io/docs/v1.9/api-types/backup/ includedNamespaces: - - {{ .metadata.namespace }} + - {{ .Application.metadata.namespace }} labelSelector: - apps.cozystack.io/application.Kind: {{ .kind }} + apps.cozystack.io/application.Kind: {{ .Application.kind }} includedResources: - helmreleases.helm.toolkit.fluxcd.io diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index ba5ab71b..3ad28827 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -258,6 +258,13 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref common.ReferenceCallbac Format: "", }, }, + "externalIPsCount": { + SchemaProps: spec.SchemaProps{ + Description: "ExternalIPsCount holds the number of LoadBalancer services with assigned external IPs for Tenant applications.", + Type: []string{"integer"}, + Format: "int32", + }, + }, }, }, },