Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit db30535

Browse files
committed
Add validation webhook
1 parent dd2e83e commit db30535

21 files changed

+820
-113
lines changed

PROJECT

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ resources:
1717
kind: ScalewayCluster
1818
path: github.com/Tomy2e/cluster-api-provider-scaleway/api/v1beta1
1919
version: v1beta1
20+
webhooks:
21+
validation: true
22+
webhookVersion: v1
2023
- api:
2124
crdVersion: v1
2225
namespaced: true
@@ -26,6 +29,9 @@ resources:
2629
kind: ScalewayMachine
2730
path: github.com/Tomy2e/cluster-api-provider-scaleway/api/v1beta1
2831
version: v1beta1
32+
webhooks:
33+
validation: true
34+
webhookVersion: v1
2935
- api:
3036
crdVersion: v1
3137
namespaced: true

api/v1beta1/scalewaycluster_types.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,21 @@ type ScalewayClusterSpec struct {
1313
// +optional
1414
ControlPlaneEndpoint clusterv1beta1.APIEndpoint `json:"controlPlaneEndpoint"`
1515

16-
// TODO: enforce immutable field(s)
17-
1816
// FailureDomains is a list of failure domains where the control-plane nodes
1917
// and resources (loadbalancer, public gateway, etc.) will be created.
18+
// +optional
2019
FailureDomains []string `json:"failureDomains,omitempty"`
2120

2221
// Region represents the region where the cluster will be hosted.
2322
Region string `json:"region"`
2423

2524
// Network contains network related options for the cluster.
2625
// +optional
27-
Network NetworkSpec `json:"network"`
26+
Network *NetworkSpec `json:"network,omitempty"`
2827

2928
// ControlPlaneLoadBalancer contains loadbalancer options.
3029
// +optional
31-
ControlPlaneLoadBalancer *LoadBalancerSpec `json:"controlPlaneLoadBalancer"`
30+
ControlPlaneLoadBalancer *LoadBalancerSpec `json:"controlPlaneLoadBalancer,omitempty"`
3231

3332
// Name of the secret that contains the Scaleway client parameters.
3433
// The following keys must be set: accessKey, secretKey, projectID.
@@ -85,6 +84,8 @@ type PublicGatewaySpec struct {
8584
Type *string `json:"type,omitempty"`
8685

8786
// IP to use when creating a Public Gateway.
87+
// +kubebuilder:validation:Format=ipv4
88+
// +optional
8889
IP *string `json:"ip,omitempty"`
8990

9091
// Zone where to create the Public Gateway. Must be in the same region as the
@@ -106,6 +107,8 @@ type LoadBalancerSpec struct {
106107
Type *string `json:"type,omitempty"`
107108

108109
// IP to use when creating a loadbalancer.
110+
// +kubebuilder:validation:Format=ipv4
111+
// +optional
109112
IP *string `json:"ip,omitempty"`
110113
}
111114

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package v1beta1
2+
3+
import (
4+
"reflect"
5+
6+
"github.com/scaleway/scaleway-sdk-go/scw"
7+
apierrors "k8s.io/apimachinery/pkg/api/errors"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
"k8s.io/apimachinery/pkg/util/validation/field"
11+
ctrl "sigs.k8s.io/controller-runtime"
12+
logf "sigs.k8s.io/controller-runtime/pkg/log"
13+
"sigs.k8s.io/controller-runtime/pkg/webhook"
14+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
15+
)
16+
17+
// log is for logging in this package.
18+
var scalewayclusterlog = logf.Log.WithName("scalewaycluster-resource")
19+
20+
func (r *ScalewayCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
21+
return ctrl.NewWebhookManagedBy(mgr).
22+
For(r).
23+
Complete()
24+
}
25+
26+
//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-scalewaycluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=scalewayclusters,verbs=create;update,versions=v1beta1,name=vscalewaycluster.kb.io,admissionReviewVersions=v1
27+
28+
var _ webhook.Validator = &ScalewayCluster{}
29+
30+
func (r *ScalewayCluster) validate() error {
31+
region, err := r.validateRegion()
32+
if err != nil {
33+
return apierrors.NewInvalid(schema.GroupKind{Group: GroupVersion.Group, Kind: "ScalewayCluster"}, r.Name, field.ErrorList{err})
34+
35+
}
36+
37+
var allErrs field.ErrorList
38+
39+
if err := r.validateFailureDomains(region); err != nil {
40+
allErrs = append(allErrs, err)
41+
}
42+
43+
if err := r.validateLoadBalancerSpec(region); err != nil {
44+
allErrs = append(allErrs, err)
45+
}
46+
47+
if err := r.validateNetworkSpec(region); err != nil {
48+
allErrs = append(allErrs, err)
49+
}
50+
51+
if allErrs == nil {
52+
return nil
53+
}
54+
55+
return apierrors.NewInvalid(schema.GroupKind{Group: GroupVersion.Group, Kind: "ScalewayCluster"}, r.Name, allErrs)
56+
}
57+
58+
func (r *ScalewayCluster) validateRegion() (scw.Region, *field.Error) {
59+
region, err := scw.ParseRegion(r.Spec.Region)
60+
if err != nil {
61+
return scw.Region(""), field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, err.Error())
62+
}
63+
64+
return region, nil
65+
}
66+
67+
func (r *ScalewayCluster) validateFailureDomains(region scw.Region) *field.Error {
68+
if len(r.Spec.FailureDomains) == 0 {
69+
return nil
70+
}
71+
72+
// If set, FailureDomains must:
73+
// - have no duplicates
74+
// - be in the same region as the cluster region
75+
dupeMap := make(map[scw.Zone]struct{})
76+
77+
for i, fd := range r.Spec.FailureDomains {
78+
f := field.NewPath("spec", "failureDomains").Index(i)
79+
zone, err := scw.ParseZone(fd)
80+
if err != nil {
81+
return field.Invalid(f, fd, err.Error())
82+
}
83+
84+
zoneRegion, err := zone.Region()
85+
if err != nil {
86+
return field.Invalid(f, fd, err.Error())
87+
}
88+
89+
if region != zoneRegion {
90+
return field.Invalid(f, fd, "failureDomain must be in the cluster region")
91+
}
92+
93+
if _, ok := dupeMap[zone]; ok {
94+
return field.Duplicate(f, fd)
95+
}
96+
97+
dupeMap[zone] = struct{}{}
98+
}
99+
100+
return nil
101+
}
102+
103+
func (r *ScalewayCluster) validateLoadBalancerSpec(region scw.Region) *field.Error {
104+
if r.Spec.ControlPlaneLoadBalancer == nil || r.Spec.ControlPlaneLoadBalancer.Zone == nil {
105+
return nil
106+
}
107+
108+
// Zone:
109+
// - must be valid
110+
// - in the same region as the cluster region
111+
f := field.NewPath("spec", "controlPlaneLoadBalancer", "zone")
112+
zone, err := scw.ParseZone(*r.Spec.ControlPlaneLoadBalancer.Zone)
113+
if err != nil {
114+
return field.Invalid(f, *r.Spec.ControlPlaneLoadBalancer.Zone, err.Error())
115+
}
116+
117+
zoneRegion, err := zone.Region()
118+
if err != nil {
119+
return field.Invalid(f, *r.Spec.ControlPlaneLoadBalancer.Zone, err.Error())
120+
}
121+
122+
if zoneRegion != region {
123+
return field.Invalid(f, *r.Spec.ControlPlaneLoadBalancer.Zone, "loadbalancer zone must be in the cluster region")
124+
}
125+
126+
return nil
127+
}
128+
129+
func (r *ScalewayCluster) validateNetworkSpec(region scw.Region) *field.Error {
130+
// Currently, this field is mandatory.
131+
if r.Spec.Network == nil {
132+
return field.Invalid(
133+
field.NewPath("spec", "network"),
134+
false,
135+
"network must be set",
136+
)
137+
}
138+
139+
// Currently, using a PrivateNetwork is mandatory.
140+
if r.Spec.Network.PrivateNetwork == nil || !r.Spec.Network.PrivateNetwork.Enabled {
141+
return field.Invalid(
142+
field.NewPath("spec", "network", "privateNetwork", "enabled"),
143+
false,
144+
"private network must be enabled",
145+
)
146+
}
147+
148+
// Currently, using a PublicGateway is mandatory.
149+
if r.Spec.Network.PublicGateway == nil || !r.Spec.Network.PublicGateway.Enabled {
150+
return field.Invalid(
151+
field.NewPath("spec", "network", "publicGateway", "enabled"),
152+
false,
153+
"public gateway must be enabled",
154+
)
155+
}
156+
157+
if r.Spec.Network.PublicGateway.Zone != nil {
158+
zone, err := scw.ParseZone(*r.Spec.Network.PublicGateway.Zone)
159+
if err != nil {
160+
return field.Invalid(
161+
field.NewPath("spec", "network", "publicGateway", "zone"),
162+
*r.Spec.Network.PublicGateway.Zone,
163+
err.Error(),
164+
)
165+
}
166+
167+
zoneRegion, err := zone.Region()
168+
if err != nil {
169+
return field.Invalid(
170+
field.NewPath("spec", "network", "publicGateway", "zone"),
171+
*r.Spec.Network.PublicGateway.Zone,
172+
err.Error(),
173+
)
174+
}
175+
176+
if region != zoneRegion {
177+
return field.Invalid(
178+
field.NewPath("spec", "network", "publicGateway", "zone"),
179+
*r.Spec.Network.PublicGateway.Zone,
180+
"public gateway must be in the cluster region",
181+
)
182+
}
183+
}
184+
185+
if r.Spec.Network.PublicGateway.ID != nil {
186+
if r.Spec.Network.PublicGateway.Type != nil {
187+
return field.Invalid(
188+
field.NewPath("spec", "network", "publicGateway", "type"),
189+
*r.Spec.Network.PublicGateway.Type,
190+
"type should not be specified because id is set",
191+
)
192+
}
193+
194+
if r.Spec.Network.PublicGateway.IP != nil {
195+
return field.Invalid(
196+
field.NewPath("spec", "network", "publicGateway", "ip"),
197+
*r.Spec.Network.PublicGateway.IP,
198+
"ip should not be specified because id is set",
199+
)
200+
}
201+
202+
if r.Spec.Network.PublicGateway.Zone == nil {
203+
return field.Invalid(
204+
field.NewPath("spec", "network", "publicGateway", "zone"),
205+
*r.Spec.Network.PublicGateway.Zone,
206+
"zone is needed",
207+
)
208+
}
209+
}
210+
211+
return nil
212+
}
213+
214+
func (r *ScalewayCluster) enforceImmutability(old *ScalewayCluster) error {
215+
var allErrs field.ErrorList
216+
217+
if r.Spec.Region != old.Spec.Region {
218+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, "field is immutable"))
219+
}
220+
221+
if r.Spec.ScalewaySecretName != old.Spec.ScalewaySecretName {
222+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "scalewaySecretName"), r.Spec.ScalewaySecretName, "field is immutable"))
223+
}
224+
225+
if !reflect.DeepEqual(r.Spec.Network, old.Spec.Network) {
226+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "network"), r.Spec.Network, "field is immutable"))
227+
}
228+
229+
// TODO: allow updating load balancer type when it's implemented.
230+
if !reflect.DeepEqual(r.Spec.ControlPlaneLoadBalancer, old.Spec.ControlPlaneLoadBalancer) {
231+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer"), r.Spec.ControlPlaneLoadBalancer, "field is immutable"))
232+
}
233+
234+
if allErrs == nil {
235+
return nil
236+
}
237+
238+
return apierrors.NewInvalid(schema.GroupKind{Group: GroupVersion.Group, Kind: "ScalewayCluster"}, r.Name, allErrs)
239+
}
240+
241+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
242+
func (r *ScalewayCluster) ValidateCreate() (admission.Warnings, error) {
243+
scalewayclusterlog.Info("validate create", "name", r.Name)
244+
245+
return nil, r.validate()
246+
}
247+
248+
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
249+
func (r *ScalewayCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
250+
scalewayclusterlog.Info("validate update", "name", r.Name)
251+
252+
if err := r.enforceImmutability(old.(*ScalewayCluster)); err != nil {
253+
return nil, err
254+
}
255+
256+
return nil, r.validate()
257+
}
258+
259+
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
260+
func (r *ScalewayCluster) ValidateDelete() (admission.Warnings, error) {
261+
scalewayclusterlog.Info("validate delete", "name", r.Name)
262+
return nil, nil
263+
}

api/v1beta1/scalewaymachine_types.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const MachineFinalizer = "scalewaymachine.infrastructure.cluster.x-k8s.io"
99

1010
// ScalewayMachineSpec defines the desired state of ScalewayMachine
1111
type ScalewayMachineSpec struct {
12-
// TODO: enforce immutable field(s)
13-
1412
// +optional
1513
ProviderID *string `json:"providerID,omitempty"`
1614

0 commit comments

Comments
 (0)