diff --git a/PROJECT b/PROJECT index 9b8be0d9..7ef15ee7 100644 --- a/PROJECT +++ b/PROJECT @@ -37,6 +37,10 @@ resources: kind: AerospikeBackup path: github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1 version: v1beta1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -46,6 +50,10 @@ resources: kind: AerospikeRestore path: github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1 version: v1beta1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -55,4 +63,8 @@ resources: kind: AerospikeBackupService path: github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1 version: v1beta1 + webhooks: + defaulting: false + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1beta1/aerospikebackup_webhook.go b/api/v1beta1/aerospikebackup_webhook.go new file mode 100644 index 00000000..e3ddb0f1 --- /dev/null +++ b/api/v1beta1/aerospikebackup_webhook.go @@ -0,0 +1,186 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + clientGoScheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/abhishekdwivedi3060/aerospike-backup-service/pkg/model" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/yaml" +) + +// log is for logging in this package. +var aerospikebackuplog = logf.Log.WithName("aerospikebackup-resource") + +func (r *AerospikeBackup) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//nolint:lll // for readability +//+kubebuilder:webhook:path=/mutate-asdb-aerospike-com-v1beta1-aerospikebackup,mutating=true,failurePolicy=fail,sideEffects=None,groups=asdb.aerospike.com,resources=aerospikebackups,verbs=create;update,versions=v1beta1,name=maerospikebackup.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &AerospikeBackup{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AerospikeBackup) Default() { + aerospikebackuplog.Info("default", "name", r.Name) +} + +//nolint:lll // for readability +//+kubebuilder:webhook:path=/validate-asdb-aerospike-com-v1beta1-aerospikebackup,mutating=false,failurePolicy=fail,sideEffects=None,groups=asdb.aerospike.com,resources=aerospikebackups,verbs=create;update,versions=v1beta1,name=vaerospikebackup.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AerospikeBackup{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackup) ValidateCreate() (admission.Warnings, error) { + aerospikebackuplog.Info("validate create", "name", r.Name) + + if err := r.validateBackupConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackup) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { + aerospikebackuplog.Info("validate update", "name", r.Name) + + if err := r.validateBackupConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackup) ValidateDelete() (admission.Warnings, error) { + aerospikebackuplog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} + +func (r *AerospikeBackup) validateBackupConfig() error { + var backupSvc AerospikeBackupService + + cl, gErr := getK8sClient() + if gErr != nil { + return gErr + } + + if err := cl.Get(context.TODO(), + types.NamespacedName{Name: r.Spec.BackupService.Name, Namespace: r.Spec.BackupService.Namespace}, + &backupSvc); err != nil { + return err + } + + var config model.Config + + if err := yaml.Unmarshal(backupSvc.Spec.Config.Raw, &config); err != nil { + return err + } + + configMap := make(map[string]interface{}) + + if err := yaml.Unmarshal(r.Spec.Config.Raw, &configMap); err != nil { + return err + } + + if _, ok := configMap["aerospike-cluster"]; !ok { + return fmt.Errorf("aerospike-cluster field is required in config") + } + + cluster, ok := configMap["aerospike-cluster"].(map[string]interface{}) + if !ok { + return fmt.Errorf("aerospike-cluster field is not in the right format") + } + + clusterBytes, cErr := yaml.Marshal(cluster) + if cErr != nil { + return cErr + } + + aeroClusters := make(map[string]*model.AerospikeCluster) + + if err := yaml.Unmarshal(clusterBytes, &aeroClusters); err != nil { + return err + } + + if len(config.AerospikeClusters) == 0 { + config.AerospikeClusters = make(map[string]*model.AerospikeCluster) + } + + for name, aeroCluster := range aeroClusters { + config.AerospikeClusters[name] = aeroCluster + } + + if _, ok = configMap["backup-routines"]; !ok { + return fmt.Errorf("backup-routines field is required in config") + } + + routines, ok := configMap["backup-routines"].(map[string]interface{}) + if !ok { + return fmt.Errorf("backup-routines field is not in the right format") + } + + routineBytes, rErr := yaml.Marshal(routines) + if rErr != nil { + return rErr + } + + backupRoutines := make(map[string]*model.BackupRoutine) + + if err := yaml.Unmarshal(routineBytes, &backupRoutines); err != nil { + return err + } + + if len(config.BackupRoutines) == 0 { + config.BackupRoutines = make(map[string]*model.BackupRoutine) + } + + for name, routine := range backupRoutines { + config.BackupRoutines[name] = routine + } + + return config.Validate() +} + +func getK8sClient() (client.Client, error) { + restConfig := ctrl.GetConfigOrDie() + + cl, err := client.New(restConfig, client.Options{ + Scheme: clientGoScheme.Scheme, + }) + if err != nil { + return nil, err + } + + return cl, nil +} diff --git a/api/v1beta1/aerospikebackupservice_webhook.go b/api/v1beta1/aerospikebackupservice_webhook.go new file mode 100644 index 00000000..38b76676 --- /dev/null +++ b/api/v1beta1/aerospikebackupservice_webhook.go @@ -0,0 +1,88 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "github.com/abhishekdwivedi3060/aerospike-backup-service/pkg/model" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/yaml" +) + +// log is for logging in this package. +var aerospikebackupservicelog = logf.Log.WithName("aerospikebackupservice-resource") + +func (r *AerospikeBackupService) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +var _ webhook.Defaulter = &AerospikeBackupService{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AerospikeBackupService) Default() { + aerospikebackupservicelog.Info("default", "name", r.Name) +} + +//nolint:lll // for readability +//+kubebuilder:webhook:path=/validate-asdb-aerospike-com-v1beta1-aerospikebackupservice,mutating=false,failurePolicy=fail,sideEffects=None,groups=asdb.aerospike.com,resources=aerospikebackupservices,verbs=create;update,versions=v1beta1,name=vaerospikebackupservice.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AerospikeBackupService{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackupService) ValidateCreate() (admission.Warnings, error) { + aerospikebackupservicelog.Info("validate create", "name", r.Name) + + if err := r.validateBackupServiceConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackupService) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) { + aerospikebackupservicelog.Info("validate update", "name", r.Name) + + if err := r.validateBackupServiceConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeBackupService) ValidateDelete() (admission.Warnings, error) { + aerospikebackupservicelog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} + +func (r *AerospikeBackupService) validateBackupServiceConfig() error { + var config model.Config + + if err := yaml.Unmarshal(r.Spec.Config.Raw, &config); err != nil { + return err + } + + return config.Validate() +} diff --git a/api/v1beta1/aerospikerestore_webhook.go b/api/v1beta1/aerospikerestore_webhook.go new file mode 100644 index 00000000..867a35b4 --- /dev/null +++ b/api/v1beta1/aerospikerestore_webhook.go @@ -0,0 +1,119 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "fmt" + "reflect" + "time" + + "github.com/abhishekdwivedi3060/aerospike-backup-service/pkg/model" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/yaml" +) + +// log is for logging in this package. +var aerospikerestorelog = logf.Log.WithName("aerospikerestore-resource") + +const defaultPollingPeriod time.Duration = 60 * time.Second + +func (r *AerospikeRestore) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//nolint:lll // for readability +//+kubebuilder:webhook:path=/mutate-asdb-aerospike-com-v1beta1-aerospikerestore,mutating=true,failurePolicy=fail,sideEffects=None,groups=asdb.aerospike.com,resources=aerospikerestores,verbs=create;update,versions=v1beta1,name=maerospikerestore.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &AerospikeRestore{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *AerospikeRestore) Default() { + aerospikerestorelog.Info("default", "name", r.Name) + + if r.Spec.PollingPeriod.Duration.Seconds() == 0 { + r.Spec.PollingPeriod.Duration = defaultPollingPeriod + } +} + +//nolint:lll // for readability +//+kubebuilder:webhook:path=/validate-asdb-aerospike-com-v1beta1-aerospikerestore,mutating=false,failurePolicy=fail,sideEffects=None,groups=asdb.aerospike.com,resources=aerospikerestores,verbs=create;update,versions=v1beta1,name=vaerospikerestore.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AerospikeRestore{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeRestore) ValidateCreate() (admission.Warnings, error) { + aerospikerestorelog.Info("validate create", "name", r.Name) + + if err := r.validateRestoreConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeRestore) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + aerospikerestorelog.Info("validate update", "name", r.Name) + + oldRestore := old.(*AerospikeRestore) + + if !reflect.DeepEqual(oldRestore.Spec, r.Spec) { + return nil, fmt.Errorf("AerospikeRestore Spec is immutable") + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *AerospikeRestore) ValidateDelete() (admission.Warnings, error) { + aerospikerestorelog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil, nil +} + +func (r *AerospikeRestore) validateRestoreConfig() error { + switch r.Spec.Type { + case Full, Incremental: + var restoreRequest model.RestoreRequest + + if err := yaml.Unmarshal(r.Spec.Config.Raw, &restoreRequest); err != nil { + return err + } + + return restoreRequest.Validate() + + case TimeStamp: + var restoreRequest model.RestoreTimestampRequest + + if err := yaml.Unmarshal(r.Spec.Config.Raw, &restoreRequest); err != nil { + return err + } + + return restoreRequest.Validate() + + default: + // Code flow should not come here + return fmt.Errorf("unknown restore type %s", r.Spec.Type) + } +} diff --git a/config/samples/asdb_v1beta1_aerospikerestore.yaml b/config/samples/asdb_v1beta1_aerospikerestore.yaml index 1c4e5e01..1f096c7c 100644 --- a/config/samples/asdb_v1beta1_aerospikerestore.yaml +++ b/config/samples/asdb_v1beta1_aerospikerestore.yaml @@ -27,6 +27,6 @@ spec: no-generation: true no-indexes: true source: - "path": "/localStorage/test-routine/backup/1719824240816/data/test" + "path": "/localStorage/test-routine/backup/1719911170537/data/test" "type": 0 diff --git a/go.mod b/go.mod index 0de80b2d..272fd40b 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( ) require ( + github.com/abhishekdwivedi3060/aerospike-backup-service v0.0.0-20240604175608-042db47ee091 github.com/aerospike/aerospike-client-go/v7 v7.4.0 github.com/deckarep/golang-set/v2 v2.3.1 github.com/sirupsen/logrus v1.9.0 @@ -33,6 +34,7 @@ require ( ) require ( + github.com/aws/smithy-go v1.20.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -68,6 +70,7 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/reugn/go-quartz v0.11.2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index 03fb7764..300994a4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/abhishekdwivedi3060/aerospike-backup-service v0.0.0-20240604175608-042db47ee091 h1:eY5MT18Ocf5cCHohbZ1qcdophb/aOqr066lwSM3zGPk= +github.com/abhishekdwivedi3060/aerospike-backup-service v0.0.0-20240604175608-042db47ee091/go.mod h1:CMA+bHRLvL/Kj/aLlbu95iNnlPnvP67q62X81b5e2G4= github.com/aerospike/aerospike-client-go/v7 v7.4.0 h1:g8/7v8RHhQhTArhW3C7Au7o+u8j8x5eySZL6MXfpHKU= github.com/aerospike/aerospike-client-go/v7 v7.4.0/go.mod h1:pPKnWiS8VDJcH4IeB1b8SA2TWnkjcVLHwAAJ+BHfGK8= github.com/aerospike/aerospike-management-lib v1.4.0 h1:wT0l3kwzXv5DV5Cd+hD0BQq3hjSIyaPX1HaUb1304TI= @@ -6,6 +8,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -111,6 +115,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/reugn/go-quartz v0.11.2 h1:+jc54Ji06n/D/endEPmc+CuG/Jc8466nda1oxtFRrks= +github.com/reugn/go-quartz v0.11.2/go.mod h1:no4ktgYbAAuY0E1SchR8cTx1LF4jYIzdgaQhzRPSkpk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= diff --git a/main.go b/main.go index eb3f3522..af7d3f1f 100644 --- a/main.go +++ b/main.go @@ -20,15 +20,16 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - // +kubebuilder:scaffold:imports + "github.com/aerospike/aerospike-management-lib/asconfig" + asdbv1 "github.com/aerospike/aerospike-kubernetes-operator/api/v1" + // +kubebuilder:scaffold:imports asdbv1beta1 "github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1" aerospikecluster "github.com/aerospike/aerospike-kubernetes-operator/controllers" "github.com/aerospike/aerospike-kubernetes-operator/controllers/backup" backupservice "github.com/aerospike/aerospike-kubernetes-operator/controllers/backup-service" "github.com/aerospike/aerospike-kubernetes-operator/controllers/restore" "github.com/aerospike/aerospike-kubernetes-operator/pkg/configschema" - "github.com/aerospike/aerospike-management-lib/asconfig" ) var ( @@ -158,6 +159,20 @@ func main() { os.Exit(1) } + if err = (&backupservice.AerospikeBackupServiceReconciler{ + Client: client, + Scheme: mgr.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("AerospikeBackupService"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AerospikeBackupService") + os.Exit(1) + } + + if err = (&asdbv1beta1.AerospikeBackupService{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AerospikeBackupService") + os.Exit(1) + } + if err = (&backup.AerospikeBackupReconciler{ Client: client, Scheme: mgr.GetScheme(), @@ -167,6 +182,11 @@ func main() { os.Exit(1) } + if err = (&asdbv1beta1.AerospikeBackup{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AerospikeBackup") + os.Exit(1) + } + if err = (&restore.AerospikeRestoreReconciler{ Client: client, Scheme: mgr.GetScheme(), @@ -176,14 +196,11 @@ func main() { os.Exit(1) } - if err = (&backupservice.AerospikeBackupServiceReconciler{ - Client: client, - Scheme: mgr.GetScheme(), - Log: ctrl.Log.WithName("controllers").WithName("AerospikeBackupService"), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "AerospikeBackupService") + if err = (&asdbv1beta1.AerospikeRestore{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AerospikeRestore") os.Exit(1) } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil {