@@ -17,9 +17,16 @@ limitations under the License.
17
17
package v1alpha4
18
18
19
19
import (
20
+ "fmt"
21
+ "strings"
22
+
23
+ "github.com/blang/semver"
20
24
apierrors "k8s.io/apimachinery/pkg/api/errors"
21
25
"k8s.io/apimachinery/pkg/runtime"
26
+ "k8s.io/apimachinery/pkg/util/sets"
22
27
"k8s.io/apimachinery/pkg/util/validation/field"
28
+ "sigs.k8s.io/cluster-api/feature"
29
+ "sigs.k8s.io/cluster-api/util/version"
23
30
ctrl "sigs.k8s.io/controller-runtime"
24
31
"sigs.k8s.io/controller-runtime/pkg/webhook"
25
32
)
@@ -45,24 +52,36 @@ func (c *Cluster) Default() {
45
52
if c .Spec .ControlPlaneRef != nil && len (c .Spec .ControlPlaneRef .Namespace ) == 0 {
46
53
c .Spec .ControlPlaneRef .Namespace = c .Namespace
47
54
}
55
+
56
+ // If the Cluster uses a managed topology
57
+ if c .Spec .Topology != nil {
58
+ // tolerate version strings without a "v" prefix: prepend it if it's not there
59
+ if ! strings .HasPrefix (c .Spec .Topology .Version , "v" ) {
60
+ c .Spec .Topology .Version = "v" + c .Spec .Topology .Version
61
+ }
62
+ }
48
63
}
49
64
50
65
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
51
66
func (c * Cluster ) ValidateCreate () error {
52
- return c .validate ()
67
+ return c .validate (nil )
53
68
}
54
69
55
70
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
56
71
func (c * Cluster ) ValidateUpdate (old runtime.Object ) error {
57
- return c .validate ()
72
+ oldCluster , ok := old .(* Cluster )
73
+ if ! ok {
74
+ return apierrors .NewBadRequest (fmt .Sprintf ("expected a Cluster but got a %T" , old ))
75
+ }
76
+ return c .validate (oldCluster )
58
77
}
59
78
60
79
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
61
80
func (c * Cluster ) ValidateDelete () error {
62
81
return nil
63
82
}
64
83
65
- func (c * Cluster ) validate () error {
84
+ func (c * Cluster ) validate (old * Cluster ) error {
66
85
var allErrs field.ErrorList
67
86
if c .Spec .InfrastructureRef != nil && c .Spec .InfrastructureRef .Namespace != c .Namespace {
68
87
allErrs = append (
@@ -86,8 +105,145 @@ func (c *Cluster) validate() error {
86
105
)
87
106
}
88
107
108
+ // Validate the managed topology, if defined.
109
+ if c .Spec .Topology != nil {
110
+ if topologyErrs := c .validateTopology (old ); len (topologyErrs ) > 0 {
111
+ allErrs = append (allErrs , topologyErrs ... )
112
+ }
113
+ }
114
+
89
115
if len (allErrs ) == 0 {
90
116
return nil
91
117
}
92
118
return apierrors .NewInvalid (GroupVersion .WithKind ("Cluster" ).GroupKind (), c .Name , allErrs )
93
119
}
120
+
121
+ func (c * Cluster ) validateTopology (old * Cluster ) field.ErrorList {
122
+ // NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
123
+ // must prevent the usage of Cluster.Topology in case the feature flag is disabled.
124
+ if ! feature .Gates .Enabled (feature .ClusterTopology ) {
125
+ return field.ErrorList {
126
+ field .Forbidden (
127
+ field .NewPath ("spec" , "topology" ),
128
+ "can be set only if the ClusterTopology feature flag is enabled" ,
129
+ ),
130
+ }
131
+ }
132
+
133
+ var allErrs field.ErrorList
134
+
135
+ // class should be defined.
136
+ if len (c .Spec .Topology .Class ) == 0 {
137
+ allErrs = append (
138
+ allErrs ,
139
+ field .Invalid (
140
+ field .NewPath ("spec" , "topology" , "class" ),
141
+ c .Spec .Topology .Class ,
142
+ "cannot be empty" ,
143
+ ),
144
+ )
145
+ }
146
+
147
+ // version should be valid.
148
+ if ! version .KubeSemver .MatchString (c .Spec .Topology .Version ) {
149
+ allErrs = append (
150
+ allErrs ,
151
+ field .Invalid (
152
+ field .NewPath ("spec" , "topology" , "version" ),
153
+ c .Spec .Topology .Version ,
154
+ "must be a valid semantic version" ,
155
+ ),
156
+ )
157
+ }
158
+
159
+ // MachineDeployment names must be unique.
160
+ if c .Spec .Topology .Workers != nil {
161
+ names := sets.String {}
162
+ for _ , md := range c .Spec .Topology .Workers .MachineDeployments {
163
+ if names .Has (md .Name ) {
164
+ allErrs = append (allErrs ,
165
+ field .Invalid (
166
+ field .NewPath ("spec" , "topology" , "workers" , "machineDeployments" ),
167
+ md ,
168
+ fmt .Sprintf ("MachineDeployment names should be unique. MachineDeployment with name %q is defined more than once." , md .Name ),
169
+ ),
170
+ )
171
+ }
172
+ names .Insert (md .Name )
173
+ }
174
+ }
175
+
176
+ switch old {
177
+ case nil : // On create
178
+ // c.Spec.InfrastructureRef and c.Spec.ControlPlaneRef could not be set
179
+ if c .Spec .InfrastructureRef != nil {
180
+ allErrs = append (
181
+ allErrs ,
182
+ field .Invalid (
183
+ field .NewPath ("spec" , "infrastructureRef" ),
184
+ c .Spec .InfrastructureRef ,
185
+ "cannot be set when a Topology is defined" ,
186
+ ),
187
+ )
188
+ }
189
+ if c .Spec .ControlPlaneRef != nil {
190
+ allErrs = append (
191
+ allErrs ,
192
+ field .Invalid (
193
+ field .NewPath ("spec" , "controlPlaneRef" ),
194
+ c .Spec .ControlPlaneRef ,
195
+ "cannot be set when a Topology is defined" ,
196
+ ),
197
+ )
198
+ }
199
+ default : // On update
200
+ // Class could not be mutated.
201
+ if c .Spec .Topology .Class != old .Spec .Topology .Class {
202
+ allErrs = append (
203
+ allErrs ,
204
+ field .Invalid (
205
+ field .NewPath ("spec" , "topology" , "class" ),
206
+ c .Spec .Topology .Class ,
207
+ "class cannot be changed" ,
208
+ ),
209
+ )
210
+ }
211
+
212
+ // Version could only be increased.
213
+ inVersion , err := semver .ParseTolerant (c .Spec .Topology .Version )
214
+ if err != nil {
215
+ allErrs = append (
216
+ allErrs ,
217
+ field .Invalid (
218
+ field .NewPath ("spec" , "topology" , "version" ),
219
+ c .Spec .Topology .Version ,
220
+ "is not a valid version" ,
221
+ ),
222
+ )
223
+ }
224
+ oldVersion , err := semver .ParseTolerant (old .Spec .Topology .Version )
225
+ if err != nil {
226
+ // NOTE: this should never happen. Nevertheless, handling this for extra caution.
227
+ allErrs = append (
228
+ allErrs ,
229
+ field .Invalid (
230
+ field .NewPath ("spec" , "topology" , "version" ),
231
+ c .Spec .Topology .Class ,
232
+ "cannot be compared with the old version" ,
233
+ ),
234
+ )
235
+ }
236
+ if inVersion .NE (semver.Version {}) && oldVersion .NE (semver.Version {}) && ! inVersion .GTE (oldVersion ) {
237
+ allErrs = append (
238
+ allErrs ,
239
+ field .Invalid (
240
+ field .NewPath ("spec" , "topology" , "version" ),
241
+ c .Spec .Topology .Version ,
242
+ "cannot be decreased" ,
243
+ ),
244
+ )
245
+ }
246
+ }
247
+
248
+ return allErrs
249
+ }
0 commit comments