diff --git a/cmd/main.go b/cmd/main.go index 3a8bf5b38..33e4abf12 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -612,6 +612,14 @@ func main() { // nolint: gocyclo os.Exit(1) } } + + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err := webhookv1alpha1.SetupServerMaintenanceWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "Failed to create webhook", "webhook", "ServerMaintenance") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 3187b2ccc..99a8436b0 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -148,3 +148,23 @@ webhooks: resources: - servers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-metal-ironcore-dev-v1alpha1-servermaintenance + failurePolicy: Fail + name: vservermaintenance-v1alpha1.kb.io + rules: + - apiGroups: + - metal.ironcore.dev + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - servermaintenances + sideEffects: None diff --git a/dist/chart/templates/webhook/webhooks.yaml b/dist/chart/templates/webhook/webhooks.yaml index d75c1501c..f647fd09f 100644 --- a/dist/chart/templates/webhook/webhooks.yaml +++ b/dist/chart/templates/webhook/webhooks.yaml @@ -155,4 +155,24 @@ webhooks: - v1alpha1 resources: - servers + - name: vservermaintenance-v1alpha1.kb.io + clientConfig: + service: + name: metal-operator-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-metal-ironcore-dev-v1alpha1-servermaintenance + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - metal.ironcore.dev + apiVersions: + - v1alpha1 + resources: + - servermaintenances {{- end }} diff --git a/internal/webhook/v1alpha1/servermaintenance_webhook.go b/internal/webhook/v1alpha1/servermaintenance_webhook.go new file mode 100644 index 000000000..d2268481d --- /dev/null +++ b/internal/webhook/v1alpha1/servermaintenance_webhook.go @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" +) + +// ------------------------- +// Setup webhook registration +// ------------------------- + +func SetupServerMaintenanceWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr, &metalv1alpha1.ServerMaintenance{}). + WithValidator(&ServerMaintenanceValidator{Client: mgr.GetClient()}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-servermaintenance,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=servermaintenances,verbs=create;update,versions=v1alpha1,name=vservermaintenance-v1alpha1.kb.io,admissionReviewVersions=v1 + +// ------------------------- +// Validator struct +// ------------------------- + +type ServerMaintenanceValidator struct { + Client client.Client +} + +// ------------------------- +// CREATE validation +// ------------------------- + +func (v *ServerMaintenanceValidator) ValidateCreate( + ctx context.Context, + obj *metalv1alpha1.ServerMaintenance, +) (admission.Warnings, error) { + + // 1. ServerRef must exist + if obj.Spec.ServerRef == nil || obj.Spec.ServerRef.Name == "" { + return nil, fmt.Errorf("serverRef.name must be set") + } + + // 2. Check that referenced Server exists + server := &metalv1alpha1.Server{} + + err := v.Client.Get(ctx, client.ObjectKey{ + Name: obj.Spec.ServerRef.Name, + }, server) + + + if err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("referenced Server %q does not exist", obj.Spec.ServerRef.Name) + } + return nil, err + } + + return nil, nil +} + +// ------------------------- +// UPDATE validation +// ------------------------- + +func (v *ServerMaintenanceValidator) ValidateUpdate( + ctx context.Context, + oldObj, newObj *metalv1alpha1.ServerMaintenance, +) (admission.Warnings, error) { + + // reuse create logic + return v.ValidateCreate(ctx, newObj) +} + +// ------------------------- +// DELETE validation +// ------------------------- + +func (v *ServerMaintenanceValidator) ValidateDelete( + ctx context.Context, + obj *metalv1alpha1.ServerMaintenance, +) (admission.Warnings, error) { + + // usually allowed + return nil, nil +} diff --git a/internal/webhook/v1alpha1/servermaintenance_webhook_test.go b/internal/webhook/v1alpha1/servermaintenance_webhook_test.go new file mode 100644 index 000000000..52cc5be84 --- /dev/null +++ b/internal/webhook/v1alpha1/servermaintenance_webhook_test.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" +) + +var _ = Describe("ServerMaintenance Webhook", func() { + + var ( + ctx context.Context + server *metalv1alpha1.Server + maint *metalv1alpha1.ServerMaintenance + validator ServerMaintenanceValidator + ) + + BeforeEach(func() { + ctx = context.Background() + + // create a valid Server first + server = &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Spec: metalv1alpha1.ServerSpec{ + SystemUUID: "1234", + }, + } + Expect(k8sClient.Create(ctx, server)).To(Succeed()) + + validator = ServerMaintenanceValidator{Client: k8sClient} + komega.SetClient(k8sClient) + + maint = &metalv1alpha1.ServerMaintenance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-maintenance", + Namespace: "default", + }, + Spec: metalv1alpha1.ServerMaintenanceSpec{ + ServerRef: &v1.LocalObjectReference{ + Name: "test-server", + }, + }, + } + }) + + AfterEach(func(ctx context.Context) { + _ = k8sClient.DeleteAllOf(ctx, &metalv1alpha1.Server{}) + _ = k8sClient.DeleteAllOf(ctx, &metalv1alpha1.ServerMaintenance{}) + }) + + Context("ValidateCreate", func() { + + It("should allow creation when Server exists", func() { + _, err := validator.ValidateCreate(ctx, maint) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should reject creation when Server does NOT exist", func() { + maint.Spec.ServerRef.Name = "non-existent-server" + + _, err := validator.ValidateCreate(ctx, maint) + Expect(err).To(HaveOccurred()) + }) + + It("should reject creation when ServerRef is empty", func() { + maint.Spec.ServerRef = &v1.LocalObjectReference{ + Name: "", + } + + _, err := validator.ValidateCreate(ctx, maint) + Expect(err).To(HaveOccurred()) + }) + + It("should reject creation when ServerRef is nil", func() { + maint.Spec.ServerRef = nil + + _, err := validator.ValidateCreate(ctx, maint) + Expect(err).To(HaveOccurred()) + }) + }) +})