Skip to content

Commit 68d7199

Browse files
Add webhook for BMCSettings resources
1 parent 5a49de7 commit 68d7199

File tree

7 files changed

+586
-0
lines changed

7 files changed

+586
-0
lines changed

PROJECT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,7 @@ resources:
8787
kind: BMCSettings
8888
path: github.com/ironcore-dev/metal-operator/api/v1alpha1
8989
version: v1alpha1
90+
webhooks:
91+
validation: true
92+
webhookVersion: v1
9093
version: "3"

cmd/manager/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,13 @@ func main() { // nolint: gocyclo
378378
setupLog.Error(err, "unable to create controller", "controller", "BIOSSettings")
379379
os.Exit(1)
380380
}
381+
// nolint:goconst
382+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
383+
if err = webhookmetalv1alpha1.SetupBIOSSettingsWebhookWithManager(mgr); err != nil {
384+
setupLog.Error(err, "unable to create webhook", "webhook", "BIOSSettings")
385+
os.Exit(1)
386+
}
387+
}
381388
if err = (&controller.BMCSettingsReconciler{
382389
Client: mgr.GetClient(),
383390
Scheme: mgr.GetScheme(),
@@ -394,6 +401,14 @@ func main() { // nolint: gocyclo
394401
setupLog.Error(err, "unable to create controller", "controller", "BMCSettings")
395402
os.Exit(1)
396403
}
404+
405+
// nolint:goconst
406+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
407+
if err = webhookmetalv1alpha1.SetupBMCSettingsWebhookWithManager(mgr); err != nil {
408+
setupLog.Error(err, "unable to create webhook", "webhook", "BMCSettings")
409+
os.Exit(1)
410+
}
411+
}
397412
//+kubebuilder:scaffold:builder
398413

399414
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

config/webhook/manifests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ webhooks:
2424
resources:
2525
- biossettings
2626
sideEffects: None
27+
- admissionReviewVersions:
28+
- v1
29+
clientConfig:
30+
service:
31+
name: webhook-service
32+
namespace: system
33+
path: /validate-metal-ironcore-dev-v1alpha1-bmcsettings
34+
failurePolicy: Fail
35+
name: vbmcsettings-v1alpha1.kb.io
36+
rules:
37+
- apiGroups:
38+
- metal.ironcore.dev
39+
apiVersions:
40+
- v1alpha1
41+
operations:
42+
- CREATE
43+
- UPDATE
44+
resources:
45+
- bmcsettings
46+
sideEffects: None
2747
- admissionReviewVersions:
2848
- v1
2949
clientConfig:

dist/chart/templates/webhook/webhooks.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ webhooks:
3131
- v1alpha1
3232
resources:
3333
- biossettings
34+
- name: vbmcsettings-v1alpha1.kb.io
35+
clientConfig:
36+
service:
37+
name: metal-operator-webhook-service
38+
namespace: {{ .Release.Namespace }}
39+
path: /validate-metal-ironcore-dev-v1alpha1-bmcsettings
40+
failurePolicy: Fail
41+
sideEffects: None
42+
admissionReviewVersions:
43+
- v1
44+
rules:
45+
- operations:
46+
- CREATE
47+
- UPDATE
48+
apiGroups:
49+
- metal.ironcore.dev
50+
apiVersions:
51+
- v1alpha1
52+
resources:
53+
- bmcsettings
3454
- name: vendpoint-v1alpha1.kb.io
3555
clientConfig:
3656
service:
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1alpha1
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
"k8s.io/apimachinery/pkg/runtime/schema"
13+
"k8s.io/apimachinery/pkg/util/validation/field"
14+
ctrl "sigs.k8s.io/controller-runtime"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
logf "sigs.k8s.io/controller-runtime/pkg/log"
17+
"sigs.k8s.io/controller-runtime/pkg/webhook"
18+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
19+
20+
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
21+
)
22+
23+
// nolint:unused
24+
// log is for logging in this package.
25+
var bmcsettingslog = logf.Log.WithName("bmcsettings-resource")
26+
27+
// SetupBMCSettingsWebhookWithManager registers the webhook for BMCSettings in the manager.
28+
func SetupBMCSettingsWebhookWithManager(mgr ctrl.Manager) error {
29+
return ctrl.NewWebhookManagedBy(mgr).For(&metalv1alpha1.BMCSettings{}).
30+
WithValidator(&BMCSettingsCustomValidator{Client: mgr.GetClient()}).
31+
Complete()
32+
}
33+
34+
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
35+
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
36+
// +kubebuilder:webhook:path=/validate-metal-ironcore-dev-v1alpha1-bmcsettings,mutating=false,failurePolicy=fail,sideEffects=None,groups=metal.ironcore.dev,resources=bmcsettings,verbs=create;update,versions=v1alpha1,name=vbmcsettings-v1alpha1.kb.io,admissionReviewVersions=v1
37+
38+
// BMCSettingsCustomValidator struct is responsible for validating the BMCSettings resource
39+
// when it is created, updated, or deleted.
40+
//
41+
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
42+
// as this struct is used only for temporary operations and does not need to be deeply copied.
43+
type BMCSettingsCustomValidator struct {
44+
Client client.Client
45+
}
46+
47+
var _ webhook.CustomValidator = &BMCSettingsCustomValidator{}
48+
49+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings.
50+
func (v *BMCSettingsCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
51+
bmcSettings, ok := obj.(*metalv1alpha1.BMCSettings)
52+
if !ok {
53+
return nil, fmt.Errorf("expected a BMCSettings object but got %T", obj)
54+
}
55+
bmcsettingslog.Info("Validation for BMCSettings upon creation", "name", bmcSettings.GetName())
56+
57+
if bmcSettings.Spec.BMCRef == nil && bmcSettings.Spec.ServerRefList == nil {
58+
return nil, apierrors.NewInvalid(
59+
schema.GroupKind{Group: bmcSettings.GroupVersionKind().Group, Kind: bmcSettings.Kind},
60+
bmcSettings.GetName(), field.ErrorList{field.Required(field.NewPath("spec"), "Spec.BMCRef or Spec.ServerRefList is required")})
61+
}
62+
63+
bmcSettingsList := &metalv1alpha1.BMCSettingsList{}
64+
if err := v.Client.List(ctx, bmcSettingsList); err != nil {
65+
return nil, fmt.Errorf("failed to list bmcSettingsList: %w", err)
66+
}
67+
68+
// this make one API call rather than multiple when trying to find duplicates
69+
serversList := &metalv1alpha1.ServerList{}
70+
if err := v.Client.List(ctx, serversList); err != nil {
71+
return nil, fmt.Errorf("failed to list serversList: %w", err)
72+
}
73+
serversMap := make(map[string]*metalv1alpha1.Server, len(serversList.Items))
74+
for _, server := range serversList.Items {
75+
serversMap[server.Name] = &server
76+
}
77+
78+
var bmcSettingsBMCName string
79+
var path string
80+
var bsBMCName string
81+
var err error
82+
// get the intended BMC
83+
if bmcSettings.Spec.BMCRef != nil {
84+
bmcSettingsBMCName = bmcSettings.Spec.BMCRef.Name
85+
path = "Spec.BMCRef"
86+
} else {
87+
bmcSettingsBMCName, err = getBMCNameFromServerRef(serversMap, bmcSettings)
88+
if err != nil {
89+
return nil, err
90+
}
91+
path = "Spec.ServerRefList"
92+
}
93+
94+
bmcsettingslog.Info("TEMP:bmcSettings", "bmcSettings name", bmcSettings.Name, "bmcSettings BMCRef", bmcSettings.Spec.BMCRef, "bmcSettings ServerList", bmcSettings.Spec.ServerRefList)
95+
96+
for _, bs := range bmcSettingsList.Items {
97+
bmcsettingslog.Info("TEMP:bs ", "bs name", bs.Name, "bs BMCRef", bs.Spec.BMCRef, "bs ServerList", bs.Spec.ServerRefList)
98+
if bs.Spec.BMCRef != nil {
99+
bsBMCName = bs.Spec.BMCRef.Name
100+
} else {
101+
bsBMCName, err = getBMCNameFromServerRef(serversMap, &bs)
102+
if err != nil {
103+
bmcsettingslog.Info("Skipping as no referred BMC was found", "BMCSettings", bs.Name, "error", err)
104+
continue
105+
}
106+
}
107+
if bsBMCName == bmcSettingsBMCName {
108+
err = fmt.Errorf("BMC (%v) referred in %v is duplicate of BMC (%v) referred in %v", bmcSettingsBMCName, bmcSettings.Name, bsBMCName, bs.Name)
109+
return nil, apierrors.NewInvalid(
110+
schema.GroupKind{Group: bmcSettings.GroupVersionKind().Group, Kind: bmcSettings.Kind},
111+
bmcSettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec", path), err)})
112+
}
113+
}
114+
return nil, nil
115+
}
116+
117+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings.
118+
func (v *BMCSettingsCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
119+
bmcSettings, ok := newObj.(*metalv1alpha1.BMCSettings)
120+
if !ok {
121+
return nil, fmt.Errorf("expected a BMCSettings object for the newObj but got %T", newObj)
122+
}
123+
bmcsettingslog.Info("Validation for BMCSettings upon update", "name", bmcSettings.GetName())
124+
125+
if bmcSettings.Spec.BMCRef == nil && bmcSettings.Spec.ServerRefList == nil {
126+
return nil, apierrors.NewInvalid(
127+
schema.GroupKind{Group: bmcSettings.GroupVersionKind().Group, Kind: bmcSettings.Kind},
128+
bmcSettings.GetName(), field.ErrorList{field.Required(field.NewPath("spec"), "Spec.BMCRef or Spec.ServerRefList is required")})
129+
}
130+
131+
bmcSettingsList := &metalv1alpha1.BMCSettingsList{}
132+
if err := v.Client.List(ctx, bmcSettingsList); err != nil {
133+
return nil, fmt.Errorf("failed to list bmcSettingsList: %w", err)
134+
}
135+
136+
// this make one API call rather than multiple when trying to find duplicates
137+
serversList := &metalv1alpha1.ServerList{}
138+
if err := v.Client.List(ctx, serversList); err != nil {
139+
return nil, fmt.Errorf("failed to list serversList: %w", err)
140+
}
141+
serversMap := make(map[string]*metalv1alpha1.Server, len(serversList.Items))
142+
for _, server := range serversList.Items {
143+
serversMap[server.Name] = &server
144+
}
145+
146+
var bmcSettingsBMCName string
147+
var path string
148+
var bsBMCName string
149+
var err error
150+
151+
// get the intended BMC
152+
if bmcSettings.Spec.BMCRef != nil {
153+
bmcSettingsBMCName = bmcSettings.Spec.BMCRef.Name
154+
path = "Spec.BMCRef"
155+
} else {
156+
bmcSettingsBMCName, err = getBMCNameFromServerRef(serversMap, bmcSettings)
157+
if err != nil {
158+
return nil, err
159+
}
160+
path = "Spec.ServerRefList"
161+
}
162+
163+
bmcsettingslog.Info("TEMP:bmcSettings", "bmcSettings name", bmcSettings.Name, "bmcSettings BMCRef", bmcSettings.Spec.BMCRef, "bmcSettings ServerList", bmcSettings.Spec.ServerRefList)
164+
165+
for _, bs := range bmcSettingsList.Items {
166+
if bmcSettings.Name == bs.Name {
167+
continue
168+
}
169+
bmcsettingslog.Info("TEMP: bs", "bs name", bs.Name, "bs BMCRef", bs.Spec.BMCRef, "bs ServerList", bs.Spec.ServerRefList)
170+
if bs.Spec.BMCRef != nil {
171+
bsBMCName = bs.Spec.BMCRef.Name
172+
} else {
173+
bsBMCName, err = getBMCNameFromServerRef(serversMap, &bs)
174+
if err != nil {
175+
bmcsettingslog.Info("Skipping as no referred BMC was found", "BMCSettings", bs.Name, "error", err)
176+
continue
177+
}
178+
}
179+
if bsBMCName == bmcSettingsBMCName {
180+
err = fmt.Errorf("BMC (%v) referred in %v is duplicate of BMC (%v) referred in %v", bmcSettingsBMCName, bmcSettings.Name, bsBMCName, bs.Name)
181+
return nil, apierrors.NewInvalid(
182+
schema.GroupKind{Group: bmcSettings.GroupVersionKind().Group, Kind: bmcSettings.Kind},
183+
bmcSettings.GetName(), field.ErrorList{field.Duplicate(field.NewPath("spec", path), err)})
184+
}
185+
}
186+
return nil, nil
187+
}
188+
189+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BMCSettings.
190+
func (v *BMCSettingsCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
191+
bmcsettings, ok := obj.(*metalv1alpha1.BMCSettings)
192+
if !ok {
193+
return nil, fmt.Errorf("expected a BMCSettings object but got %T", obj)
194+
}
195+
bmcsettingslog.Info("Validation for BMCSettings upon deletion", "name", bmcsettings.GetName())
196+
197+
return nil, nil
198+
}
199+
200+
func getBMCNameFromServerRef(serversMap map[string]*metalv1alpha1.Server, bmcSettings *metalv1alpha1.BMCSettings) (string, error) {
201+
for _, serverRef := range bmcSettings.Spec.ServerRefList {
202+
bmcsettingslog.Info("TEMP: Validation ", "serverRef", serverRef, "serversMap[serverRef.Name]", serversMap)
203+
if server, ok := serversMap[serverRef.Name]; ok && server != nil && server.Spec.BMCRef != nil {
204+
return server.Spec.BMCRef.Name, nil
205+
}
206+
}
207+
return "", fmt.Errorf("no servers found with reference to BMC in given 'ServerRefList' %v", bmcSettings.Spec.ServerRefList)
208+
}

0 commit comments

Comments
 (0)