sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machinehealthcheck_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 "testing" 21 "time" 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/apimachinery/pkg/util/intstr" 27 28 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 29 "sigs.k8s.io/cluster-api/internal/webhooks/util" 30 ) 31 32 func TestMachineHealthCheckDefault(t *testing.T) { 33 g := NewWithT(t) 34 mhc := &clusterv1.MachineHealthCheck{ 35 ObjectMeta: metav1.ObjectMeta{ 36 Namespace: "foo", 37 }, 38 Spec: clusterv1.MachineHealthCheckSpec{ 39 Selector: metav1.LabelSelector{ 40 MatchLabels: map[string]string{"foo": "bar"}, 41 }, 42 RemediationTemplate: &corev1.ObjectReference{}, 43 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 44 { 45 Type: corev1.NodeReady, 46 Status: corev1.ConditionFalse, 47 }, 48 }, 49 }, 50 } 51 webhook := &MachineHealthCheck{} 52 53 t.Run("for MachineHealthCheck", util.CustomDefaultValidateTest(ctx, mhc, webhook)) 54 g.Expect(webhook.Default(ctx, mhc)).To(Succeed()) 55 56 g.Expect(mhc.Labels[clusterv1.ClusterNameLabel]).To(Equal(mhc.Spec.ClusterName)) 57 g.Expect(mhc.Spec.MaxUnhealthy.String()).To(Equal("100%")) 58 g.Expect(mhc.Spec.NodeStartupTimeout).ToNot(BeNil()) 59 g.Expect(*mhc.Spec.NodeStartupTimeout).To(BeComparableTo(metav1.Duration{Duration: 10 * time.Minute})) 60 g.Expect(mhc.Spec.RemediationTemplate.Namespace).To(Equal(mhc.Namespace)) 61 } 62 63 func TestMachineHealthCheckLabelSelectorAsSelectorValidation(t *testing.T) { 64 tests := []struct { 65 name string 66 selectors map[string]string 67 expectErr bool 68 }{ 69 { 70 name: "should not return error for valid selector", 71 selectors: map[string]string{"foo": "bar"}, 72 expectErr: false, 73 }, 74 { 75 name: "should return error for invalid selector", 76 selectors: map[string]string{"-123-foo": "bar"}, 77 expectErr: true, 78 }, 79 } 80 81 for _, tt := range tests { 82 t.Run(tt.name, func(t *testing.T) { 83 g := NewWithT(t) 84 mhc := &clusterv1.MachineHealthCheck{ 85 Spec: clusterv1.MachineHealthCheckSpec{ 86 Selector: metav1.LabelSelector{ 87 MatchLabels: tt.selectors, 88 }, 89 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 90 { 91 Type: corev1.NodeReady, 92 Status: corev1.ConditionFalse, 93 }, 94 }, 95 }, 96 } 97 webhook := &MachineHealthCheck{} 98 99 if tt.expectErr { 100 warnings, err := webhook.ValidateCreate(ctx, mhc) 101 g.Expect(err).To(HaveOccurred()) 102 g.Expect(warnings).To(BeEmpty()) 103 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 104 g.Expect(err).To(HaveOccurred()) 105 g.Expect(warnings).To(BeEmpty()) 106 } else { 107 warnings, err := webhook.ValidateCreate(ctx, mhc) 108 g.Expect(err).ToNot(HaveOccurred()) 109 g.Expect(warnings).To(BeEmpty()) 110 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 111 g.Expect(err).ToNot(HaveOccurred()) 112 g.Expect(warnings).To(BeEmpty()) 113 } 114 }) 115 } 116 } 117 118 func TestMachineHealthCheckClusterNameImmutable(t *testing.T) { 119 tests := []struct { 120 name string 121 oldClusterName string 122 newClusterName string 123 expectErr bool 124 }{ 125 { 126 name: "when the cluster name has not changed", 127 oldClusterName: "foo", 128 newClusterName: "foo", 129 expectErr: false, 130 }, 131 { 132 name: "when the cluster name has changed", 133 oldClusterName: "foo", 134 newClusterName: "bar", 135 expectErr: true, 136 }, 137 } 138 139 for _, tt := range tests { 140 t.Run(tt.name, func(t *testing.T) { 141 g := NewWithT(t) 142 143 newMHC := &clusterv1.MachineHealthCheck{ 144 Spec: clusterv1.MachineHealthCheckSpec{ 145 ClusterName: tt.newClusterName, 146 Selector: metav1.LabelSelector{ 147 MatchLabels: map[string]string{ 148 "test": "test", 149 }, 150 }, 151 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 152 { 153 Type: corev1.NodeReady, 154 Status: corev1.ConditionFalse, 155 }, 156 }, 157 }, 158 } 159 oldMHC := &clusterv1.MachineHealthCheck{ 160 Spec: clusterv1.MachineHealthCheckSpec{ 161 ClusterName: tt.oldClusterName, 162 Selector: metav1.LabelSelector{ 163 MatchLabels: map[string]string{ 164 "test": "test", 165 }, 166 }, 167 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 168 { 169 Type: corev1.NodeReady, 170 Status: corev1.ConditionFalse, 171 }, 172 }, 173 }, 174 } 175 176 warnings, err := (&MachineHealthCheck{}).ValidateUpdate(ctx, oldMHC, newMHC) 177 if tt.expectErr { 178 g.Expect(err).To(HaveOccurred()) 179 } else { 180 g.Expect(err).ToNot(HaveOccurred()) 181 } 182 g.Expect(warnings).To(BeEmpty()) 183 }) 184 } 185 } 186 187 func TestMachineHealthCheckUnhealthyConditions(t *testing.T) { 188 tests := []struct { 189 name string 190 unhealthConditions []clusterv1.UnhealthyCondition 191 expectErr bool 192 }{ 193 { 194 name: "pass with correctly defined unhealthyConditions", 195 unhealthConditions: []clusterv1.UnhealthyCondition{ 196 { 197 Type: corev1.NodeReady, 198 Status: corev1.ConditionFalse, 199 }, 200 }, 201 expectErr: false, 202 }, 203 { 204 name: "fail if the UnhealthCondition array is nil", 205 unhealthConditions: nil, 206 expectErr: true, 207 }, 208 { 209 name: "fail if the UnhealthCondition array is empty", 210 unhealthConditions: []clusterv1.UnhealthyCondition{}, 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 mhc := &clusterv1.MachineHealthCheck{ 219 Spec: clusterv1.MachineHealthCheckSpec{ 220 Selector: metav1.LabelSelector{ 221 MatchLabels: map[string]string{ 222 "test": "test", 223 }, 224 }, 225 UnhealthyConditions: tt.unhealthConditions, 226 }, 227 } 228 webhook := &MachineHealthCheck{} 229 230 if tt.expectErr { 231 warnings, err := webhook.ValidateCreate(ctx, mhc) 232 g.Expect(err).To(HaveOccurred()) 233 g.Expect(warnings).To(BeEmpty()) 234 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 235 g.Expect(err).To(HaveOccurred()) 236 g.Expect(warnings).To(BeEmpty()) 237 } else { 238 warnings, err := webhook.ValidateCreate(ctx, mhc) 239 g.Expect(err).ToNot(HaveOccurred()) 240 g.Expect(warnings).To(BeEmpty()) 241 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 242 g.Expect(err).ToNot(HaveOccurred()) 243 g.Expect(warnings).To(BeEmpty()) 244 } 245 }) 246 } 247 } 248 249 func TestMachineHealthCheckNodeStartupTimeout(t *testing.T) { 250 zero := metav1.Duration{Duration: 0} 251 twentyNineSeconds := metav1.Duration{Duration: 29 * time.Second} 252 thirtySeconds := metav1.Duration{Duration: 30 * time.Second} 253 oneMinute := metav1.Duration{Duration: 1 * time.Minute} 254 minusOneMinute := metav1.Duration{Duration: -1 * time.Minute} 255 256 tests := []struct { 257 name string 258 timeout *metav1.Duration 259 expectErr bool 260 }{ 261 { 262 name: "when the nodeStartupTimeout is not given", 263 timeout: nil, 264 expectErr: false, 265 }, 266 { 267 name: "when the nodeStartupTimeout is greater than 30s", 268 timeout: &oneMinute, 269 expectErr: false, 270 }, 271 { 272 name: "when the nodeStartupTimeout is 30s", 273 timeout: &thirtySeconds, 274 expectErr: false, 275 }, 276 { 277 name: "when the nodeStartupTimeout is 29s", 278 timeout: &twentyNineSeconds, 279 expectErr: true, 280 }, 281 { 282 name: "when the nodeStartupTimeout is less than 0", 283 timeout: &minusOneMinute, 284 expectErr: true, 285 }, 286 { 287 name: "when the nodeStartupTimeout is 0 (disabled)", 288 timeout: &zero, 289 expectErr: false, 290 }, 291 } 292 293 for _, tt := range tests { 294 g := NewWithT(t) 295 296 mhc := &clusterv1.MachineHealthCheck{ 297 Spec: clusterv1.MachineHealthCheckSpec{ 298 NodeStartupTimeout: tt.timeout, 299 Selector: metav1.LabelSelector{ 300 MatchLabels: map[string]string{ 301 "test": "test", 302 }, 303 }, 304 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 305 { 306 Type: corev1.NodeReady, 307 Status: corev1.ConditionFalse, 308 }, 309 }, 310 }, 311 } 312 webhook := &MachineHealthCheck{} 313 314 if tt.expectErr { 315 warnings, err := webhook.ValidateCreate(ctx, mhc) 316 g.Expect(err).To(HaveOccurred()) 317 g.Expect(warnings).To(BeEmpty()) 318 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 319 g.Expect(err).To(HaveOccurred()) 320 g.Expect(warnings).To(BeEmpty()) 321 } else { 322 warnings, err := webhook.ValidateCreate(ctx, mhc) 323 g.Expect(err).ToNot(HaveOccurred()) 324 g.Expect(warnings).To(BeEmpty()) 325 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 326 g.Expect(err).ToNot(HaveOccurred()) 327 g.Expect(warnings).To(BeEmpty()) 328 } 329 } 330 } 331 332 func TestMachineHealthCheckMaxUnhealthy(t *testing.T) { 333 tests := []struct { 334 name string 335 value intstr.IntOrString 336 expectErr bool 337 }{ 338 { 339 name: "when the value is an integer", 340 value: intstr.Parse("10"), 341 expectErr: false, 342 }, 343 { 344 name: "when the value is a percentage", 345 value: intstr.Parse("10%"), 346 expectErr: false, 347 }, 348 { 349 name: "when the value is a random string", 350 value: intstr.Parse("abcdef"), 351 expectErr: true, 352 }, 353 { 354 name: "when the value stringified integer", 355 value: intstr.FromString("10"), 356 expectErr: true, 357 }, 358 } 359 360 for _, tt := range tests { 361 g := NewWithT(t) 362 363 maxUnhealthy := tt.value 364 mhc := &clusterv1.MachineHealthCheck{ 365 Spec: clusterv1.MachineHealthCheckSpec{ 366 MaxUnhealthy: &maxUnhealthy, 367 Selector: metav1.LabelSelector{ 368 MatchLabels: map[string]string{ 369 "test": "test", 370 }, 371 }, 372 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 373 { 374 Type: corev1.NodeReady, 375 Status: corev1.ConditionFalse, 376 }, 377 }, 378 }, 379 } 380 webhook := &MachineHealthCheck{} 381 382 if tt.expectErr { 383 warnings, err := webhook.ValidateCreate(ctx, mhc) 384 g.Expect(err).To(HaveOccurred()) 385 g.Expect(warnings).To(BeEmpty()) 386 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 387 g.Expect(err).To(HaveOccurred()) 388 g.Expect(warnings).To(BeEmpty()) 389 } else { 390 warnings, err := webhook.ValidateCreate(ctx, mhc) 391 g.Expect(err).ToNot(HaveOccurred()) 392 g.Expect(warnings).To(BeEmpty()) 393 warnings, err = webhook.ValidateUpdate(ctx, mhc, mhc) 394 g.Expect(err).ToNot(HaveOccurred()) 395 g.Expect(warnings).To(BeEmpty()) 396 } 397 } 398 } 399 400 func TestMachineHealthCheckSelectorValidation(t *testing.T) { 401 g := NewWithT(t) 402 mhc := &clusterv1.MachineHealthCheck{ 403 Spec: clusterv1.MachineHealthCheckSpec{ 404 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 405 { 406 Type: corev1.NodeReady, 407 Status: corev1.ConditionFalse, 408 }, 409 }, 410 }, 411 } 412 webhook := &MachineHealthCheck{} 413 414 err := webhook.validate(nil, mhc) 415 g.Expect(err).To(HaveOccurred()) 416 g.Expect(err.Error()).To(ContainSubstring("selector must not be empty")) 417 } 418 419 func TestMachineHealthCheckClusterNameSelectorValidation(t *testing.T) { 420 g := NewWithT(t) 421 mhc := &clusterv1.MachineHealthCheck{ 422 Spec: clusterv1.MachineHealthCheckSpec{ 423 ClusterName: "foo", 424 Selector: metav1.LabelSelector{ 425 MatchLabels: map[string]string{ 426 clusterv1.ClusterNameLabel: "bar", 427 "baz": "qux", 428 }, 429 }, 430 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 431 { 432 Type: corev1.NodeReady, 433 Status: corev1.ConditionFalse, 434 }, 435 }, 436 }, 437 } 438 webhook := &MachineHealthCheck{} 439 440 err := webhook.validate(nil, mhc) 441 g.Expect(err).To(HaveOccurred()) 442 g.Expect(err.Error()).To(ContainSubstring("cannot specify a cluster selector other than the one specified by ClusterName")) 443 444 mhc.Spec.Selector.MatchLabels[clusterv1.ClusterNameLabel] = "foo" 445 g.Expect(webhook.validate(nil, mhc)).To(Succeed()) 446 delete(mhc.Spec.Selector.MatchLabels, clusterv1.ClusterNameLabel) 447 g.Expect(webhook.validate(nil, mhc)).To(Succeed()) 448 } 449 450 func TestMachineHealthCheckRemediationTemplateNamespaceValidation(t *testing.T) { 451 valid := &clusterv1.MachineHealthCheck{ 452 ObjectMeta: metav1.ObjectMeta{ 453 Namespace: "foo", 454 }, 455 Spec: clusterv1.MachineHealthCheckSpec{ 456 Selector: metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, 457 RemediationTemplate: &corev1.ObjectReference{Namespace: "foo"}, 458 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 459 { 460 Type: corev1.NodeReady, 461 Status: corev1.ConditionFalse, 462 }, 463 }, 464 }, 465 } 466 invalid := valid.DeepCopy() 467 invalid.Spec.RemediationTemplate.Namespace = "bar" 468 469 tests := []struct { 470 name string 471 expectErr bool 472 c *clusterv1.MachineHealthCheck 473 }{ 474 { 475 name: "should return error when MachineHealthCheck namespace and RemediationTemplate ref namespace mismatch", 476 expectErr: true, 477 c: invalid, 478 }, 479 { 480 name: "should succeed when namespaces match", 481 expectErr: false, 482 c: valid, 483 }, 484 } 485 486 for _, tt := range tests { 487 t.Run(tt.name, func(t *testing.T) { 488 g := NewWithT(t) 489 webhook := &MachineHealthCheck{} 490 491 if tt.expectErr { 492 g.Expect(webhook.validate(nil, tt.c)).NotTo(Succeed()) 493 } else { 494 g.Expect(webhook.validate(nil, tt.c)).To(Succeed()) 495 } 496 }) 497 } 498 }