@@ -19,20 +19,26 @@ package webhooks
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "strings"
22
23
24
+ "github.com/blang/semver"
23
25
apierrors "k8s.io/apimachinery/pkg/api/errors"
24
26
"k8s.io/apimachinery/pkg/runtime"
27
+ "k8s.io/apimachinery/pkg/util/sets"
28
+ "k8s.io/apimachinery/pkg/util/validation/field"
25
29
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
30
+ "sigs.k8s.io/cluster-api/feature"
31
+ "sigs.k8s.io/cluster-api/util/version"
26
32
ctrl "sigs.k8s.io/controller-runtime"
27
33
"sigs.k8s.io/controller-runtime/pkg/webhook"
28
34
)
29
35
30
36
// SetupWebhookWithManager sets up Cluster webhooks.
31
- func (c * Cluster ) SetupWebhookWithManager (mgr ctrl.Manager ) error {
37
+ func (webhook * Cluster ) SetupWebhookWithManager (mgr ctrl.Manager ) error {
32
38
return ctrl .NewWebhookManagedBy (mgr ).
33
39
For (& clusterv1.Cluster {}).
34
- WithDefaulter (c ).
35
- WithValidator (c ).
40
+ WithDefaulter (webhook ).
41
+ WithValidator (webhook ).
36
42
Complete ()
37
43
}
38
44
@@ -46,60 +52,197 @@ var _ webhook.CustomDefaulter = &Cluster{}
46
52
var _ webhook.CustomValidator = & Cluster {}
47
53
48
54
// 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 {
50
56
cluster , ok := obj .(* clusterv1.Cluster )
51
57
if ! ok {
52
58
return apierrors .NewBadRequest (fmt .Sprintf ("expected a Cluster but got a %T" , obj ))
53
59
}
54
60
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
+ }
60
68
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
+ }
61
76
return nil
62
77
}
63
78
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 {
66
81
cluster , ok := obj .(* clusterv1.Cluster )
67
82
if ! ok {
68
83
return apierrors .NewBadRequest (fmt .Sprintf ("expected a Cluster but got a %T" , obj ))
69
84
}
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 )
77
86
}
78
87
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 )
82
91
if ! ok {
83
92
return apierrors .NewBadRequest (fmt .Sprintf ("expected a Cluster but got a %T" , newObj ))
84
93
}
85
94
oldCluster , ok := oldObj .(* clusterv1.Cluster )
86
95
if ! ok {
87
96
return apierrors .NewBadRequest (fmt .Sprintf ("expected a Cluster but got a %T" , oldObj ))
88
97
}
98
+ return webhook .validate (oldCluster , newCluster )
99
+ }
89
100
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
96
104
}
97
105
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
+ }
103
135
}
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
105
248
}
0 commit comments