From 2cca1bc8d85ae016b36c0cf5ffcbc8c030edd2a7 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Thu, 22 Jan 2026 12:30:41 +0400 Subject: [PATCH] add webhook for validation backupClass is immutable in BackupJob Signed-off-by: Andrey Kolkov --- api/backups/v1alpha1/backupjob_types.go | 2 + api/backups/v1alpha1/backupjob_webhook.go | 71 +++++++++++++++++++++++ cmd/backup-controller/main.go | 6 ++ 3 files changed, 79 insertions(+) create mode 100644 api/backups/v1alpha1/backupjob_webhook.go diff --git a/api/backups/v1alpha1/backupjob_types.go b/api/backups/v1alpha1/backupjob_types.go index f9aa42e6..6675c71d 100644 --- a/api/backups/v1alpha1/backupjob_types.go +++ b/api/backups/v1alpha1/backupjob_types.go @@ -52,6 +52,8 @@ type BackupJobSpec struct { // 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. + // +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 new file mode 100644 index 00000000..8c167c22 --- /dev/null +++ b/api/backups/v1alpha1/backupjob_webhook.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +import ( + "context" + "fmt" + + 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" + "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 + +var _ webhook.Defaulter = &BackupJob{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (j *BackupJob) Default() { + // No defaults needed for BackupJob currently +} + +// +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 + +var _ webhook.Validator = &BackupJob{} + +// 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 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 != "" && 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/cmd/backup-controller/main.go b/cmd/backup-controller/main.go index a99f9350..3278ecf7 100644 --- a/cmd/backup-controller/main.go +++ b/cmd/backup-controller/main.go @@ -168,6 +168,12 @@ func main() { 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 {