Skip to content

Commit

Permalink
Merge pull request K0rdent#301 from slysunkin/HMC-246
Browse files Browse the repository at this point in the history
Prevent ClusterTemplate of being removed if ManagedCluster exists
  • Loading branch information
Kshatrix authored Sep 18, 2024
2 parents 55a88e0 + acb99ad commit 7be8bb2
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 4 deletions.
9 changes: 8 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
4 changes: 2 additions & 2 deletions internal/webhook/management_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
51 changes: 50 additions & 1 deletion internal/webhook/template_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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).
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
88 changes: 88 additions & 0 deletions internal/webhook/template_webhook_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
}

0 comments on commit 7be8bb2

Please sign in to comment.