Skip to content

Commit

Permalink
Allow Templates deletion by the controller only
Browse files Browse the repository at this point in the history
In case the TemplateManagement object exists on the environment
the admission controller will block the removal of any Template
managed by HMC.
  • Loading branch information
eromanova committed Sep 23, 2024
1 parent 07398dc commit f27b9ce
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 57 deletions.
38 changes: 30 additions & 8 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admissionregistration/v1"
authenticationv1 "k8s.io/api/authentication/v1"
utilyaml "sigs.k8s.io/cluster-api/util/yaml"
ctrl "sigs.k8s.io/controller-runtime"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
Expand All @@ -55,13 +57,19 @@ import (
const (
mutatingWebhookKind = "MutatingWebhookConfiguration"
validatingWebhookKind = "ValidatingWebhookConfiguration"

hmcServiceAccountName = "hmc-controller-manager"
)

var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var ctx context.Context
var cancel context.CancelFunc
var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
ctx context.Context
cancel context.CancelFunc

userInfo = authenticationv1.UserInfo{Username: fmt.Sprintf("system:serviceaccount:hmc-system:%s", hmcServiceAccountName)}
)

func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
Expand All @@ -81,6 +89,9 @@ var _ = BeforeSuite(func() {
)
Expect(err).NotTo(HaveOccurred())

err = os.Setenv(hmcwebhook.ServiceAccountEnvName, hmcServiceAccountName)
Expect(err).To(Succeed())

testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "templates", "provider", "hmc", "templates", "crds"),
Expand Down Expand Up @@ -147,13 +158,21 @@ var _ = BeforeSuite(func() {
err = (&hmcwebhook.TemplateManagementValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr)
injectUserInfo := func(req *admission.Request) {
req.UserInfo = userInfo
}

templateValidator := hmcwebhook.TemplateValidator{
InjectUserInfo: injectUserInfo,
}

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

err = (&hmcwebhook.ServiceTemplateValidator{}).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 All @@ -179,6 +198,9 @@ var _ = AfterSuite(func() {
cancel()
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())

err = os.Unsetenv(hmcwebhook.ServiceAccountEnvName)
Expect(err).To(Succeed())
})

func loadWebhooks(path string) ([]*admissionv1.ValidatingWebhookConfiguration, []*admissionv1.MutatingWebhookConfiguration, error) {
Expand Down
26 changes: 6 additions & 20 deletions internal/controller/template_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (

hmc "github.com/Mirantis/hmc/api/v1alpha1"
"github.com/Mirantis/hmc/internal/helm"
"github.com/Mirantis/hmc/internal/templateutil"
)

const (
Expand Down Expand Up @@ -124,14 +125,7 @@ func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Req
return r.ReconcileTemplate(ctx, providerTemplate)
}

// Template is the interface defining a list of methods to interact with templates
type Template interface {
client.Object
GetSpec() *hmc.TemplateSpecMixin
GetStatus() *hmc.TemplateStatusMixin
}

func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Template) (ctrl.Result, error) {
func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template templateutil.Template) (ctrl.Result, error) {
l := log.FromContext(ctx)

spec := template.GetSpec()
Expand All @@ -150,7 +144,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem
l.Error(err, "invalid helm chart reference")
return ctrl.Result{}, err
}
if template.GetNamespace() == r.SystemNamespace || !templateManagedByHMC(template) {
if template.GetNamespace() == r.SystemNamespace || !templateutil.IsManagedByHMC(template) {
err := r.reconcileDefaultHelmRepository(ctx, template.GetNamespace())
if err != nil {
l.Error(err, "Failed to reconcile default HelmRepository", "namespace", template.GetNamespace())
Expand Down Expand Up @@ -223,15 +217,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem
return ctrl.Result{}, r.updateStatus(ctx, template, "")
}

func templateManagedByHMC(template Template) bool {
labels := template.GetLabels()
if labels == nil {
return false
}
return labels[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue
}

func (r *TemplateReconciler) parseChartMetadata(template Template, chart *chart.Chart) error {
func (r *TemplateReconciler) parseChartMetadata(template templateutil.Template, chart *chart.Chart) error {
if chart.Metadata == nil {
return fmt.Errorf("chart metadata is empty")
}
Expand Down Expand Up @@ -266,7 +252,7 @@ func (r *TemplateReconciler) parseChartMetadata(template Template, chart *chart.
return nil
}

func (r *TemplateReconciler) updateStatus(ctx context.Context, template Template, validationError string) error {
func (r *TemplateReconciler) updateStatus(ctx context.Context, template templateutil.Template, validationError string) error {
status := template.GetStatus()
status.ObservedGeneration = template.GetGeneration()
status.ValidationError = validationError
Expand Down Expand Up @@ -317,7 +303,7 @@ func (r *TemplateReconciler) reconcileDefaultHelmRepository(ctx context.Context,
return nil
}

func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Template) (*sourcev1.HelmChart, error) {
func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template templateutil.Template) (*sourcev1.HelmChart, error) {
spec := template.GetSpec()
namespace := template.GetNamespace()
if namespace == "" {
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 @@ -170,7 +170,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())
})
It("should successfully reconcile the resource", func() {
Expand Down
36 changes: 36 additions & 0 deletions internal/templateutil/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 templateutil

import (
"sigs.k8s.io/controller-runtime/pkg/client"

hmc "github.com/Mirantis/hmc/api/v1alpha1"
)

// Template is the interface defining a list of methods to interact with templates
type Template interface {
client.Object
GetSpec() *hmc.TemplateSpecMixin
GetStatus() *hmc.TemplateStatusMixin
}

func IsManagedByHMC(template Template) bool {
labels := template.GetLabels()
if labels == nil {
return false
}
return labels[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue
}
106 changes: 88 additions & 18 deletions internal/webhook/template_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"os"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/fields"
Expand All @@ -28,22 +29,30 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/Mirantis/hmc/api/v1alpha1"
"github.com/Mirantis/hmc/internal/templateutil"
)

type ClusterTemplateValidator struct {
type TemplateValidator struct {
client.Client

SystemNamespace string
InjectUserInfo func(*admission.Request)
}

type ClusterTemplateValidator struct {
TemplateValidator
}

var (
ErrTemplateDeletionForbidden = errors.New("template deletion is forbidden")
)

func (in *ClusterTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
in.Client = mgr.GetClient()
func (v *ClusterTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ClusterTemplate{}).
WithValidator(in).
WithDefaulter(in).
WithValidator(v).
WithDefaulter(v).
Complete()
}

Expand All @@ -68,13 +77,20 @@ func (v *ClusterTemplateValidator) ValidateDelete(ctx context.Context, obj runti
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ClusterTemplate but got a %T", obj))
}
deletionAllowed, err := v.isTemplateDeletionAllowed(ctx, template)
if err != nil {
return nil, fmt.Errorf("failed to check if the ClusterTemplate %s/%s is allowed to be deleted: %v", template.Namespace, template.Name, err)
}
if !deletionAllowed {
return nil, ErrTemplateDeletionForbidden
}

managedClusters := &v1alpha1.ManagedClusterList{}
listOptions := client.ListOptions{
FieldSelector: fields.SelectorFromSet(fields.Set{v1alpha1.TemplateKey: template.Name}),
Limit: 1,
}
err := v.Client.List(ctx, managedClusters, &listOptions)
err = v.Client.List(ctx, managedClusters, &listOptions)
if err != nil {
return nil, err
}
Expand All @@ -92,15 +108,15 @@ func (*ClusterTemplateValidator) Default(_ context.Context, _ runtime.Object) er
}

type ServiceTemplateValidator struct {
client.Client
TemplateValidator
}

func (in *ServiceTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
in.Client = mgr.GetClient()
func (v *ServiceTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ServiceTemplate{}).
WithValidator(in).
WithDefaulter(in).
WithValidator(v).
WithDefaulter(v).
Complete()
}

Expand All @@ -120,7 +136,18 @@ func (*ServiceTemplateValidator) ValidateUpdate(_ context.Context, _ runtime.Obj
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (*ServiceTemplateValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
func (v *ServiceTemplateValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
template, ok := obj.(*v1alpha1.ServiceTemplate)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ServiceTemplate but got a %T", obj))
}
deletionAllowed, err := v.isTemplateDeletionAllowed(ctx, template)
if err != nil {
return nil, fmt.Errorf("failed to check if the ServiceTemplate %s/%s is allowed to be deleted: %v", template.Namespace, template.Name, err)
}
if !deletionAllowed {
return nil, ErrTemplateDeletionForbidden
}
return nil, nil
}

Expand All @@ -130,15 +157,15 @@ func (*ServiceTemplateValidator) Default(_ context.Context, _ runtime.Object) er
}

type ProviderTemplateValidator struct {
client.Client
TemplateValidator
}

func (in *ProviderTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
in.Client = mgr.GetClient()
func (v *ProviderTemplateValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ProviderTemplate{}).
WithValidator(in).
WithDefaulter(in).
WithValidator(v).
WithDefaulter(v).
Complete()
}

Expand All @@ -158,11 +185,54 @@ func (*ProviderTemplateValidator) ValidateUpdate(_ context.Context, _ runtime.Ob
}

// 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.ServiceTemplate)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ProviderTemplate but got a %T", obj))
}
deletionAllowed, err := v.isTemplateDeletionAllowed(ctx, template)
if err != nil {
return nil, fmt.Errorf("failed to check if the ProviderTemplate %s is allowed to be deleted: %v", template.Name, err)
}
if !deletionAllowed {
return nil, 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) isTemplateDeletionAllowed(ctx context.Context, template templateutil.Template) (bool, error) {
req, err := admission.RequestFromContext(ctx)
if err != nil {
return false, err
}
if v.InjectUserInfo != nil {
v.InjectUserInfo(&req)
}
// Allow all templates' deletion for the HMC controller
if serviceAccountIsEqual(req, os.Getenv(ServiceAccountEnvName)) {
return true, nil
}
// Templates in the system namespace are not allowed to be deleted
if template.GetNamespace() == v.SystemNamespace {
return false, nil
}
tmList := &v1alpha1.TemplateManagementList{}
err = v.List(ctx, tmList)
if err != nil {
return false, err
}
// Allow template deletion if TemplateManagement object is not created
if len(tmList.Items) == 0 {
return true, nil
}
// Allow template deletion if template is not managed by the TemplateManagement
if !templateutil.IsManagedByHMC(template) {
return true, nil
}
return false, nil
}
Loading

0 comments on commit f27b9ce

Please sign in to comment.