sigs.k8s.io/cluster-api@v1.7.1/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 "k8s.io/utils/ptr" 27 ctrl "sigs.k8s.io/controller-runtime" 28 29 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 30 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 31 "sigs.k8s.io/cluster-api/internal/webhooks/util" 32 ) 33 34 var ctx = ctrl.SetupSignalHandler() 35 36 func TestMachinePoolDefault(t *testing.T) { 37 g := NewWithT(t) 38 39 mp := &expv1.MachinePool{ 40 ObjectMeta: metav1.ObjectMeta{ 41 Namespace: "foobar", 42 }, 43 Spec: expv1.MachinePoolSpec{ 44 Template: clusterv1.MachineTemplateSpec{ 45 Spec: clusterv1.MachineSpec{ 46 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 47 Version: ptr.To("1.20.0"), 48 }, 49 }, 50 }, 51 } 52 webhook := &MachinePool{} 53 t.Run("for MachinePool", util.CustomDefaultValidateTest(ctx, mp, webhook)) 54 g.Expect(webhook.Default(ctx, mp)).To(Succeed()) 55 56 g.Expect(mp.Labels[clusterv1.ClusterNameLabel]).To(Equal(mp.Spec.ClusterName)) 57 g.Expect(mp.Spec.Replicas).To(Equal(ptr.To[int32](1))) 58 g.Expect(mp.Spec.MinReadySeconds).To(Equal(ptr.To[int32](0))) 59 g.Expect(mp.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace).To(Equal(mp.Namespace)) 60 g.Expect(mp.Spec.Template.Spec.InfrastructureRef.Namespace).To(Equal(mp.Namespace)) 61 g.Expect(mp.Spec.Template.Spec.Version).To(Equal(ptr.To("v1.20.0"))) 62 } 63 64 func TestMachinePoolBootstrapValidation(t *testing.T) { 65 tests := []struct { 66 name string 67 bootstrap clusterv1.Bootstrap 68 expectErr bool 69 }{ 70 { 71 name: "should return error if configref and data are nil", 72 bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: nil}, 73 expectErr: true, 74 }, 75 { 76 name: "should not return error if dataSecretName is set", 77 bootstrap: clusterv1.Bootstrap{ConfigRef: nil, DataSecretName: ptr.To("test")}, 78 expectErr: false, 79 }, 80 { 81 name: "should not return error if config ref is set", 82 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}, DataSecretName: nil}, 83 expectErr: false, 84 }, 85 } 86 87 for _, tt := range tests { 88 t.Run(tt.name, func(t *testing.T) { 89 g := NewWithT(t) 90 webhook := &MachinePool{} 91 mp := &expv1.MachinePool{ 92 Spec: expv1.MachinePoolSpec{ 93 Template: clusterv1.MachineTemplateSpec{ 94 Spec: clusterv1.MachineSpec{ 95 Bootstrap: tt.bootstrap, 96 }, 97 }, 98 }, 99 } 100 101 if tt.expectErr { 102 warnings, err := webhook.ValidateCreate(ctx, mp) 103 g.Expect(err).To(HaveOccurred()) 104 g.Expect(warnings).To(BeEmpty()) 105 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 106 g.Expect(err).To(HaveOccurred()) 107 g.Expect(warnings).To(BeEmpty()) 108 } else { 109 warnings, err := webhook.ValidateCreate(ctx, mp) 110 g.Expect(err).ToNot(HaveOccurred()) 111 g.Expect(warnings).To(BeEmpty()) 112 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 113 g.Expect(err).ToNot(HaveOccurred()) 114 g.Expect(warnings).To(BeEmpty()) 115 } 116 }) 117 } 118 } 119 120 func TestMachinePoolNamespaceValidation(t *testing.T) { 121 tests := []struct { 122 name string 123 expectErr bool 124 bootstrap clusterv1.Bootstrap 125 infraRef corev1.ObjectReference 126 namespace string 127 }{ 128 { 129 name: "should succeed if all namespaces match", 130 expectErr: false, 131 namespace: "foobar", 132 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}}, 133 infraRef: corev1.ObjectReference{Namespace: "foobar"}, 134 }, 135 { 136 name: "should return error if namespace and bootstrap namespace don't match", 137 expectErr: true, 138 namespace: "foobar", 139 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar123"}}, 140 infraRef: corev1.ObjectReference{Namespace: "foobar"}, 141 }, 142 { 143 name: "should return error if namespace and infrastructure ref namespace don't match", 144 expectErr: true, 145 namespace: "foobar", 146 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar"}}, 147 infraRef: corev1.ObjectReference{Namespace: "foobar123"}, 148 }, 149 { 150 name: "should return error if no namespaces match", 151 expectErr: true, 152 namespace: "foobar1", 153 bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{Namespace: "foobar2"}}, 154 infraRef: corev1.ObjectReference{Namespace: "foobar3"}, 155 }, 156 } 157 158 for _, tt := range tests { 159 t.Run(tt.name, func(t *testing.T) { 160 g := NewWithT(t) 161 162 webhook := &MachinePool{} 163 mp := &expv1.MachinePool{ 164 ObjectMeta: metav1.ObjectMeta{Namespace: tt.namespace}, 165 Spec: expv1.MachinePoolSpec{ 166 Template: clusterv1.MachineTemplateSpec{ 167 Spec: clusterv1.MachineSpec{ 168 Bootstrap: tt.bootstrap, 169 InfrastructureRef: tt.infraRef, 170 }, 171 }, 172 }, 173 } 174 175 if tt.expectErr { 176 warnings, err := webhook.ValidateCreate(ctx, mp) 177 g.Expect(err).To(HaveOccurred()) 178 g.Expect(warnings).To(BeEmpty()) 179 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 180 g.Expect(err).To(HaveOccurred()) 181 g.Expect(warnings).To(BeEmpty()) 182 } else { 183 warnings, err := webhook.ValidateCreate(ctx, mp) 184 g.Expect(err).ToNot(HaveOccurred()) 185 g.Expect(warnings).To(BeEmpty()) 186 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 187 g.Expect(err).ToNot(HaveOccurred()) 188 g.Expect(warnings).To(BeEmpty()) 189 } 190 }) 191 } 192 } 193 194 func TestMachinePoolClusterNameImmutable(t *testing.T) { 195 tests := []struct { 196 name string 197 oldClusterName string 198 newClusterName string 199 expectErr bool 200 }{ 201 { 202 name: "when the cluster name has not changed", 203 oldClusterName: "foo", 204 newClusterName: "foo", 205 expectErr: false, 206 }, 207 { 208 name: "when the cluster name has changed", 209 oldClusterName: "foo", 210 newClusterName: "bar", 211 expectErr: true, 212 }, 213 } 214 215 for _, tt := range tests { 216 t.Run(tt.name, func(t *testing.T) { 217 g := NewWithT(t) 218 219 newMP := &expv1.MachinePool{ 220 Spec: expv1.MachinePoolSpec{ 221 ClusterName: tt.newClusterName, 222 Template: clusterv1.MachineTemplateSpec{ 223 Spec: clusterv1.MachineSpec{ 224 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 225 }, 226 }, 227 }, 228 } 229 230 oldMP := &expv1.MachinePool{ 231 Spec: expv1.MachinePoolSpec{ 232 ClusterName: tt.oldClusterName, 233 Template: clusterv1.MachineTemplateSpec{ 234 Spec: clusterv1.MachineSpec{ 235 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 236 }, 237 }, 238 }, 239 } 240 241 webhook := MachinePool{} 242 warnings, err := webhook.ValidateUpdate(ctx, oldMP, newMP) 243 if tt.expectErr { 244 g.Expect(err).To(HaveOccurred()) 245 } else { 246 g.Expect(err).ToNot(HaveOccurred()) 247 } 248 g.Expect(warnings).To(BeEmpty()) 249 }) 250 } 251 } 252 253 func TestMachinePoolVersionValidation(t *testing.T) { 254 tests := []struct { 255 name string 256 expectErr bool 257 version string 258 }{ 259 { 260 name: "should succeed version is a valid kube semver", 261 expectErr: false, 262 version: "v1.23.3", 263 }, 264 { 265 name: "should succeed version is a valid pre-release", 266 expectErr: false, 267 version: "v1.19.0-alpha.1", 268 }, 269 { 270 name: "should fail if version is not a valid semver", 271 expectErr: true, 272 version: "v1.1", 273 }, 274 { 275 name: "should fail if version is missing a v prefix", 276 expectErr: true, 277 version: "1.20.0", 278 }, 279 } 280 281 for i := range tests { 282 tt := tests[i] 283 t.Run(tt.name, func(t *testing.T) { 284 g := NewWithT(t) 285 286 mp := &expv1.MachinePool{ 287 Spec: expv1.MachinePoolSpec{ 288 Template: clusterv1.MachineTemplateSpec{ 289 Spec: clusterv1.MachineSpec{ 290 Bootstrap: clusterv1.Bootstrap{ConfigRef: &corev1.ObjectReference{}}, 291 Version: &tt.version, 292 }, 293 }, 294 }, 295 } 296 webhook := &MachinePool{} 297 298 if tt.expectErr { 299 warnings, err := webhook.ValidateCreate(ctx, mp) 300 g.Expect(err).To(HaveOccurred()) 301 g.Expect(warnings).To(BeEmpty()) 302 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 303 g.Expect(err).To(HaveOccurred()) 304 g.Expect(warnings).To(BeEmpty()) 305 } else { 306 warnings, err := webhook.ValidateCreate(ctx, mp) 307 g.Expect(err).ToNot(HaveOccurred()) 308 g.Expect(warnings).To(BeEmpty()) 309 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 310 g.Expect(err).ToNot(HaveOccurred()) 311 g.Expect(warnings).To(BeEmpty()) 312 } 313 }) 314 } 315 } 316 317 func TestMachinePoolMetadataValidation(t *testing.T) { 318 tests := []struct { 319 name string 320 labels map[string]string 321 annotations map[string]string 322 expectErr bool 323 }{ 324 { 325 name: "should return error for invalid labels and annotations", 326 labels: map[string]string{ 327 "foo": "$invalid-key", 328 "bar": strings.Repeat("a", 64) + "too-long-value", 329 "/invalid-key": "foo", 330 }, 331 annotations: map[string]string{ 332 "/invalid-key": "foo", 333 }, 334 expectErr: true, 335 }, 336 } 337 338 for _, tt := range tests { 339 t.Run(tt.name, func(t *testing.T) { 340 g := NewWithT(t) 341 mp := &expv1.MachinePool{ 342 Spec: expv1.MachinePoolSpec{ 343 Template: clusterv1.MachineTemplateSpec{ 344 ObjectMeta: clusterv1.ObjectMeta{ 345 Labels: tt.labels, 346 Annotations: tt.annotations, 347 }, 348 }, 349 }, 350 } 351 webhook := &MachinePool{} 352 if tt.expectErr { 353 warnings, err := webhook.ValidateCreate(ctx, mp) 354 g.Expect(err).To(HaveOccurred()) 355 g.Expect(warnings).To(BeEmpty()) 356 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 357 g.Expect(err).To(HaveOccurred()) 358 g.Expect(warnings).To(BeEmpty()) 359 } else { 360 warnings, err := webhook.ValidateCreate(ctx, mp) 361 g.Expect(err).ToNot(HaveOccurred()) 362 g.Expect(warnings).To(BeEmpty()) 363 warnings, err = webhook.ValidateUpdate(ctx, mp, mp) 364 g.Expect(err).ToNot(HaveOccurred()) 365 g.Expect(warnings).To(BeEmpty()) 366 } 367 }) 368 } 369 }