Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion api/core/v1alpha1/managedcontrolplane_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"

admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
apierrors "k8s.io/apimachinery/pkg/util/errors"
ctrl "sigs.k8s.io/controller-runtime"
Expand All @@ -19,10 +21,34 @@ var managedcontrolplanelog = logf.Log.WithName("managedcontrolplane-resource")
func (r *ManagedControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
WithDefaulter(r).
WithValidator(r).
Complete()
}

// +kubebuilder:webhook:path=/mutate-core-openmcp-cloud-v1alpha1-managedcontrolplane,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=managedcontrolplanes,verbs=create;update,versions=v1alpha1,name=vmanagedcontrolplane.kb.io,admissionReviewVersions=v1

var _ webhook.CustomDefaulter = &ManagedControlPlane{}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
func (r *ManagedControlPlane) Default(ctx context.Context, obj runtime.Object) error {
mcp, ok := obj.(*ManagedControlPlane)
if !ok {
return fmt.Errorf("object not supported")
}

managedcontrolplanelog.Info("default", "name", mcp.Name)

req, err := admission.RequestFromContext(ctx)
if err != nil {
return err
}

setCreatedBy(mcp, req)

return nil
}

// +kubebuilder:webhook:path=/validate-core-openmcp-cloud-v1alpha1-managedcontrolplane,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=managedcontrolplanes,verbs=delete,versions=v1alpha1,name=vmanagedcontrolplane.kb.io,admissionReviewVersions=v1

var _ webhook.CustomValidator = &ManagedControlPlane{}
Expand All @@ -49,7 +75,9 @@ func (r *ManagedControlPlane) ValidateUpdate(_ context.Context, old runtime.Obje
var errorList []error

// Add update validators here when needed
var updateValidators []func(*ManagedControlPlane, *ManagedControlPlane) error
updateValidators := []func(*ManagedControlPlane, *ManagedControlPlane) error{
validateCreatedByUnchanged,
}

for _, validator := range updateValidators {
if err := validator(newMcp, oldMcp); err != nil {
Expand All @@ -75,3 +103,43 @@ func (r *ManagedControlPlane) ValidateDelete(_ context.Context, obj runtime.Obje
}
return nil, fmt.Errorf("ManagedControlPlane %q requires annotation %q to be set to true, before it can be deleted", r.Name, ManagedControlPlaneDeletionConfirmationAnnotation)
}

// errCreatedByImmutable is the error that is returned when the value of the resource creator annotation has been changed by the user.
var errCreatedByImmutable = fmt.Errorf("annotation %s is immutable", CreatedByAnnotation)

// compareStringMapValue compares the value of string values identified by a key in two maps.
// Returns "true" if the value is the same.
func compareStringMapValue(a, b map[string]string, key string) bool {
return a[key] == b[key]
}

// validateCreatedByUnchanged checks if the value of the annotation that contains the name of the resource creator has been changed.
// Returns an error if the value has been changed or "nil" if it's the same.
func validateCreatedByUnchanged(old, new *ManagedControlPlane) error {
if compareStringMapValue(old.GetAnnotations(), new.GetAnnotations(), CreatedByAnnotation) {
return nil
}

return errCreatedByImmutable
}

// setCreatedBy sets an annotation that contains the name of the user who created the resource.
// The value is only set when the "Operation" is "Create".
func setCreatedBy(obj metav1.Object, req admission.Request) {
if req.Operation != admissionv1.Create {
return
}

setMetaDataAnnotation(obj, CreatedByAnnotation, req.UserInfo.Username)
}

// setMetaDataAnnotation sets the annotation on the given object.
// If the given Object did not yet have annotations, they are initialized.
func setMetaDataAnnotation(meta metav1.Object, key, value string) {
labels := meta.GetAnnotations()
if labels == nil {
labels = make(map[string]string)
}
labels[key] = value
meta.SetAnnotations(labels)
}
48 changes: 47 additions & 1 deletion api/core/v1alpha1/managedcontrolplane_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package v1alpha1
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admission/v1"
authv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

var _ = Describe("ManagedControlPlane Webhook", func() {
Expand All @@ -27,7 +30,7 @@ var _ = Describe("ManagedControlPlane Webhook", func() {
Expect(apierrors.IsForbidden(err)).Should(BeTrue())
})

It("Should admit the deletion if the annoation was set", func() {
It("Should admit the deletion if the annotation was set", func() {
var err error

namespace := string(uuid.NewUUID())
Expand Down Expand Up @@ -124,6 +127,49 @@ var _ = Describe("ManagedControlPlane Webhook", func() {
Expect(err).To(HaveOccurred())
})

It("Should deny to update annotations:openmcp.cloud/created-by", func() {
var err error

namespace := string(uuid.NewUUID())
err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})
Expect(err).ShouldNot(HaveOccurred())

mcp := &ManagedControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "mcp",
Namespace: namespace,
},
}
err = k8sClient.Create(ctx, mcp)
Expect(err).ShouldNot(HaveOccurred())

request := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
UserInfo: authv1.UserInfo{
Username: "[email protected]",
},
},
}

setCreatedBy(mcp, request)

Expect(mcp.Annotations).Should(Equal(map[string]string{CreatedByAnnotation: "[email protected]"}))

// doesn't set the CreatedBy annotation if operation is NOT create
request = admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Operation: admissionv1.Update,
UserInfo: authv1.UserInfo{
Username: "[email protected]",
},
},
}

setCreatedBy(mcp, request)

Expect(mcp.Annotations).Should(Equal(map[string]string{CreatedByAnnotation: "[email protected]"}))
})
})

})
2 changes: 1 addition & 1 deletion charts/mcp-operator/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ rules:
- apiGroups: ["admissionregistration.k8s.io"]
resources:
- validatingwebhookconfigurations
# - mutatingwebhookconfigurations for now we are only using validatingwebhooks
- mutatingwebhookconfigurations
verbs: ["*"]
{{- end }}
{{- if not (and .Values.crds .Values.crds.disabled) }}
Expand Down