Skip to content

Commit

Permalink
Improve Templates deletion validation
Browse files Browse the repository at this point in the history
The following rules are validated:
1. ClusterTemplate or ServiceTemplate can't be removed if it is in use by
   ManagedCluster or TemplateChain
2. ClusterTemplate or ServiceTemplate can't be removed if the template is
   managed by the TemplateManagement
3. ProviderTemplate can't be removed if it's a core provider or enabled in
   Management spec.providers
  • Loading branch information
eromanova committed Dec 3, 2024
1 parent 5ddea6f commit de21f26
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 57 deletions.
6 changes: 3 additions & 3 deletions api/v1alpha1/indexers.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ func extractReleaseTemplates(rawObj client.Object) []string {
const TemplateChainSupportedTemplatesIndexKey = ".spec.supportedTemplates[].Name"

func setupClusterTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &ClusterTemplateChain{}, TemplateChainSupportedTemplatesIndexKey, extractSupportedTemplatesNames)
return mgr.GetFieldIndexer().IndexField(ctx, &ClusterTemplateChain{}, TemplateChainSupportedTemplatesIndexKey, ExtractSupportedTemplatesNamesFromTemplateChain)
}

func setupServiceTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &ServiceTemplateChain{}, TemplateChainSupportedTemplatesIndexKey, extractSupportedTemplatesNames)
return mgr.GetFieldIndexer().IndexField(ctx, &ServiceTemplateChain{}, TemplateChainSupportedTemplatesIndexKey, ExtractSupportedTemplatesNamesFromTemplateChain)
}

func extractSupportedTemplatesNames(rawObj client.Object) []string {
func ExtractSupportedTemplatesNamesFromTemplateChain(rawObj client.Object) []string {
chainSpec := TemplateChainSpec{}
switch chain := rawObj.(type) {
case *ClusterTemplateChain:
Expand Down
10 changes: 7 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,15 +352,19 @@ func setupWebhooks(mgr ctrl.Manager, currentNamespace string) error {
setupLog.Error(err, "unable to create webhook", "webhook", "ServiceTemplateChain")
return err
}
if err := (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr); err != nil {

templateValidator := hmcwebhook.TemplateValidator{
SystemNamespace: currentNamespace,
}
if err := (&hmcwebhook.ClusterTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ClusterTemplate")
return err
}
if err := (&hmcwebhook.ServiceTemplateValidator{SystemNamespace: currentNamespace}).SetupWebhookWithManager(mgr); err != nil {
if err := (&hmcwebhook.ServiceTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ServiceTemplate")
return err
}
if err := (&hmcwebhook.ProviderTemplateValidator{}).SetupWebhookWithManager(mgr); err != nil {
if err := (&hmcwebhook.ProviderTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ProviderTemplate")
return err
}
Expand Down
10 changes: 7 additions & 3 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,17 @@ var _ = BeforeSuite(func() {
err = (&hmcwebhook.ServiceTemplateChainValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr)
templateValidator := hmcwebhook.TemplateValidator{
SystemNamespace: testSystemNamespace,
}

err = (&hmcwebhook.ClusterTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ServiceTemplateValidator{SystemNamespace: testSystemNamespace}).SetupWebhookWithManager(mgr)
err = (&hmcwebhook.ServiceTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ProviderTemplateValidator{}).SetupWebhookWithManager(mgr)
err = (&hmcwebhook.ProviderTemplateValidator{TemplateValidator: templateValidator}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

go func() {
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/template_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ var _ = Describe("Template Controller", func() {
err = k8sClient.Get(ctx, typeNamespacedName, providerTemplateResource)
Expect(err).NotTo(HaveOccurred())

By("Cleanup the specific resource instance ClusterTemplate")
By("Cleanup the specific resource instance ProviderTemplate")
Expect(k8sClient.Delete(ctx, providerTemplateResource)).To(Succeed())
})

Expand Down
127 changes: 106 additions & 21 deletions internal/webhook/template_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook"
Expand All @@ -29,14 +32,23 @@ import (
"github.com/Mirantis/hmc/api/v1alpha1"
)

type ClusterTemplateValidator struct {
var errTemplateDeletionForbidden = errors.New("template deletion is forbidden")

type TemplateValidator struct {
client.Client
SystemNamespace string
templateKind string
templateChainKind string
}

var errTemplateDeletionForbidden = errors.New("template deletion is forbidden")
type ClusterTemplateValidator struct {
TemplateValidator
}

func (v *ClusterTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
v.templateKind = v1alpha1.ClusterTemplateKind
v.templateChainKind = v1alpha1.ClusterTemplateChainKind
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ClusterTemplate{}).
WithValidator(v).
Expand Down Expand Up @@ -66,16 +78,18 @@ func (v *ClusterTemplateValidator) ValidateDelete(ctx context.Context, obj runti
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ClusterTemplate but got a %T", obj))
}

managedClusters := &v1alpha1.ManagedClusterList{}
if err := v.Client.List(ctx, managedClusters,
client.InNamespace(template.Namespace),
client.MatchingFields{v1alpha1.ManagedClusterTemplateIndexKey: template.Name},
client.Limit(1)); err != nil {
inUseByCluster, err := v.templateIsInUseByCluster(ctx, template)
if err != nil {
return nil, err
}
if inUseByCluster {
return admission.Warnings{fmt.Sprintf("The %s object can't be removed if ManagedCluster objects referencing it still exist", v.templateKind)}, errTemplateDeletionForbidden
}

if len(managedClusters.Items) > 0 {
return admission.Warnings{"The ClusterTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, errTemplateDeletionForbidden
owners := v.getTemplateChainsOwners(template)
if len(owners) > 0 {
return admission.Warnings{fmt.Sprintf("The %s object can't be removed if it is managed by %s: %s",
v.templateKind, v.templateChainKind, strings.Join(owners, ", "))}, errTemplateDeletionForbidden
}

return nil, nil
Expand All @@ -87,12 +101,13 @@ func (*ClusterTemplateValidator) Default(context.Context, runtime.Object) error
}

type ServiceTemplateValidator struct {
client.Client
SystemNamespace string
TemplateValidator
}

func (v *ServiceTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
v.templateKind = v1alpha1.ServiceTemplateKind
v.templateChainKind = v1alpha1.ServiceTemplateChainKind
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ServiceTemplate{}).
WithValidator(v).
Expand Down Expand Up @@ -122,16 +137,18 @@ func (v *ServiceTemplateValidator) ValidateDelete(ctx context.Context, obj runti
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ServiceTemplate but got a %T", obj))
}

managedClusters := &v1alpha1.ManagedClusterList{}
if err := v.Client.List(ctx, managedClusters,
client.InNamespace(tmpl.Namespace),
client.MatchingFields{v1alpha1.ManagedClusterServiceTemplatesIndexKey: tmpl.Name},
client.Limit(1)); err != nil {
return nil, err
inUseByCluster, err := v.templateIsInUseByCluster(ctx, tmpl)
if err != nil {
return nil, fmt.Errorf("failed to check if the ServiceTemplate %s/%s is in use: %w", tmpl.Namespace, tmpl.Name, err)
}
if inUseByCluster {
return admission.Warnings{fmt.Sprintf("The %s object can't be removed if ManagedCluster objects referencing it still exist", v.templateKind)}, errTemplateDeletionForbidden
}

if len(managedClusters.Items) > 0 {
return admission.Warnings{"The ServiceTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, errTemplateDeletionForbidden
owners := v.getTemplateChainsOwners(tmpl)
if len(owners) > 0 {
return admission.Warnings{fmt.Sprintf("The %s object can't be removed if it is managed by %s: %s",
v.templateKind, v.templateChainKind, strings.Join(owners, ", "))}, errTemplateDeletionForbidden
}

// MultiClusterServices can only refer to serviceTemplates in system namespace.
Expand All @@ -157,11 +174,12 @@ func (*ServiceTemplateValidator) Default(_ context.Context, _ runtime.Object) er
}

type ProviderTemplateValidator struct {
client.Client
TemplateValidator
}

func (v *ProviderTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
v.templateKind = v1alpha1.ProviderTemplateKind
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ProviderTemplate{}).
WithValidator(v).
Expand All @@ -185,11 +203,78 @@ func (*ProviderTemplateValidator) ValidateUpdate(_ context.Context, _, _ runtime
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (*ProviderTemplateValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
func (v *ProviderTemplateValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
template, ok := obj.(*v1alpha1.ProviderTemplate)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ProviderTemplate but got a %T", obj))
}

mgmt := &v1alpha1.Management{}
err := v.Get(ctx, types.NamespacedName{Name: v1alpha1.ManagementName}, mgmt)
if err != nil {
if !apierrors.IsNotFound(err) {
return nil, err
}
}
if slices.Contains(mgmt.Templates(), template.Name) {
return admission.Warnings{fmt.Sprintf("The ProviderTemplate %s cannot be removed while it is enabled in the Management spec",
template.GetName())}, errTemplateDeletionForbidden
}

owners := getReleasesOwners(template)
if len(owners) > 0 {
return admission.Warnings{fmt.Sprintf("The ProviderTemplate %s cannot be removed while it is part of existing Releases: %s",
template.GetName(), strings.Join(owners, ", "))}, errTemplateDeletionForbidden
}
return nil, nil
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (*ProviderTemplateValidator) Default(_ context.Context, _ runtime.Object) error {
return nil
}

func (v TemplateValidator) templateIsInUseByCluster(ctx context.Context, template client.Object) (bool, error) {
var key string

switch v.templateKind {
case v1alpha1.ClusterTemplateKind:
key = v1alpha1.ManagedClusterTemplateIndexKey
case v1alpha1.ServiceTemplateKind:
key = v1alpha1.ManagedClusterServiceTemplatesIndexKey
default:
return false, fmt.Errorf("invalid Template kind %s. Supported values are: %s and %s", v.templateKind, v1alpha1.ClusterTemplateKind, v1alpha1.ServiceTemplateKind)
}

managedClusters := &v1alpha1.ManagedClusterList{}
if err := v.Client.List(ctx, managedClusters,
client.InNamespace(template.GetNamespace()),
client.MatchingFields{key: template.GetName()},
client.Limit(1)); err != nil {
return false, err
}
if len(managedClusters.Items) > 0 {
return true, nil
}
return false, nil
}

func (v TemplateValidator) getTemplateChainsOwners(template client.Object) []string {
var owners []string
for _, ownerRef := range template.GetOwnerReferences() {
if ownerRef.Kind == v.templateChainKind {
owners = append(owners, ownerRef.Name)
}
}
return owners
}

func getReleasesOwners(template client.Object) []string {
var owners []string
for _, ownerRef := range template.GetOwnerReferences() {
if ownerRef.Kind == v1alpha1.ReleaseKind {
owners = append(owners, ownerRef.Name)
}
}
return owners
}
Loading

0 comments on commit de21f26

Please sign in to comment.