diff --git a/cmd/main.go b/cmd/main.go index 1a9914ef9..0233a22fd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -181,6 +181,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ClusterTemplate") os.Exit(1) } + + ctx := ctrl.SetupSignalHandler() + if err = hmcwebhook.SetupTemplateIndex(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create template index", "index", "ClusterTemplate") + os.Exit(1) + } + if err = (&controller.ServiceTemplateReconciler{ TemplateReconciler: controller.TemplateReconciler{ Client: mgr.GetClient(), @@ -292,7 +299,7 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index f389a60a5..718452e84 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -141,6 +141,9 @@ var _ = BeforeSuite(func() { err = (&hmcwebhook.ManagementValidator{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = hmcwebhook.SetupTemplateIndex(ctx, mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) diff --git a/internal/webhook/management_webhook.go b/internal/webhook/management_webhook.go index 47f6b2386..1c008fa11 100644 --- a/internal/webhook/management_webhook.go +++ b/internal/webhook/management_webhook.go @@ -34,7 +34,7 @@ type ManagementValidator struct { } var ( - ManagementDeletionForbidden = errors.New("management deletion is forbidden") + ErrManagementDeletionForbidden = errors.New("management deletion is forbidden") ) func (in *ManagementValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { @@ -69,7 +69,7 @@ func (v *ManagementValidator) ValidateDelete(ctx context.Context, _ runtime.Obje return nil, err } if len(managedClusters.Items) > 0 { - return admission.Warnings{"The Management object can't be removed if ManagedCluster objects still exist"}, ManagementDeletionForbidden + return admission.Warnings{"The Management object can't be removed if ManagedCluster objects still exist"}, ErrManagementDeletionForbidden } return nil, nil } diff --git a/internal/webhook/template_webhook.go b/internal/webhook/template_webhook.go index ab51d9308..b332c2a34 100644 --- a/internal/webhook/template_webhook.go +++ b/internal/webhook/template_webhook.go @@ -16,7 +16,11 @@ package webhook // nolint:dupl import ( "context" + "errors" + "fmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -30,6 +34,12 @@ type ClusterTemplateValidator struct { client.Client } +const TemplateKey = ".spec.template" + +var ( + ErrTemplateDeletionForbidden = errors.New("template deletion is forbidden") +) + func (in *ClusterTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { in.Client = mgr.GetClient() return ctrl.NewWebhookManagedBy(mgr). @@ -55,7 +65,26 @@ func (*ClusterTemplateValidator) ValidateUpdate(_ context.Context, _ runtime.Obj } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. -func (*ClusterTemplateValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { +func (v *ClusterTemplateValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + template, ok := obj.(*v1alpha1.ClusterTemplate) + if !ok { + return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ClusterTemplate but got a %T", obj)) + } + + managedClusters := &v1alpha1.ManagedClusterList{} + listOptions := client.ListOptions{ + FieldSelector: fields.SelectorFromSet(fields.Set{TemplateKey: template.Name}), + Limit: 1, + } + err := v.Client.List(ctx, managedClusters, &listOptions) + if err != nil { + return nil, err + } + + if len(managedClusters.Items) > 0 { + return admission.Warnings{"The ClusterTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, ErrTemplateDeletionForbidden + } + return nil, nil } @@ -139,3 +168,23 @@ func (*ProviderTemplateValidator) ValidateDelete(_ context.Context, _ runtime.Ob func (*ProviderTemplateValidator) Default(_ context.Context, _ runtime.Object) error { return nil } + +func ExtractTemplateName(rawObj client.Object) []string { + cluster, ok := rawObj.(*v1alpha1.ManagedCluster) + if !ok { + return nil + } + if cluster.Spec.Template == "" { + return []string{} + } + return []string{cluster.Spec.Template} +} + +func SetupTemplateIndex(ctx context.Context, mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer(). + IndexField(ctx, &v1alpha1.ManagedCluster{}, TemplateKey, ExtractTemplateName); err != nil { + return err + } + + return nil +} diff --git a/internal/webhook/template_webhook_test.go b/internal/webhook/template_webhook_test.go new file mode 100644 index 000000000..d980f311d --- /dev/null +++ b/internal/webhook/template_webhook_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 +// +// 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 webhook + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/test/objects/managedcluster" + "github.com/Mirantis/hmc/test/objects/template" + "github.com/Mirantis/hmc/test/scheme" +) + +func TestClusterTemplateValidateDelete(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + tpl := template.NewClusterTemplate(template.WithName("testTemplateFail")) + tplTest := template.NewClusterTemplate(template.WithName("testTemplate")) + + tests := []struct { + name string + template *v1alpha1.ClusterTemplate + existingObjects []runtime.Object + err string + warnings admission.Warnings + }{ + { + name: "should fail if ManagedCluster objects exist", + template: tpl, + existingObjects: []runtime.Object{managedcluster.NewManagedCluster(managedcluster.WithTemplate(tpl.Name))}, + warnings: admission.Warnings{"The ClusterTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, + err: "template deletion is forbidden", + }, + { + name: "should be OK because of a different cluster", + template: tpl, + existingObjects: []runtime.Object{managedcluster.NewManagedCluster()}, + }, + { + name: "should succeed", + template: template.NewClusterTemplate(), + existingObjects: []runtime.Object{managedcluster.NewManagedCluster(managedcluster.WithTemplate(tplTest.Name))}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithRuntimeObjects(tt.existingObjects...). + WithIndex(tt.existingObjects[0], TemplateKey, ExtractTemplateName). + Build() + validator := &ClusterTemplateValidator{Client: c} + warn, err := validator.ValidateDelete(ctx, tt.template) + if tt.err != "" { + g.Expect(err).To(HaveOccurred()) + if err.Error() != tt.err { + t.Fatalf("expected error '%s', got error: %s", tt.err, err.Error()) + } + } else { + g.Expect(err).To(Succeed()) + } + if len(tt.warnings) > 0 { + g.Expect(warn).To(Equal(tt.warnings)) + } else { + g.Expect(warn).To(BeEmpty()) + } + }) + } +}