Skip to content

Commit 0d01205

Browse files
authored
Merge pull request #5596 from killianmuldoon/pr-move-webhook-implementation
🌱 Move Cluster and ClusterClass webhook implementation to top level package
2 parents 30b8f68 + d4576e5 commit 0d01205

File tree

4 files changed

+2234
-79
lines changed

4 files changed

+2234
-79
lines changed

webhooks/cluster.go

Lines changed: 176 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,26 @@ package webhooks
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

24+
"github.com/blang/semver"
2325
apierrors "k8s.io/apimachinery/pkg/api/errors"
2426
"k8s.io/apimachinery/pkg/runtime"
27+
"k8s.io/apimachinery/pkg/util/sets"
28+
"k8s.io/apimachinery/pkg/util/validation/field"
2529
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
30+
"sigs.k8s.io/cluster-api/feature"
31+
"sigs.k8s.io/cluster-api/util/version"
2632
ctrl "sigs.k8s.io/controller-runtime"
2733
"sigs.k8s.io/controller-runtime/pkg/webhook"
2834
)
2935

3036
// SetupWebhookWithManager sets up Cluster webhooks.
31-
func (c *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
37+
func (webhook *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
3238
return ctrl.NewWebhookManagedBy(mgr).
3339
For(&clusterv1.Cluster{}).
34-
WithDefaulter(c).
35-
WithValidator(c).
40+
WithDefaulter(webhook).
41+
WithValidator(webhook).
3642
Complete()
3743
}
3844

@@ -46,60 +52,197 @@ var _ webhook.CustomDefaulter = &Cluster{}
4652
var _ webhook.CustomValidator = &Cluster{}
4753

4854
// Default satisfies the defaulting webhook interface.
49-
func (c *Cluster) Default(_ context.Context, obj runtime.Object) error {
55+
func (webhook *Cluster) Default(_ context.Context, obj runtime.Object) error {
5056
cluster, ok := obj.(*clusterv1.Cluster)
5157
if !ok {
5258
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
5359
}
5460

55-
// Note: The code in Default is intentionally not duplicated to avoid that we accidentally
56-
// implement new checks in the API package and forget to duplicate them to the webhook package.
57-
// The idea is to add new defaulting which requires a reader in the webhook package.
58-
// When we drop the method in the API package we must inline it here.
59-
cluster.Default()
61+
if cluster.Spec.InfrastructureRef != nil && len(cluster.Spec.InfrastructureRef.Namespace) == 0 {
62+
cluster.Spec.InfrastructureRef.Namespace = cluster.Namespace
63+
}
64+
65+
if cluster.Spec.ControlPlaneRef != nil && len(cluster.Spec.ControlPlaneRef.Namespace) == 0 {
66+
cluster.Spec.ControlPlaneRef.Namespace = cluster.Namespace
67+
}
6068

69+
// If the Cluster uses a managed topology
70+
if cluster.Spec.Topology != nil {
71+
// tolerate version strings without a "v" prefix: prepend it if it's not there
72+
if !strings.HasPrefix(cluster.Spec.Topology.Version, "v") {
73+
cluster.Spec.Topology.Version = "v" + cluster.Spec.Topology.Version
74+
}
75+
}
6176
return nil
6277
}
6378

64-
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
65-
func (c *Cluster) ValidateCreate(_ context.Context, obj runtime.Object) error {
79+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
80+
func (webhook *Cluster) ValidateCreate(_ context.Context, obj runtime.Object) error {
6681
cluster, ok := obj.(*clusterv1.Cluster)
6782
if !ok {
6883
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
6984
}
70-
71-
// Note: The code in ValidateCreate is intentionally not duplicated to avoid that we accidentally
72-
// implement new checks in the API package and forget to duplicate them to the webhook package.
73-
// The idea is to add new validation which requires a reader and Cluster variable/patch validation
74-
// in the webhook package.
75-
// When we drop the method in the API package we must inline it here.
76-
return cluster.ValidateCreate()
85+
return webhook.validate(nil, cluster)
7786
}
7887

79-
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
80-
func (c *Cluster) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) error {
81-
cluster, ok := newObj.(*clusterv1.Cluster)
88+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type.
89+
func (webhook *Cluster) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) error {
90+
newCluster, ok := newObj.(*clusterv1.Cluster)
8291
if !ok {
8392
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", newObj))
8493
}
8594
oldCluster, ok := oldObj.(*clusterv1.Cluster)
8695
if !ok {
8796
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", oldObj))
8897
}
98+
return webhook.validate(oldCluster, newCluster)
99+
}
89100

90-
// Note: The code in ValidateUpdate is intentionally not duplicated to avoid that we accidentally
91-
// implement new checks in the API package and forget to duplicate them to the webhook package.
92-
// The idea is to add new validation which requires a reader and Cluster variable/patch validation
93-
// in the webhook package.
94-
// When we drop the method in the API package we must inline it here.
95-
return cluster.ValidateUpdate(oldCluster)
101+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
102+
func (webhook *Cluster) ValidateDelete(_ context.Context, obj runtime.Object) error {
103+
return nil
96104
}
97105

98-
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
99-
func (c *Cluster) ValidateDelete(ctx context.Context, obj runtime.Object) error {
100-
cluster, ok := obj.(*clusterv1.Cluster)
101-
if !ok {
102-
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
106+
func (webhook *Cluster) validate(old, new *clusterv1.Cluster) error {
107+
var allErrs field.ErrorList
108+
if new.Spec.InfrastructureRef != nil && new.Spec.InfrastructureRef.Namespace != new.Namespace {
109+
allErrs = append(
110+
allErrs,
111+
field.Invalid(
112+
field.NewPath("spec", "infrastructureRef", "namespace"),
113+
new.Spec.InfrastructureRef.Namespace,
114+
"must match metadata.namespace",
115+
),
116+
)
117+
}
118+
119+
if new.Spec.ControlPlaneRef != nil && new.Spec.ControlPlaneRef.Namespace != new.Namespace {
120+
allErrs = append(
121+
allErrs,
122+
field.Invalid(
123+
field.NewPath("spec", "controlPlaneRef", "namespace"),
124+
new.Spec.ControlPlaneRef.Namespace,
125+
"must match metadata.namespace",
126+
),
127+
)
128+
}
129+
130+
// Validate the managed topology, if defined.
131+
if new.Spec.Topology != nil {
132+
if topologyErrs := webhook.validateTopology(old, new); len(topologyErrs) > 0 {
133+
allErrs = append(allErrs, topologyErrs...)
134+
}
103135
}
104-
return cluster.ValidateDelete()
136+
137+
if len(allErrs) == 0 {
138+
return nil
139+
}
140+
return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Cluster").GroupKind(), new.Name, allErrs)
141+
}
142+
143+
func (webhook *Cluster) validateTopology(old, new *clusterv1.Cluster) field.ErrorList {
144+
// NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
145+
// must prevent the usage of Cluster.Topology in case the feature flag is disabled.
146+
if !feature.Gates.Enabled(feature.ClusterTopology) {
147+
return field.ErrorList{
148+
field.Forbidden(
149+
field.NewPath("spec", "topology"),
150+
"can be set only if the ClusterTopology feature flag is enabled",
151+
),
152+
}
153+
}
154+
155+
var allErrs field.ErrorList
156+
157+
// class should be defined.
158+
if len(new.Spec.Topology.Class) == 0 {
159+
allErrs = append(
160+
allErrs,
161+
field.Invalid(
162+
field.NewPath("spec", "topology", "class"),
163+
new.Spec.Topology.Class,
164+
"cannot be empty",
165+
),
166+
)
167+
}
168+
169+
// version should be valid.
170+
if !version.KubeSemver.MatchString(new.Spec.Topology.Version) {
171+
allErrs = append(
172+
allErrs,
173+
field.Invalid(
174+
field.NewPath("spec", "topology", "version"),
175+
new.Spec.Topology.Version,
176+
"must be a valid semantic version",
177+
),
178+
)
179+
}
180+
181+
// MachineDeployment names must be unique.
182+
if new.Spec.Topology.Workers != nil {
183+
names := sets.String{}
184+
for _, md := range new.Spec.Topology.Workers.MachineDeployments {
185+
if names.Has(md.Name) {
186+
allErrs = append(allErrs,
187+
field.Invalid(
188+
field.NewPath("spec", "topology", "workers", "machineDeployments"),
189+
md,
190+
fmt.Sprintf("MachineDeployment names should be unique. MachineDeployment with name %q is defined more than once.", md.Name),
191+
),
192+
)
193+
}
194+
names.Insert(md.Name)
195+
}
196+
}
197+
198+
if old != nil { // On update
199+
// Class could not be mutated.
200+
if new.Spec.Topology.Class != old.Spec.Topology.Class {
201+
allErrs = append(
202+
allErrs,
203+
field.Invalid(
204+
field.NewPath("spec", "topology", "class"),
205+
new.Spec.Topology.Class,
206+
"class cannot be changed",
207+
),
208+
)
209+
}
210+
211+
// Version could only be increased.
212+
inVersion, err := semver.ParseTolerant(new.Spec.Topology.Version)
213+
if err != nil {
214+
allErrs = append(
215+
allErrs,
216+
field.Invalid(
217+
field.NewPath("spec", "topology", "version"),
218+
new.Spec.Topology.Version,
219+
"is not a valid version",
220+
),
221+
)
222+
}
223+
oldVersion, err := semver.ParseTolerant(old.Spec.Topology.Version)
224+
if err != nil {
225+
// NOTE: this should never happen. Nevertheless, handling this for extra caution.
226+
allErrs = append(
227+
allErrs,
228+
field.Invalid(
229+
field.NewPath("spec", "topology", "version"),
230+
new.Spec.Topology.Class,
231+
"cannot be compared with the old version",
232+
),
233+
)
234+
}
235+
if inVersion.NE(semver.Version{}) && oldVersion.NE(semver.Version{}) && version.Compare(inVersion, oldVersion, version.WithBuildTags()) == -1 {
236+
allErrs = append(
237+
allErrs,
238+
field.Invalid(
239+
field.NewPath("spec", "topology", "version"),
240+
new.Spec.Topology.Version,
241+
"cannot be decreased",
242+
),
243+
)
244+
}
245+
}
246+
247+
return allErrs
105248
}

0 commit comments

Comments
 (0)