sigs.k8s.io/cluster-api@v1.6.3/exp/internal/webhooks/machinepool_test.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package webhooks 18 19 import ( 20 "strings" 21 "testing" 22 23 . "github.com/onsi/gomega" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 utilfeature "k8s.io/component-base/featuregate/testing" 27 "k8s.io/utils/pointer" 28 ctrl "sigs.k8s.io/controller-runtime" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 32 "sigs.k8s.io/cluster-api/feature" 33 "sigs.k8s.io/cluster-api/internal/webhooks/util" 34 ) 35 36 var ctx = ctrl.SetupSignalHandler() 37 38 func TestMachinePoolDefault(t *testing.T) { 39 // NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool. 40 // Enabling the feature flag temporarily for this test. 41 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)() 42 43 g := NewWithT(t) 44 45 mp := &expv1.MachinePool{ 46 ObjectMeta: metav1.ObjectMeta{ 47 Namespace: "foobar", 48 }, 49 Spec: expv1.MachinePoolSpec{ 50 Template: clusterv1.MachineTemplateSpec{ 51 Spec: clusterv1.MachineSpec{ 52 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 53 Version: pointer.String("1.20.0"), 54 }, 55 }, 56 }, 57 } 58 webhook := &MachinePool{} 59 t.Run("for MachinePool", util.CustomDefaultValidateTest(ctx, mp, webhook)) 60 g.Expect(webhook.Default(ctx, mp)).To(Succeed()) 61 62 g.Expect(mp.Labels[clusterv1.ClusterNameLabel]).To(Equal(mp.Spec.ClusterName)) 63 g.Expect(mp.Spec.Replicas).To(Equal(pointer.Int32(1))) 64 g.Expect(mp.Spec.MinReadySeconds).To(Equal(pointer.Int32(0))) 65 g.Expect(mp.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace).To(Equal(mp.Namespace)) 66 g.Expect(mp.Spec.Template.Spec.InfrastructureRef.Namespace).To(Equal(mp.Namespace)) 67 g.Expect(mp.Spec.Template.Spec.Version).To(Equal(pointer.String("v1.20.0"))) 68 } 69 70 func TestMachinePoolBootstrapValidation(t *testing.T) { 71 // NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool. 72 // Enabling the feature flag temporarily for this test. 73 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)() 74 tests := []struct { 75 name string 76 bootstrap clusterv1.Bootstrap 77 expectErr bool 78 }{ 79 { 80 name: "should return error if configref and data are nil", 81 bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: nil}, 82 expectErr: true, 83 }, 84 { 85 name: "should not return error if dataSecretName is set", 86 bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: pointer.String("test")}, 87 expectErr: false, 88 }, 89 { 90 name: "should not return error if config ref is set", 91 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}, DataSecretName: nil}, 92 expectErr: false, 93 }, 94 } 95 96 for _, tt := range tests { 97 t.Run(tt.name, func(t *testing.T) { 98 g := NewWithT(t) 99 webhook := &MachinePool{} 100 mp := &expv1.MachinePool{ 101 Spec: expv1.MachinePoolSpec{ 102 Template: clusterv1.MachineTemplateSpec{ 103 Spec: clusterv1.MachineSpec{ 104 Bootstrap: tt.bootstrap, 105 }, 106 }, 107 }, 108 } 109 110 if tt.expectErr { 111 warnings, err := webhook.ValidateCreate(ctx, mp) 112 g.Expect(err).To(HaveOccurred()) 113 g.Expect(warnings).To(BeEmpty()) 114 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 115 g.Expect(err).To(HaveOccurred()) 116 g.Expect(warnings).To(BeEmpty()) 117 } else { 118 warnings, err := webhook.ValidateCreate(ctx, mp) 119 g.Expect(err).ToNot(HaveOccurred()) 120 g.Expect(warnings).To(BeEmpty()) 121 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 122 g.Expect(err).ToNot(HaveOccurred()) 123 g.Expect(warnings).To(BeEmpty()) 124 } 125 }) 126 } 127 } 128 129 func TestMachinePoolNamespaceValidation(t *testing.T) { 130 // NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool. 131 // Enabling the feature flag temporarily for this test. 132 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)() 133 tests := []struct { 134 name string 135 expectErr bool 136 bootstrap clusterv1.Bootstrap 137 infraRef corev1.ObjectReference 138 namespace string 139 }{ 140 { 141 name: "should succeed if all namespaces match", 142 expectErr: false, 143 namespace: "foobar", 144 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}}, 145 infraRef: corev1.ObjectReference{Namespace: "foobar"}, 146 }, 147 { 148 name: "should return error if namespace and bootstrap namespace don't match", 149 expectErr: true, 150 namespace: "foobar", 151 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar123"}}, 152 infraRef: corev1.ObjectReference{Namespace: "foobar"}, 153 }, 154 { 155 name: "should return error if namespace and infrastructure ref namespace don't match", 156 expectErr: true, 157 namespace: "foobar", 158 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}}, 159 infraRef: corev1.ObjectReference{Namespace: "foobar123"}, 160 }, 161 { 162 name: "should return error if no namespaces match", 163 expectErr: true, 164 namespace: "foobar1", 165 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar2"}}, 166 infraRef: corev1.ObjectReference{Namespace: "foobar3"}, 167 }, 168 } 169 170 for _, tt := range tests { 171 t.Run(tt.name, func(t *testing.T) { 172 g := NewWithT(t) 173 174 webhook := &MachinePool{} 175 mp := &expv1.MachinePool{ 176 ObjectMeta: metav1.ObjectMeta{Namespace: tt.namespace}, 177 Spec: expv1.MachinePoolSpec{ 178 Template: clusterv1.MachineTemplateSpec{ 179 Spec: clusterv1.MachineSpec{ 180 Bootstrap: tt.bootstrap, 181 InfrastructureRef: tt.infraRef, 182 }, 183 }, 184 }, 185 } 186 187 if tt.expectErr { 188 warnings, err := webhook.ValidateCreate(ctx, mp) 189 g.Expect(err).To(HaveOccurred()) 190 g.Expect(warnings).To(BeEmpty()) 191 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 192 g.Expect(err).To(HaveOccurred()) 193 g.Expect(warnings).To(BeEmpty()) 194 } else { 195 warnings, err := webhook.ValidateCreate(ctx, mp) 196 g.Expect(err).ToNot(HaveOccurred()) 197 g.Expect(warnings).To(BeEmpty()) 198 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 199 g.Expect(err).ToNot(HaveOccurred()) 200 g.Expect(warnings).To(BeEmpty()) 201 } 202 }) 203 } 204 } 205 206 func TestMachinePoolClusterNameImmutable(t *testing.T) { 207 // NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool. 208 // Enabling the feature flag temporarily for this test. 209 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)() 210 tests := []struct { 211 name string 212 oldClusterName string 213 newClusterName string 214 expectErr bool 215 }{ 216 { 217 name: "when the cluster name has not changed", 218 oldClusterName: "foo", 219 newClusterName: "foo", 220 expectErr: false, 221 }, 222 { 223 name: "when the cluster name has changed", 224 oldClusterName: "foo", 225 newClusterName: "bar", 226 expectErr: true, 227 }, 228 } 229 230 for _, tt := range tests { 231 t.Run(tt.name, func(t *testing.T) { 232 g := NewWithT(t) 233 234 newMP := &expv1.MachinePool{ 235 Spec: expv1.MachinePoolSpec{ 236 ClusterName: tt.newClusterName, 237 Template: clusterv1.MachineTemplateSpec{ 238 Spec: clusterv1.MachineSpec{ 239 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 240 }, 241 }, 242 }, 243 } 244 245 oldMP := &expv1.MachinePool{ 246 Spec: expv1.MachinePoolSpec{ 247 ClusterName: tt.oldClusterName, 248 Template: clusterv1.MachineTemplateSpec{ 249 Spec: clusterv1.MachineSpec{ 250 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 251 }, 252 }, 253 }, 254 } 255 256 webhook := MachinePool{} 257 warnings, err := webhook.ValidateUpdate(ctx, oldMP, newMP) 258 if tt.expectErr { 259 g.Expect(err).To(HaveOccurred()) 260 } else { 261 g.Expect(err).ToNot(HaveOccurred()) 262 } 263 g.Expect(warnings).To(BeEmpty()) 264 }) 265 } 266 } 267 268 func TestMachinePoolVersionValidation(t *testing.T) { 269 // NOTE: MachinePool feature flag is disabled by default, thus preventing to create or update MachinePool. 270 // Enabling the feature flag temporarily for this test. 271 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)() 272 tests := []struct { 273 name string 274 expectErr bool 275 version string 276 }{ 277 { 278 name: "should succeed version is a valid kube semver", 279 expectErr: false, 280 version: "v1.23.3", 281 }, 282 { 283 name: "should succeed version is a valid pre-release", 284 expectErr: false, 285 version: "v1.19.0-alpha.1", 286 }, 287 { 288 name: "should fail if version is not a valid semver", 289 expectErr: true, 290 version: "v1.1", 291 }, 292 { 293 name: "should fail if version is missing a v prefix", 294 expectErr: true, 295 version: "1.20.0", 296 }, 297 } 298 299 for i := range tests { 300 tt := tests[i] 301 t.Run(tt.name, func(t *testing.T) { 302 g := NewWithT(t) 303 304 mp := &expv1.MachinePool{ 305 Spec: expv1.MachinePoolSpec{ 306 Template: clusterv1.MachineTemplateSpec{ 307 Spec: clusterv1.MachineSpec{ 308 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 309 Version: &tt.version, 310 }, 311 }, 312 }, 313 } 314 webhook := &MachinePool{} 315 316 if tt.expectErr { 317 warnings, err := webhook.ValidateCreate(ctx, mp) 318 g.Expect(err).To(HaveOccurred()) 319 g.Expect(warnings).To(BeEmpty()) 320 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 321 g.Expect(err).To(HaveOccurred()) 322 g.Expect(warnings).To(BeEmpty()) 323 } else { 324 warnings, err := webhook.ValidateCreate(ctx, mp) 325 g.Expect(err).ToNot(HaveOccurred()) 326 g.Expect(warnings).To(BeEmpty()) 327 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 328 g.Expect(err).ToNot(HaveOccurred()) 329 g.Expect(warnings).To(BeEmpty()) 330 } 331 }) 332 } 333 } 334 335 func TestMachinePoolMetadataValidation(t *testing.T) { 336 tests := []struct { 337 name string 338 labels map[string]string 339 annotations map[string]string 340 expectErr bool 341 }{ 342 { 343 name: "should return error for invalid labels and annotations", 344 labels: map[string]string{ 345 "foo": "$invalid-key", 346 "bar": strings.Repeat("a", 64) + "too-long-value", 347 "/invalid-key": "foo", 348 }, 349 annotations: map[string]string{ 350 "/invalid-key": "foo", 351 }, 352 expectErr: true, 353 }, 354 } 355 356 for _, tt := range tests { 357 t.Run(tt.name, func(t *testing.T) { 358 g := NewWithT(t) 359 mp := &expv1.MachinePool{ 360 Spec: expv1.MachinePoolSpec{ 361 Template: clusterv1.MachineTemplateSpec{ 362 ObjectMeta: clusterv1.ObjectMeta{ 363 Labels: tt.labels, 364 Annotations: tt.annotations, 365 }, 366 }, 367 }, 368 } 369 webhook := &MachinePool{} 370 if tt.expectErr { 371 warnings, err := webhook.ValidateCreate(ctx, mp) 372 g.Expect(err).To(HaveOccurred()) 373 g.Expect(warnings).To(BeEmpty()) 374 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 375 g.Expect(err).To(HaveOccurred()) 376 g.Expect(warnings).To(BeEmpty()) 377 } else { 378 warnings, err := webhook.ValidateCreate(ctx, mp) 379 g.Expect(err).ToNot(HaveOccurred()) 380 g.Expect(warnings).To(BeEmpty()) 381 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 382 g.Expect(err).ToNot(HaveOccurred()) 383 g.Expect(warnings).To(BeEmpty()) 384 } 385 }) 386 } 387 }