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 Nov 29, 2024
1 parent d49e006 commit d18beb2
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 47 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
29 changes: 29 additions & 0 deletions api/v1alpha1/management_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ func (in *Component) HelmValues() (values map[string]any, err error) {
return values, err
}

// Templates returns the templates for all enabled components.
// If a template is not specified in Management, it retrieves the default from the Release object.
func (in *Management) Templates(release *Release) []string {
templates := make([]string, 0, len(in.Spec.Providers)+2)
if core := in.Spec.Core; core != nil {
if core.HMC.Template != "" {
templates = append(templates, core.HMC.Template)
} else {
templates = append(templates, release.Spec.HMC.Template)
}
if core.CAPI.Template != "" {
templates = append(templates, core.CAPI.Template)
} else {
templates = append(templates, release.Spec.CAPI.Template)
}
} else {
templates = append(templates, release.Spec.HMC.Template, release.Spec.CAPI.Template)
}

for _, p := range in.Spec.Providers {
if p.Template != "" {
templates = append(templates, p.Template)
} else {
templates = append(templates, release.ProviderTemplate(p.Name))
}
}
return templates
}

func GetDefaultProviders() []Provider {
return []Provider{
{Name: ProviderK0smotronName},
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
153 changes: 131 additions & 22 deletions internal/webhook/template_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"context"
"errors"
"fmt"
"slices"

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 +31,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 +77,16 @@ 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 {
return nil, err
if templateManagedByHMC(template) {
return admission.Warnings{"The Template is managed by the TemplateManagement and " + v.templateChainKind}, errTemplateDeletionForbidden
}

if len(managedClusters.Items) > 0 {
return admission.Warnings{"The ClusterTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, errTemplateDeletionForbidden
inUse, warnings, err := v.templateIsInUse(ctx, template)
if err != nil {
return nil, fmt.Errorf("failed to check if the ClusterTemplate %s/%s is in use: %w", template.Namespace, template.Name, err)
}
if inUse {
return warnings, errTemplateDeletionForbidden
}

return nil, nil
Expand All @@ -87,12 +98,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 +134,16 @@ 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
if templateManagedByHMC(tmpl) {
return admission.Warnings{"The Template is managed by the TemplateManagement and " + v.templateChainKind}, errTemplateDeletionForbidden
}

if len(managedClusters.Items) > 0 {
return admission.Warnings{"The ServiceTemplate object can't be removed if ManagedCluster objects referencing it still exist"}, errTemplateDeletionForbidden
inUse, warnings, err := v.templateIsInUse(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 inUse {
return warnings, errTemplateDeletionForbidden
}

// MultiClusterServices can only refer to serviceTemplates in system namespace.
Expand All @@ -157,11 +169,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 +198,107 @@ 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
}
return nil, nil
}

release := &v1alpha1.Release{}
err = v.Get(ctx, types.NamespacedName{Name: mgmt.Spec.Release}, release)
if err != nil {
return nil, err
}
if slices.Contains(mgmt.Templates(release), template.GetName()) {
return admission.Warnings{fmt.Sprintf("The ProviderTemplate %s can't be removed while it is enabled in the Management spec", template.GetName())}, 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) templateIsInUse(ctx context.Context, template client.Object) (bool, admission.Warnings, error) {
inUseByCluster, err := v.templateIsInUseByCluster(ctx, template)
if err != nil {
return false, nil, err
}
if inUseByCluster {
return true, admission.Warnings{fmt.Sprintf("The %s object can't be removed if ManagedCluster objects referencing it still exist", v.templateKind)}, nil
}
inUseByChain, err := v.templateIsInUseByTemplateChain(ctx, template)
if err != nil {
return false, nil, err
}
if inUseByChain {
return true, admission.Warnings{fmt.Sprintf("The %s object can't be removed if %s object referencing it exists", v.templateKind, v.templateChainKind)}, nil
}
return false, nil, 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) templateIsInUseByTemplateChain(ctx context.Context, template client.Object) (bool, error) {
listOpts := []client.ListOption{
client.InNamespace(template.GetNamespace()),
client.MatchingFields{v1alpha1.TemplateChainSupportedTemplatesIndexKey: template.GetName()},
client.Limit(1),
}
if v.templateChainKind == v1alpha1.ClusterTemplateChainKind {
chainList := &v1alpha1.ClusterTemplateChainList{}
if err := v.Client.List(ctx, chainList, listOpts...); err != nil {
return false, err
}
if len(chainList.Items) > 0 {
return true, nil
}
}
if v.templateChainKind == v1alpha1.ServiceTemplateChainKind {
chainList := &v1alpha1.ServiceTemplateChainList{}
if err := v.Client.List(ctx, chainList, listOpts...); err != nil {
return false, err
}
if len(chainList.Items) > 0 {
return true, nil
}
}
return false, nil
}

func templateManagedByHMC(template client.Object) bool {
return template.GetLabels()[v1alpha1.HMCManagedLabelKey] == v1alpha1.HMCManagedLabelValue
}
Loading

0 comments on commit d18beb2

Please sign in to comment.