-
Notifications
You must be signed in to change notification settings - Fork 16
Add ServerMaintenance validating webhook with tests and manifests #851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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" | ||||||||||||
|
Check failure on line 12 in internal/webhook/v1alpha1/servermaintenance_webhook.go
|
||||||||||||
| "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 == "" { | ||||||||||||
|
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isnt this already done using kubebuilder logic in api definition?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while |
||||||||||||
| 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 | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+57
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if there is a transient error or cache not updated on the newly added server? retry will solve the issue, but its not a pleasant experience. @coderabbitai what is a generic approach k8 uses for handling checking for referenced object before creating the current object patterns?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Core Problem: Cache-Backed Client in WebhooksController-runtime controllers use a cache to subscribe to events from Kubernetes objects and to read those objects more efficiently by avoiding calls to the API. The client exposed via
This is exactly the problem you've identified: if a The Idiomatic Fix: Use
|
||||||||||||
| Pattern | Use Case |
|---|---|
mgr.GetAPIReader() in webhook |
✅ Strongly-consistent cross-resource existence check |
mgr.GetClient() (cache) in webhook |
|
ClientDisableCacheFor option |
For types where you always want uncached reads across controllers + webhooks |
| Retry by operator on failure | ✅ Works but poor UX — better to fix the root cause |
The fix is straightforward: swap mgr.GetClient() for mgr.GetAPIReader() in ServerMaintenanceValidator for the Server existence lookup.
🧠 Learnings used
Learnt from: nagadeesh-nagaraja
Repo: ironcore-dev/metal-operator PR: 761
File: docs/concepts/biosversion.md:19-19
Timestamp: 2026-03-30T08:23:27.355Z
Learning: When a Kubernetes controller (reconciler) errors out and does not proceed if a CRD spec field is nil/empty, that field should not be treated as optional in the API schema. Don’t mark such fields as `+optional`/`omitempty` (or otherwise make them “not required”) unless the webhook/API layer also tolerates nil and the controller can reconcile safely. Otherwise, add/adjust validation in the webhook/API (or the CRD schema) to reject creation/update when the field is missing, or remove the `+optional`/`omitempty` so the requiredness is enforced by the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: these comments could be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: might be better to create a common function and use in both ValidateUpdate and ValidateCreate rather than calling ValidateCreate from Validate update..
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 121
Remove the unused
logfimport (build blocker).Line 12 imports
logf, but there is no symbol usage in this file, which causes Go compilation to fail.Minimal fix
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"📝 Committable suggestion
🤖 Prompt for AI Agents