k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/core/validation/validation_test.go (about) 1 /* 2 Copyright 2014 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 validation 18 19 import ( 20 "bytes" 21 "fmt" 22 "math" 23 "reflect" 24 "runtime" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 "google.golang.org/protobuf/proto" 34 v1 "k8s.io/api/core/v1" 35 "k8s.io/apimachinery/pkg/api/resource" 36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 "k8s.io/apimachinery/pkg/util/intstr" 38 "k8s.io/apimachinery/pkg/util/sets" 39 "k8s.io/apimachinery/pkg/util/validation" 40 "k8s.io/apimachinery/pkg/util/validation/field" 41 utilfeature "k8s.io/apiserver/pkg/util/feature" 42 "k8s.io/component-base/featuregate" 43 featuregatetesting "k8s.io/component-base/featuregate/testing" 44 kubeletapis "k8s.io/kubelet/pkg/apis" 45 "k8s.io/kubernetes/pkg/apis/core" 46 "k8s.io/kubernetes/pkg/capabilities" 47 "k8s.io/kubernetes/pkg/features" 48 utilpointer "k8s.io/utils/pointer" 49 "k8s.io/utils/ptr" 50 ) 51 52 const ( 53 dnsLabelErrMsg = "a lowercase RFC 1123 label must consist of" 54 dnsSubdomainLabelErrMsg = "a lowercase RFC 1123 subdomain" 55 envVarNameErrMsg = "a valid environment variable name must consist of" 56 relaxedEnvVarNameFmtErrMsg string = "a valid environment variable name must consist only of printable ASCII characters other than '='" 57 defaultGracePeriod = int64(30) 58 noUserNamespace = false 59 ) 60 61 var ( 62 containerRestartPolicyAlways = core.ContainerRestartPolicyAlways 63 containerRestartPolicyOnFailure = core.ContainerRestartPolicy("OnFailure") 64 containerRestartPolicyNever = core.ContainerRestartPolicy("Never") 65 containerRestartPolicyInvalid = core.ContainerRestartPolicy("invalid") 66 containerRestartPolicyEmpty = core.ContainerRestartPolicy("") 67 ) 68 69 type topologyPair struct { 70 key string 71 value string 72 } 73 74 func line() string { 75 _, _, line, ok := runtime.Caller(1) 76 var s string 77 if ok { 78 s = fmt.Sprintf("%d", line) 79 } else { 80 s = "<??>" 81 } 82 return s 83 } 84 85 func prettyErrorList(errs field.ErrorList) string { 86 var s string 87 for _, e := range errs { 88 s += fmt.Sprintf("\t%s\n", e) 89 } 90 return s 91 } 92 93 func newHostPathType(pathType string) *core.HostPathType { 94 hostPathType := new(core.HostPathType) 95 *hostPathType = core.HostPathType(pathType) 96 return hostPathType 97 } 98 99 func testVolume(name string, namespace string, spec core.PersistentVolumeSpec) *core.PersistentVolume { 100 objMeta := metav1.ObjectMeta{Name: name} 101 if namespace != "" { 102 objMeta.Namespace = namespace 103 } 104 105 return &core.PersistentVolume{ 106 ObjectMeta: objMeta, 107 Spec: spec, 108 } 109 } 110 111 func TestValidatePersistentVolumes(t *testing.T) { 112 validMode := core.PersistentVolumeFilesystem 113 invalidMode := core.PersistentVolumeMode("fakeVolumeMode") 114 scenarios := map[string]struct { 115 isExpectedFailure bool 116 enableVolumeAttributesClass bool 117 volume *core.PersistentVolume 118 }{ 119 "good-volume": { 120 isExpectedFailure: false, 121 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 122 Capacity: core.ResourceList{ 123 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 124 }, 125 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 126 PersistentVolumeSource: core.PersistentVolumeSource{ 127 HostPath: &core.HostPathVolumeSource{ 128 Path: "/foo", 129 Type: newHostPathType(string(core.HostPathDirectory)), 130 }, 131 }, 132 }), 133 }, 134 "good-volume-with-capacity-unit": { 135 isExpectedFailure: false, 136 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 137 Capacity: core.ResourceList{ 138 core.ResourceName(core.ResourceStorage): resource.MustParse("10Gi"), 139 }, 140 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 141 PersistentVolumeSource: core.PersistentVolumeSource{ 142 HostPath: &core.HostPathVolumeSource{ 143 Path: "/foo", 144 Type: newHostPathType(string(core.HostPathDirectory)), 145 }, 146 }, 147 }), 148 }, 149 "good-volume-without-capacity-unit": { 150 isExpectedFailure: false, 151 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 152 Capacity: core.ResourceList{ 153 core.ResourceName(core.ResourceStorage): resource.MustParse("10"), 154 }, 155 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 156 PersistentVolumeSource: core.PersistentVolumeSource{ 157 HostPath: &core.HostPathVolumeSource{ 158 Path: "/foo", 159 Type: newHostPathType(string(core.HostPathDirectory)), 160 }, 161 }, 162 }), 163 }, 164 "good-volume-with-storage-class": { 165 isExpectedFailure: false, 166 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 167 Capacity: core.ResourceList{ 168 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 169 }, 170 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 171 PersistentVolumeSource: core.PersistentVolumeSource{ 172 HostPath: &core.HostPathVolumeSource{ 173 Path: "/foo", 174 Type: newHostPathType(string(core.HostPathDirectory)), 175 }, 176 }, 177 StorageClassName: "valid", 178 }), 179 }, 180 "good-volume-with-retain-policy": { 181 isExpectedFailure: false, 182 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 183 Capacity: core.ResourceList{ 184 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 185 }, 186 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 187 PersistentVolumeSource: core.PersistentVolumeSource{ 188 HostPath: &core.HostPathVolumeSource{ 189 Path: "/foo", 190 Type: newHostPathType(string(core.HostPathDirectory)), 191 }, 192 }, 193 PersistentVolumeReclaimPolicy: core.PersistentVolumeReclaimRetain, 194 }), 195 }, 196 "good-volume-with-volume-mode": { 197 isExpectedFailure: false, 198 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 199 Capacity: core.ResourceList{ 200 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 201 }, 202 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 203 PersistentVolumeSource: core.PersistentVolumeSource{ 204 HostPath: &core.HostPathVolumeSource{ 205 Path: "/foo", 206 Type: newHostPathType(string(core.HostPathDirectory)), 207 }, 208 }, 209 VolumeMode: &validMode, 210 }), 211 }, 212 "invalid-accessmode": { 213 isExpectedFailure: true, 214 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 215 Capacity: core.ResourceList{ 216 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 217 }, 218 AccessModes: []core.PersistentVolumeAccessMode{"fakemode"}, 219 PersistentVolumeSource: core.PersistentVolumeSource{ 220 HostPath: &core.HostPathVolumeSource{ 221 Path: "/foo", 222 Type: newHostPathType(string(core.HostPathDirectory)), 223 }, 224 }, 225 }), 226 }, 227 "invalid-reclaimpolicy": { 228 isExpectedFailure: true, 229 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 230 Capacity: core.ResourceList{ 231 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 232 }, 233 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 234 PersistentVolumeSource: core.PersistentVolumeSource{ 235 HostPath: &core.HostPathVolumeSource{ 236 Path: "/foo", 237 Type: newHostPathType(string(core.HostPathDirectory)), 238 }, 239 }, 240 PersistentVolumeReclaimPolicy: "fakeReclaimPolicy", 241 }), 242 }, 243 "invalid-volume-mode": { 244 isExpectedFailure: true, 245 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 246 Capacity: core.ResourceList{ 247 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 248 }, 249 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 250 PersistentVolumeSource: core.PersistentVolumeSource{ 251 HostPath: &core.HostPathVolumeSource{ 252 Path: "/foo", 253 Type: newHostPathType(string(core.HostPathDirectory)), 254 }, 255 }, 256 VolumeMode: &invalidMode, 257 }), 258 }, 259 "with-read-write-once-pod": { 260 isExpectedFailure: false, 261 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 262 Capacity: core.ResourceList{ 263 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 264 }, 265 AccessModes: []core.PersistentVolumeAccessMode{"ReadWriteOncePod"}, 266 PersistentVolumeSource: core.PersistentVolumeSource{ 267 HostPath: &core.HostPathVolumeSource{ 268 Path: "/foo", 269 Type: newHostPathType(string(core.HostPathDirectory)), 270 }, 271 }, 272 }), 273 }, 274 "with-read-write-once-pod-and-others": { 275 isExpectedFailure: true, 276 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 277 Capacity: core.ResourceList{ 278 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 279 }, 280 AccessModes: []core.PersistentVolumeAccessMode{"ReadWriteOncePod", "ReadWriteMany"}, 281 PersistentVolumeSource: core.PersistentVolumeSource{ 282 HostPath: &core.HostPathVolumeSource{ 283 Path: "/foo", 284 Type: newHostPathType(string(core.HostPathDirectory)), 285 }, 286 }, 287 }), 288 }, 289 "unexpected-namespace": { 290 isExpectedFailure: true, 291 volume: testVolume("foo", "unexpected-namespace", core.PersistentVolumeSpec{ 292 Capacity: core.ResourceList{ 293 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 294 }, 295 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 296 PersistentVolumeSource: core.PersistentVolumeSource{ 297 HostPath: &core.HostPathVolumeSource{ 298 Path: "/foo", 299 Type: newHostPathType(string(core.HostPathDirectory)), 300 }, 301 }, 302 }), 303 }, 304 "missing-volume-source": { 305 isExpectedFailure: true, 306 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 307 Capacity: core.ResourceList{ 308 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 309 }, 310 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 311 }), 312 }, 313 "bad-name": { 314 isExpectedFailure: true, 315 volume: testVolume("123*Bad(Name", "unexpected-namespace", core.PersistentVolumeSpec{ 316 Capacity: core.ResourceList{ 317 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 318 }, 319 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 320 PersistentVolumeSource: core.PersistentVolumeSource{ 321 HostPath: &core.HostPathVolumeSource{ 322 Path: "/foo", 323 Type: newHostPathType(string(core.HostPathDirectory)), 324 }, 325 }, 326 }), 327 }, 328 "missing-name": { 329 isExpectedFailure: true, 330 volume: testVolume("", "", core.PersistentVolumeSpec{ 331 Capacity: core.ResourceList{ 332 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 333 }, 334 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 335 PersistentVolumeSource: core.PersistentVolumeSource{ 336 HostPath: &core.HostPathVolumeSource{ 337 Path: "/foo", 338 Type: newHostPathType(string(core.HostPathDirectory)), 339 }, 340 }, 341 }), 342 }, 343 "missing-capacity": { 344 isExpectedFailure: true, 345 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 346 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 347 PersistentVolumeSource: core.PersistentVolumeSource{ 348 HostPath: &core.HostPathVolumeSource{ 349 Path: "/foo", 350 Type: newHostPathType(string(core.HostPathDirectory)), 351 }, 352 }, 353 }), 354 }, 355 "bad-volume-zero-capacity": { 356 isExpectedFailure: true, 357 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 358 Capacity: core.ResourceList{ 359 core.ResourceName(core.ResourceStorage): resource.MustParse("0"), 360 }, 361 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 362 PersistentVolumeSource: core.PersistentVolumeSource{ 363 HostPath: &core.HostPathVolumeSource{ 364 Path: "/foo", 365 Type: newHostPathType(string(core.HostPathDirectory)), 366 }, 367 }, 368 }), 369 }, 370 "missing-accessmodes": { 371 isExpectedFailure: true, 372 volume: testVolume("goodname", "missing-accessmodes", core.PersistentVolumeSpec{ 373 Capacity: core.ResourceList{ 374 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 375 }, 376 PersistentVolumeSource: core.PersistentVolumeSource{ 377 HostPath: &core.HostPathVolumeSource{ 378 Path: "/foo", 379 Type: newHostPathType(string(core.HostPathDirectory)), 380 }, 381 }, 382 }), 383 }, 384 "too-many-sources": { 385 isExpectedFailure: true, 386 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 387 Capacity: core.ResourceList{ 388 core.ResourceName(core.ResourceStorage): resource.MustParse("5G"), 389 }, 390 PersistentVolumeSource: core.PersistentVolumeSource{ 391 HostPath: &core.HostPathVolumeSource{ 392 Path: "/foo", 393 Type: newHostPathType(string(core.HostPathDirectory)), 394 }, 395 GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{PDName: "foo", FSType: "ext4"}, 396 }, 397 }), 398 }, 399 "host mount of / with recycle reclaim policy": { 400 isExpectedFailure: true, 401 volume: testVolume("bad-recycle-do-not-want", "", core.PersistentVolumeSpec{ 402 Capacity: core.ResourceList{ 403 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 404 }, 405 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 406 PersistentVolumeSource: core.PersistentVolumeSource{ 407 HostPath: &core.HostPathVolumeSource{ 408 Path: "/", 409 Type: newHostPathType(string(core.HostPathDirectory)), 410 }, 411 }, 412 PersistentVolumeReclaimPolicy: core.PersistentVolumeReclaimRecycle, 413 }), 414 }, 415 "host mount of / with recycle reclaim policy 2": { 416 isExpectedFailure: true, 417 volume: testVolume("bad-recycle-do-not-want", "", core.PersistentVolumeSpec{ 418 Capacity: core.ResourceList{ 419 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 420 }, 421 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 422 PersistentVolumeSource: core.PersistentVolumeSource{ 423 HostPath: &core.HostPathVolumeSource{ 424 Path: "/a/..", 425 Type: newHostPathType(string(core.HostPathDirectory)), 426 }, 427 }, 428 PersistentVolumeReclaimPolicy: core.PersistentVolumeReclaimRecycle, 429 }), 430 }, 431 "invalid-storage-class-name": { 432 isExpectedFailure: true, 433 volume: testVolume("invalid-storage-class-name", "", core.PersistentVolumeSpec{ 434 Capacity: core.ResourceList{ 435 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 436 }, 437 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 438 PersistentVolumeSource: core.PersistentVolumeSource{ 439 HostPath: &core.HostPathVolumeSource{ 440 Path: "/foo", 441 Type: newHostPathType(string(core.HostPathDirectory)), 442 }, 443 }, 444 StorageClassName: "-invalid-", 445 }), 446 }, 447 "bad-hostpath-volume-backsteps": { 448 isExpectedFailure: true, 449 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 450 Capacity: core.ResourceList{ 451 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 452 }, 453 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 454 PersistentVolumeSource: core.PersistentVolumeSource{ 455 HostPath: &core.HostPathVolumeSource{ 456 Path: "/foo/..", 457 Type: newHostPathType(string(core.HostPathDirectory)), 458 }, 459 }, 460 StorageClassName: "backstep-hostpath", 461 }), 462 }, 463 "volume-node-affinity": { 464 isExpectedFailure: false, 465 volume: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 466 }, 467 "volume-empty-node-affinity": { 468 isExpectedFailure: true, 469 volume: testVolumeWithNodeAffinity(&core.VolumeNodeAffinity{}), 470 }, 471 "volume-bad-node-affinity": { 472 isExpectedFailure: true, 473 volume: testVolumeWithNodeAffinity( 474 &core.VolumeNodeAffinity{ 475 Required: &core.NodeSelector{ 476 NodeSelectorTerms: []core.NodeSelectorTerm{{ 477 MatchExpressions: []core.NodeSelectorRequirement{{ 478 Operator: core.NodeSelectorOpIn, 479 Values: []string{"test-label-value"}, 480 }}, 481 }}, 482 }, 483 }), 484 }, 485 "invalid-volume-attributes-class-name": { 486 isExpectedFailure: true, 487 enableVolumeAttributesClass: true, 488 volume: testVolume("invalid-volume-attributes-class-name", "", core.PersistentVolumeSpec{ 489 Capacity: core.ResourceList{ 490 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 491 }, 492 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 493 PersistentVolumeSource: core.PersistentVolumeSource{ 494 HostPath: &core.HostPathVolumeSource{ 495 Path: "/foo", 496 Type: newHostPathType(string(core.HostPathDirectory)), 497 }, 498 }, 499 StorageClassName: "invalid", 500 VolumeAttributesClassName: ptr.To("-invalid-"), 501 }), 502 }, 503 "invalid-empty-volume-attributes-class-name": { 504 isExpectedFailure: true, 505 enableVolumeAttributesClass: true, 506 volume: testVolume("invalid-empty-volume-attributes-class-name", "", core.PersistentVolumeSpec{ 507 Capacity: core.ResourceList{ 508 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 509 }, 510 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 511 PersistentVolumeSource: core.PersistentVolumeSource{ 512 HostPath: &core.HostPathVolumeSource{ 513 Path: "/foo", 514 Type: newHostPathType(string(core.HostPathDirectory)), 515 }, 516 }, 517 StorageClassName: "invalid", 518 VolumeAttributesClassName: ptr.To(""), 519 }), 520 }, 521 "volume-with-good-volume-attributes-class-and-matched-volume-resource-when-feature-gate-is-on": { 522 isExpectedFailure: false, 523 enableVolumeAttributesClass: true, 524 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 525 Capacity: core.ResourceList{ 526 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 527 }, 528 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 529 PersistentVolumeSource: core.PersistentVolumeSource{ 530 CSI: &core.CSIPersistentVolumeSource{ 531 Driver: "test-driver", 532 VolumeHandle: "test-123", 533 }, 534 }, 535 StorageClassName: "valid", 536 VolumeAttributesClassName: ptr.To("valid"), 537 }), 538 }, 539 "volume-with-good-volume-attributes-class-and-mismatched-volume-resource-when-feature-gate-is-on": { 540 isExpectedFailure: true, 541 enableVolumeAttributesClass: true, 542 volume: testVolume("foo", "", core.PersistentVolumeSpec{ 543 Capacity: core.ResourceList{ 544 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 545 }, 546 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 547 PersistentVolumeSource: core.PersistentVolumeSource{ 548 HostPath: &core.HostPathVolumeSource{ 549 Path: "/foo", 550 Type: newHostPathType(string(core.HostPathDirectory)), 551 }, 552 }, 553 StorageClassName: "valid", 554 VolumeAttributesClassName: ptr.To("valid"), 555 }), 556 }, 557 } 558 559 for name, scenario := range scenarios { 560 t.Run(name, func(t *testing.T) { 561 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass) 562 563 opts := ValidationOptionsForPersistentVolume(scenario.volume, nil) 564 errs := ValidatePersistentVolume(scenario.volume, opts) 565 if len(errs) == 0 && scenario.isExpectedFailure { 566 t.Errorf("Unexpected success for scenario: %s", name) 567 } 568 if len(errs) > 0 && !scenario.isExpectedFailure { 569 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 570 } 571 }) 572 } 573 574 } 575 576 func TestValidatePersistentVolumeSpec(t *testing.T) { 577 fsmode := core.PersistentVolumeFilesystem 578 blockmode := core.PersistentVolumeBlock 579 scenarios := map[string]struct { 580 isExpectedFailure bool 581 isInlineSpec bool 582 pvSpec *core.PersistentVolumeSpec 583 }{ 584 "pv-pvspec-valid": { 585 isExpectedFailure: false, 586 isInlineSpec: false, 587 pvSpec: &core.PersistentVolumeSpec{ 588 Capacity: core.ResourceList{ 589 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 590 }, 591 StorageClassName: "testclass", 592 PersistentVolumeReclaimPolicy: core.PersistentVolumeReclaimRecycle, 593 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 594 PersistentVolumeSource: core.PersistentVolumeSource{ 595 HostPath: &core.HostPathVolumeSource{ 596 Path: "/foo", 597 Type: newHostPathType(string(core.HostPathDirectory)), 598 }, 599 }, 600 VolumeMode: &fsmode, 601 NodeAffinity: simpleVolumeNodeAffinity("foo", "bar"), 602 }, 603 }, 604 "inline-pvspec-with-capacity": { 605 isExpectedFailure: true, 606 isInlineSpec: true, 607 pvSpec: &core.PersistentVolumeSpec{ 608 Capacity: core.ResourceList{ 609 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 610 }, 611 PersistentVolumeSource: core.PersistentVolumeSource{ 612 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 613 }, 614 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 615 }, 616 }, 617 "inline-pvspec-with-podSec": { 618 isExpectedFailure: true, 619 isInlineSpec: true, 620 pvSpec: &core.PersistentVolumeSpec{ 621 PersistentVolumeSource: core.PersistentVolumeSource{ 622 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 623 }, 624 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 625 StorageClassName: "testclass", 626 }, 627 }, 628 "inline-pvspec-with-non-fs-volume-mode": { 629 isExpectedFailure: true, 630 isInlineSpec: true, 631 pvSpec: &core.PersistentVolumeSpec{ 632 PersistentVolumeSource: core.PersistentVolumeSource{ 633 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 634 }, 635 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 636 VolumeMode: &blockmode, 637 }, 638 }, 639 "inline-pvspec-with-non-retain-reclaim-policy": { 640 isExpectedFailure: true, 641 isInlineSpec: true, 642 pvSpec: &core.PersistentVolumeSpec{ 643 PersistentVolumeReclaimPolicy: core.PersistentVolumeReclaimRecycle, 644 PersistentVolumeSource: core.PersistentVolumeSource{ 645 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 646 }, 647 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 648 }, 649 }, 650 "inline-pvspec-with-node-affinity": { 651 isExpectedFailure: true, 652 isInlineSpec: true, 653 pvSpec: &core.PersistentVolumeSpec{ 654 PersistentVolumeSource: core.PersistentVolumeSource{ 655 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 656 }, 657 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 658 NodeAffinity: simpleVolumeNodeAffinity("foo", "bar"), 659 }, 660 }, 661 "inline-pvspec-with-non-csi-source": { 662 isExpectedFailure: true, 663 isInlineSpec: true, 664 pvSpec: &core.PersistentVolumeSpec{ 665 PersistentVolumeSource: core.PersistentVolumeSource{ 666 HostPath: &core.HostPathVolumeSource{ 667 Path: "/foo", 668 Type: newHostPathType(string(core.HostPathDirectory)), 669 }, 670 }, 671 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 672 }, 673 }, 674 "inline-pvspec-valid-with-access-modes-and-mount-options": { 675 isExpectedFailure: false, 676 isInlineSpec: true, 677 pvSpec: &core.PersistentVolumeSpec{ 678 PersistentVolumeSource: core.PersistentVolumeSource{ 679 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 680 }, 681 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 682 MountOptions: []string{"soft", "read-write"}, 683 }, 684 }, 685 "inline-pvspec-valid-with-access-modes": { 686 isExpectedFailure: false, 687 isInlineSpec: true, 688 pvSpec: &core.PersistentVolumeSpec{ 689 PersistentVolumeSource: core.PersistentVolumeSource{ 690 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 691 }, 692 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 693 }, 694 }, 695 "inline-pvspec-with-missing-acess-modes": { 696 isExpectedFailure: true, 697 isInlineSpec: true, 698 pvSpec: &core.PersistentVolumeSpec{ 699 PersistentVolumeSource: core.PersistentVolumeSource{ 700 CSI: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 701 }, 702 MountOptions: []string{"soft", "read-write"}, 703 }, 704 }, 705 } 706 for name, scenario := range scenarios { 707 opts := PersistentVolumeSpecValidationOptions{} 708 errs := ValidatePersistentVolumeSpec(scenario.pvSpec, "", scenario.isInlineSpec, field.NewPath("field"), opts) 709 if len(errs) == 0 && scenario.isExpectedFailure { 710 t.Errorf("Unexpected success for scenario: %s", name) 711 } 712 if len(errs) > 0 && !scenario.isExpectedFailure { 713 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 714 } 715 } 716 } 717 718 func TestValidatePersistentVolumeSourceUpdate(t *testing.T) { 719 validVolume := testVolume("foo", "", core.PersistentVolumeSpec{ 720 Capacity: core.ResourceList{ 721 core.ResourceName(core.ResourceStorage): resource.MustParse("1G"), 722 }, 723 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 724 PersistentVolumeSource: core.PersistentVolumeSource{ 725 HostPath: &core.HostPathVolumeSource{ 726 Path: "/foo", 727 Type: newHostPathType(string(core.HostPathDirectory)), 728 }, 729 }, 730 StorageClassName: "valid", 731 }) 732 validPvSourceNoUpdate := validVolume.DeepCopy() 733 invalidPvSourceUpdateType := validVolume.DeepCopy() 734 invalidPvSourceUpdateType.Spec.PersistentVolumeSource = core.PersistentVolumeSource{ 735 FlexVolume: &core.FlexPersistentVolumeSource{ 736 Driver: "kubernetes.io/blue", 737 FSType: "ext4", 738 }, 739 } 740 invalidPvSourceUpdateDeep := validVolume.DeepCopy() 741 invalidPvSourceUpdateDeep.Spec.PersistentVolumeSource = core.PersistentVolumeSource{ 742 HostPath: &core.HostPathVolumeSource{ 743 Path: "/updated", 744 Type: newHostPathType(string(core.HostPathDirectory)), 745 }, 746 } 747 748 validCSIVolume := testVolume("csi-volume", "", core.PersistentVolumeSpec{ 749 Capacity: core.ResourceList{ 750 core.ResourceName(core.ResourceStorage): resource.MustParse("1G"), 751 }, 752 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 753 PersistentVolumeSource: core.PersistentVolumeSource{ 754 CSI: &core.CSIPersistentVolumeSource{ 755 Driver: "come.google.gcepd", 756 VolumeHandle: "foobar", 757 }, 758 }, 759 StorageClassName: "gp2", 760 }) 761 762 expandSecretRef := &core.SecretReference{ 763 Name: "expansion-secret", 764 Namespace: "default", 765 } 766 767 // shortSecretRef refers to the secretRefs which are validated with IsDNS1035Label 768 shortSecretName := "key-name" 769 shortSecretRef := &core.SecretReference{ 770 Name: shortSecretName, 771 Namespace: "default", 772 } 773 774 // longSecretRef refers to the secretRefs which are validated with IsDNS1123Subdomain 775 longSecretName := "key-name.example.com" 776 longSecretRef := &core.SecretReference{ 777 Name: longSecretName, 778 Namespace: "default", 779 } 780 781 // invalidSecrets missing name, namespace and both 782 inValidSecretRef := &core.SecretReference{ 783 Name: "", 784 Namespace: "", 785 } 786 invalidSecretRefmissingName := &core.SecretReference{ 787 Name: "", 788 Namespace: "default", 789 } 790 invalidSecretRefmissingNamespace := &core.SecretReference{ 791 Name: "invalidnamespace", 792 Namespace: "", 793 } 794 795 scenarios := map[string]struct { 796 isExpectedFailure bool 797 oldVolume *core.PersistentVolume 798 newVolume *core.PersistentVolume 799 }{ 800 "condition-no-update": { 801 isExpectedFailure: false, 802 oldVolume: validVolume, 803 newVolume: validPvSourceNoUpdate, 804 }, 805 "condition-update-source-type": { 806 isExpectedFailure: true, 807 oldVolume: validVolume, 808 newVolume: invalidPvSourceUpdateType, 809 }, 810 "condition-update-source-deep": { 811 isExpectedFailure: true, 812 oldVolume: validVolume, 813 newVolume: invalidPvSourceUpdateDeep, 814 }, 815 "csi-expansion-enabled-with-pv-secret": { 816 isExpectedFailure: false, 817 oldVolume: validCSIVolume, 818 newVolume: getCSIVolumeWithSecret(validCSIVolume, expandSecretRef, "controllerExpand"), 819 }, 820 "csi-expansion-enabled-with-old-pv-secret": { 821 isExpectedFailure: true, 822 oldVolume: getCSIVolumeWithSecret(validCSIVolume, expandSecretRef, "controllerExpand"), 823 newVolume: getCSIVolumeWithSecret(validCSIVolume, &core.SecretReference{ 824 Name: "foo-secret", 825 Namespace: "default", 826 }, "controllerExpand"), 827 }, 828 "csi-expansion-enabled-with-shortSecretRef": { 829 isExpectedFailure: false, 830 oldVolume: validCSIVolume, 831 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerExpand"), 832 }, 833 "csi-expansion-enabled-with-longSecretRef": { 834 isExpectedFailure: false, // updating controllerExpandSecretRef is allowed only from nil 835 oldVolume: validCSIVolume, 836 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerExpand"), 837 }, 838 "csi-expansion-enabled-from-shortSecretRef-to-shortSecretRef": { 839 isExpectedFailure: false, 840 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerExpand"), 841 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerExpand"), 842 }, 843 "csi-expansion-enabled-from-shortSecretRef-to-longSecretRef": { 844 isExpectedFailure: true, // updating controllerExpandSecretRef is allowed only from nil 845 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerExpand"), 846 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerExpand"), 847 }, 848 "csi-expansion-enabled-from-longSecretRef-to-longSecretRef": { 849 isExpectedFailure: false, 850 oldVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerExpand"), 851 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerExpand"), 852 }, 853 "csi-cntrlpublish-enabled-with-shortSecretRef": { 854 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 855 oldVolume: validCSIVolume, 856 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerPublish"), 857 }, 858 "csi-cntrlpublish-enabled-with-longSecretRef": { 859 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 860 oldVolume: validCSIVolume, 861 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerPublish"), 862 }, 863 "csi-cntrlpublish-enabled-from-shortSecretRef-to-shortSecretRef": { 864 isExpectedFailure: false, 865 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerPublish"), 866 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerPublish"), 867 }, 868 "csi-cntrlpublish-enabled-from-shortSecretRef-to-longSecretRef": { 869 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 870 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "controllerPublish"), 871 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerPublish"), 872 }, 873 "csi-cntrlpublish-enabled-from-longSecretRef-to-longSecretRef": { 874 isExpectedFailure: false, 875 oldVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerPublish"), 876 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "controllerPublish"), 877 }, 878 "csi-nodepublish-enabled-with-shortSecretRef": { 879 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 880 oldVolume: validCSIVolume, 881 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodePublish"), 882 }, 883 "csi-nodepublish-enabled-with-longSecretRef": { 884 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 885 oldVolume: validCSIVolume, 886 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodePublish"), 887 }, 888 "csi-nodepublish-enabled-from-shortSecretRef-to-shortSecretRef": { 889 isExpectedFailure: false, 890 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodePublish"), 891 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodePublish"), 892 }, 893 "csi-nodepublish-enabled-from-shortSecretRef-to-longSecretRef": { 894 isExpectedFailure: true, 895 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodePublish"), 896 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodePublish"), 897 }, 898 "csi-nodepublish-enabled-from-longSecretRef-to-longSecretRef": { 899 isExpectedFailure: false, 900 oldVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodePublish"), 901 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodePublish"), 902 }, 903 "csi-nodestage-enabled-with-shortSecretRef": { 904 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 905 oldVolume: validCSIVolume, 906 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodeStage"), 907 }, 908 "csi-nodestage-enabled-with-longSecretRef": { 909 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 910 oldVolume: validCSIVolume, 911 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodeStage"), 912 }, 913 "csi-nodestage-enabled-from-shortSecretRef-to-longSecretRef": { 914 isExpectedFailure: true, // updating secretRef will fail as the object is immutable eventhough the secretRef is valid 915 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodeStage"), 916 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodeStage"), 917 }, 918 919 // At present, there is no validation exist for nodeStage secretRef in 920 // ValidatePersistentVolumeSpec->validateCSIPersistentVolumeSource, due to that, below 921 // checks/validations pass! 922 923 "csi-nodestage-enabled-from-invalidSecretRef-to-invalidSecretRef": { 924 isExpectedFailure: false, 925 oldVolume: getCSIVolumeWithSecret(validCSIVolume, inValidSecretRef, "nodeStage"), 926 newVolume: getCSIVolumeWithSecret(validCSIVolume, inValidSecretRef, "nodeStage"), 927 }, 928 "csi-nodestage-enabled-from-invalidSecretRefmissingname-to-invalidSecretRefmissingname": { 929 isExpectedFailure: false, 930 oldVolume: getCSIVolumeWithSecret(validCSIVolume, invalidSecretRefmissingName, "nodeStage"), 931 newVolume: getCSIVolumeWithSecret(validCSIVolume, invalidSecretRefmissingName, "nodeStage"), 932 }, 933 "csi-nodestage-enabled-from-invalidSecretRefmissingnamespace-to-invalidSecretRefmissingnamespace": { 934 isExpectedFailure: false, 935 oldVolume: getCSIVolumeWithSecret(validCSIVolume, invalidSecretRefmissingNamespace, "nodeStage"), 936 newVolume: getCSIVolumeWithSecret(validCSIVolume, invalidSecretRefmissingNamespace, "nodeStage"), 937 }, 938 "csi-nodestage-enabled-from-shortSecretRef-to-shortSecretRef": { 939 isExpectedFailure: false, 940 oldVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodeStage"), 941 newVolume: getCSIVolumeWithSecret(validCSIVolume, shortSecretRef, "nodeStage"), 942 }, 943 "csi-nodestage-enabled-from-longSecretRef-to-longSecretRef": { 944 isExpectedFailure: false, 945 oldVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodeStage"), 946 newVolume: getCSIVolumeWithSecret(validCSIVolume, longSecretRef, "nodeStage"), 947 }, 948 } 949 for name, scenario := range scenarios { 950 opts := ValidationOptionsForPersistentVolume(scenario.newVolume, scenario.oldVolume) 951 errs := ValidatePersistentVolumeUpdate(scenario.newVolume, scenario.oldVolume, opts) 952 if len(errs) == 0 && scenario.isExpectedFailure { 953 t.Errorf("Unexpected success for scenario: %s", name) 954 } 955 if len(errs) > 0 && !scenario.isExpectedFailure { 956 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 957 } 958 } 959 } 960 961 func TestValidationOptionsForPersistentVolume(t *testing.T) { 962 tests := map[string]struct { 963 oldPv *core.PersistentVolume 964 enableVolumeAttributesClass bool 965 expectValidationOpts PersistentVolumeSpecValidationOptions 966 }{ 967 "nil old pv": { 968 oldPv: nil, 969 expectValidationOpts: PersistentVolumeSpecValidationOptions{}, 970 }, 971 "nil old pv and feature-gate VolumeAttrributesClass is on": { 972 oldPv: nil, 973 enableVolumeAttributesClass: true, 974 expectValidationOpts: PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, 975 }, 976 "nil old pv and feature-gate VolumeAttrributesClass is off": { 977 oldPv: nil, 978 enableVolumeAttributesClass: false, 979 expectValidationOpts: PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: false}, 980 }, 981 "old pv has volumeAttributesClass and feature-gate VolumeAttrributesClass is on": { 982 oldPv: &core.PersistentVolume{ 983 Spec: core.PersistentVolumeSpec{ 984 VolumeAttributesClassName: ptr.To("foo"), 985 }, 986 }, 987 enableVolumeAttributesClass: true, 988 expectValidationOpts: PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, 989 }, 990 "old pv has volumeAttributesClass and feature-gate VolumeAttrributesClass is off": { 991 oldPv: &core.PersistentVolume{ 992 Spec: core.PersistentVolumeSpec{ 993 VolumeAttributesClassName: ptr.To("foo"), 994 }, 995 }, 996 enableVolumeAttributesClass: false, 997 expectValidationOpts: PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true}, 998 }, 999 } 1000 1001 for name, tc := range tests { 1002 t.Run(name, func(t *testing.T) { 1003 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass) 1004 1005 opts := ValidationOptionsForPersistentVolume(nil, tc.oldPv) 1006 if opts != tc.expectValidationOpts { 1007 t.Errorf("Expected opts: %+v, received: %+v", opts, tc.expectValidationOpts) 1008 } 1009 }) 1010 } 1011 } 1012 1013 func getCSIVolumeWithSecret(pv *core.PersistentVolume, secret *core.SecretReference, secretfield string) *core.PersistentVolume { 1014 pvCopy := pv.DeepCopy() 1015 switch secretfield { 1016 case "controllerExpand": 1017 pvCopy.Spec.CSI.ControllerExpandSecretRef = secret 1018 case "controllerPublish": 1019 pvCopy.Spec.CSI.ControllerPublishSecretRef = secret 1020 case "nodePublish": 1021 pvCopy.Spec.CSI.NodePublishSecretRef = secret 1022 case "nodeStage": 1023 pvCopy.Spec.CSI.NodeStageSecretRef = secret 1024 default: 1025 panic("unknown string") 1026 } 1027 1028 return pvCopy 1029 } 1030 1031 func pvcWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaim { 1032 return &core.PersistentVolumeClaim{ 1033 Spec: core.PersistentVolumeClaimSpec{ 1034 VolumeAttributesClassName: vacName, 1035 }, 1036 } 1037 } 1038 1039 func pvcWithDataSource(dataSource *core.TypedLocalObjectReference) *core.PersistentVolumeClaim { 1040 return &core.PersistentVolumeClaim{ 1041 Spec: core.PersistentVolumeClaimSpec{ 1042 DataSource: dataSource, 1043 }, 1044 } 1045 } 1046 func pvcWithDataSourceRef(ref *core.TypedObjectReference) *core.PersistentVolumeClaim { 1047 return &core.PersistentVolumeClaim{ 1048 Spec: core.PersistentVolumeClaimSpec{ 1049 DataSourceRef: ref, 1050 }, 1051 } 1052 } 1053 1054 func pvcTemplateWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaimTemplate { 1055 return &core.PersistentVolumeClaimTemplate{ 1056 Spec: core.PersistentVolumeClaimSpec{ 1057 VolumeAttributesClassName: vacName, 1058 }, 1059 } 1060 } 1061 1062 func testLocalVolume(path string, affinity *core.VolumeNodeAffinity) core.PersistentVolumeSpec { 1063 return core.PersistentVolumeSpec{ 1064 Capacity: core.ResourceList{ 1065 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1066 }, 1067 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 1068 PersistentVolumeSource: core.PersistentVolumeSource{ 1069 Local: &core.LocalVolumeSource{ 1070 Path: path, 1071 }, 1072 }, 1073 NodeAffinity: affinity, 1074 StorageClassName: "test-storage-class", 1075 } 1076 } 1077 1078 func TestValidateLocalVolumes(t *testing.T) { 1079 scenarios := map[string]struct { 1080 isExpectedFailure bool 1081 volume *core.PersistentVolume 1082 }{ 1083 "alpha invalid local volume nil annotations": { 1084 isExpectedFailure: true, 1085 volume: testVolume( 1086 "invalid-local-volume-nil-annotations", 1087 "", 1088 testLocalVolume("/foo", nil)), 1089 }, 1090 "valid local volume": { 1091 isExpectedFailure: false, 1092 volume: testVolume("valid-local-volume", "", 1093 testLocalVolume("/foo", simpleVolumeNodeAffinity("foo", "bar"))), 1094 }, 1095 "invalid local volume no node affinity": { 1096 isExpectedFailure: true, 1097 volume: testVolume("invalid-local-volume-no-node-affinity", "", 1098 testLocalVolume("/foo", nil)), 1099 }, 1100 "invalid local volume empty path": { 1101 isExpectedFailure: true, 1102 volume: testVolume("invalid-local-volume-empty-path", "", 1103 testLocalVolume("", simpleVolumeNodeAffinity("foo", "bar"))), 1104 }, 1105 "invalid-local-volume-backsteps": { 1106 isExpectedFailure: true, 1107 volume: testVolume("foo", "", 1108 testLocalVolume("/foo/..", simpleVolumeNodeAffinity("foo", "bar"))), 1109 }, 1110 "valid-local-volume-relative-path": { 1111 isExpectedFailure: false, 1112 volume: testVolume("foo", "", 1113 testLocalVolume("foo", simpleVolumeNodeAffinity("foo", "bar"))), 1114 }, 1115 } 1116 1117 for name, scenario := range scenarios { 1118 opts := ValidationOptionsForPersistentVolume(scenario.volume, nil) 1119 errs := ValidatePersistentVolume(scenario.volume, opts) 1120 if len(errs) == 0 && scenario.isExpectedFailure { 1121 t.Errorf("Unexpected success for scenario: %s", name) 1122 } 1123 if len(errs) > 0 && !scenario.isExpectedFailure { 1124 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 1125 } 1126 } 1127 } 1128 1129 func testVolumeWithVolumeAttributesClass(vacName *string) *core.PersistentVolume { 1130 return testVolume("test-volume-with-volume-attributes-class", "", 1131 core.PersistentVolumeSpec{ 1132 Capacity: core.ResourceList{ 1133 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1134 }, 1135 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 1136 PersistentVolumeSource: core.PersistentVolumeSource{ 1137 CSI: &core.CSIPersistentVolumeSource{ 1138 Driver: "test-driver", 1139 VolumeHandle: "test-123", 1140 }, 1141 }, 1142 StorageClassName: "test-storage-class", 1143 VolumeAttributesClassName: vacName, 1144 }) 1145 } 1146 1147 func testVolumeWithNodeAffinity(affinity *core.VolumeNodeAffinity) *core.PersistentVolume { 1148 return testVolume("test-affinity-volume", "", 1149 core.PersistentVolumeSpec{ 1150 Capacity: core.ResourceList{ 1151 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1152 }, 1153 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 1154 PersistentVolumeSource: core.PersistentVolumeSource{ 1155 GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{ 1156 PDName: "foo", 1157 }, 1158 }, 1159 StorageClassName: "test-storage-class", 1160 NodeAffinity: affinity, 1161 }) 1162 } 1163 1164 func simpleVolumeNodeAffinity(key, value string) *core.VolumeNodeAffinity { 1165 return &core.VolumeNodeAffinity{ 1166 Required: &core.NodeSelector{ 1167 NodeSelectorTerms: []core.NodeSelectorTerm{{ 1168 MatchExpressions: []core.NodeSelectorRequirement{{ 1169 Key: key, 1170 Operator: core.NodeSelectorOpIn, 1171 Values: []string{value}, 1172 }}, 1173 }}, 1174 }, 1175 } 1176 } 1177 1178 func multipleVolumeNodeAffinity(terms [][]topologyPair) *core.VolumeNodeAffinity { 1179 nodeSelectorTerms := []core.NodeSelectorTerm{} 1180 for _, term := range terms { 1181 matchExpressions := []core.NodeSelectorRequirement{} 1182 for _, topology := range term { 1183 matchExpressions = append(matchExpressions, core.NodeSelectorRequirement{ 1184 Key: topology.key, 1185 Operator: core.NodeSelectorOpIn, 1186 Values: []string{topology.value}, 1187 }) 1188 } 1189 nodeSelectorTerms = append(nodeSelectorTerms, core.NodeSelectorTerm{ 1190 MatchExpressions: matchExpressions, 1191 }) 1192 } 1193 1194 return &core.VolumeNodeAffinity{ 1195 Required: &core.NodeSelector{ 1196 NodeSelectorTerms: nodeSelectorTerms, 1197 }, 1198 } 1199 } 1200 1201 func TestValidateVolumeNodeAffinityUpdate(t *testing.T) { 1202 scenarios := map[string]struct { 1203 isExpectedFailure bool 1204 oldPV *core.PersistentVolume 1205 newPV *core.PersistentVolume 1206 }{ 1207 "nil-nothing-changed": { 1208 isExpectedFailure: false, 1209 oldPV: testVolumeWithNodeAffinity(nil), 1210 newPV: testVolumeWithNodeAffinity(nil), 1211 }, 1212 "affinity-nothing-changed": { 1213 isExpectedFailure: false, 1214 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1215 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1216 }, 1217 "affinity-changed": { 1218 isExpectedFailure: true, 1219 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1220 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar2")), 1221 }, 1222 "affinity-non-beta-label-changed": { 1223 isExpectedFailure: true, 1224 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1225 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo2", "bar")), 1226 }, 1227 "affinity-zone-beta-unchanged": { 1228 isExpectedFailure: false, 1229 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaZone, "bar")), 1230 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaZone, "bar")), 1231 }, 1232 "affinity-zone-beta-label-to-GA": { 1233 isExpectedFailure: false, 1234 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaZone, "bar")), 1235 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelTopologyZone, "bar")), 1236 }, 1237 "affinity-zone-beta-label-to-non-GA": { 1238 isExpectedFailure: true, 1239 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaZone, "bar")), 1240 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1241 }, 1242 "affinity-zone-GA-label-changed": { 1243 isExpectedFailure: true, 1244 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelTopologyZone, "bar")), 1245 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaZone, "bar")), 1246 }, 1247 "affinity-region-beta-unchanged": { 1248 isExpectedFailure: false, 1249 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaRegion, "bar")), 1250 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaRegion, "bar")), 1251 }, 1252 "affinity-region-beta-label-to-GA": { 1253 isExpectedFailure: false, 1254 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaRegion, "bar")), 1255 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelTopologyRegion, "bar")), 1256 }, 1257 "affinity-region-beta-label-to-non-GA": { 1258 isExpectedFailure: true, 1259 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaRegion, "bar")), 1260 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1261 }, 1262 "affinity-region-GA-label-changed": { 1263 isExpectedFailure: true, 1264 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelTopologyRegion, "bar")), 1265 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelFailureDomainBetaRegion, "bar")), 1266 }, 1267 "affinity-os-beta-label-unchanged": { 1268 isExpectedFailure: false, 1269 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelOS, "bar")), 1270 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelOS, "bar")), 1271 }, 1272 "affinity-os-beta-label-to-GA": { 1273 isExpectedFailure: false, 1274 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelOS, "bar")), 1275 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelOSStable, "bar")), 1276 }, 1277 "affinity-os-beta-label-to-non-GA": { 1278 isExpectedFailure: true, 1279 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelOS, "bar")), 1280 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1281 }, 1282 "affinity-os-GA-label-changed": { 1283 isExpectedFailure: true, 1284 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelOSStable, "bar")), 1285 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelOS, "bar")), 1286 }, 1287 "affinity-arch-beta-label-unchanged": { 1288 isExpectedFailure: false, 1289 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelArch, "bar")), 1290 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelArch, "bar")), 1291 }, 1292 "affinity-arch-beta-label-to-GA": { 1293 isExpectedFailure: false, 1294 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelArch, "bar")), 1295 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelArchStable, "bar")), 1296 }, 1297 "affinity-arch-beta-label-to-non-GA": { 1298 isExpectedFailure: true, 1299 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelArch, "bar")), 1300 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1301 }, 1302 "affinity-arch-GA-label-changed": { 1303 isExpectedFailure: true, 1304 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelArchStable, "bar")), 1305 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(kubeletapis.LabelArch, "bar")), 1306 }, 1307 "affinity-instanceType-beta-label-unchanged": { 1308 isExpectedFailure: false, 1309 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "bar")), 1310 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "bar")), 1311 }, 1312 "affinity-instanceType-beta-label-to-GA": { 1313 isExpectedFailure: false, 1314 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "bar")), 1315 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceTypeStable, "bar")), 1316 }, 1317 "affinity-instanceType-beta-label-to-non-GA": { 1318 isExpectedFailure: true, 1319 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "bar")), 1320 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1321 }, 1322 "affinity-instanceType-GA-label-changed": { 1323 isExpectedFailure: true, 1324 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceTypeStable, "bar")), 1325 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "bar")), 1326 }, 1327 "affinity-same-terms-expressions-length-beta-to-GA-partially-changed": { 1328 isExpectedFailure: false, 1329 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1330 topologyPair{"foo", "bar"}, 1331 }, { 1332 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1333 topologyPair{v1.LabelFailureDomainBetaRegion, "bar"}, 1334 }, { 1335 topologyPair{kubeletapis.LabelOS, "bar"}, 1336 topologyPair{kubeletapis.LabelArch, "bar"}, 1337 topologyPair{v1.LabelInstanceType, "bar"}, 1338 }, 1339 })), 1340 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1341 topologyPair{"foo", "bar"}, 1342 }, { 1343 topologyPair{v1.LabelTopologyZone, "bar"}, 1344 topologyPair{v1.LabelFailureDomainBetaRegion, "bar"}, 1345 }, { 1346 topologyPair{kubeletapis.LabelOS, "bar"}, 1347 topologyPair{v1.LabelArchStable, "bar"}, 1348 topologyPair{v1.LabelInstanceTypeStable, "bar"}, 1349 }, 1350 })), 1351 }, 1352 "affinity-same-terms-expressions-length-beta-to-non-GA-partially-changed": { 1353 isExpectedFailure: true, 1354 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1355 topologyPair{"foo", "bar"}, 1356 }, { 1357 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1358 topologyPair{v1.LabelFailureDomainBetaRegion, "bar"}, 1359 }, 1360 })), 1361 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1362 topologyPair{"foo", "bar"}, 1363 }, { 1364 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1365 topologyPair{"foo", "bar"}, 1366 }, 1367 })), 1368 }, 1369 "affinity-same-terms-expressions-length-GA-partially-changed": { 1370 isExpectedFailure: true, 1371 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1372 topologyPair{"foo", "bar"}, 1373 }, { 1374 topologyPair{v1.LabelTopologyZone, "bar"}, 1375 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1376 topologyPair{v1.LabelOSStable, "bar"}, 1377 }, 1378 })), 1379 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1380 topologyPair{"foo", "bar"}, 1381 }, { 1382 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1383 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1384 topologyPair{v1.LabelOSStable, "bar"}, 1385 }, 1386 })), 1387 }, 1388 "affinity-same-terms-expressions-length-beta-fully-changed": { 1389 isExpectedFailure: false, 1390 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1391 topologyPair{"foo", "bar"}, 1392 }, { 1393 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1394 topologyPair{v1.LabelFailureDomainBetaRegion, "bar"}, 1395 }, { 1396 topologyPair{kubeletapis.LabelOS, "bar"}, 1397 topologyPair{kubeletapis.LabelArch, "bar"}, 1398 topologyPair{v1.LabelInstanceType, "bar"}, 1399 }, 1400 })), 1401 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1402 topologyPair{"foo", "bar"}, 1403 }, { 1404 topologyPair{v1.LabelTopologyZone, "bar"}, 1405 topologyPair{v1.LabelTopologyRegion, "bar"}, 1406 }, { 1407 topologyPair{v1.LabelOSStable, "bar"}, 1408 topologyPair{v1.LabelArchStable, "bar"}, 1409 topologyPair{v1.LabelInstanceTypeStable, "bar"}, 1410 }, 1411 })), 1412 }, 1413 "affinity-same-terms-expressions-length-beta-GA-mixed-fully-changed": { 1414 isExpectedFailure: true, 1415 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1416 topologyPair{"foo", "bar"}, 1417 }, { 1418 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1419 topologyPair{v1.LabelTopologyZone, "bar"}, 1420 }, 1421 })), 1422 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1423 topologyPair{"foo", "bar"}, 1424 }, { 1425 topologyPair{v1.LabelTopologyZone, "bar"}, 1426 topologyPair{v1.LabelFailureDomainBetaZone, "bar2"}, 1427 }, 1428 })), 1429 }, 1430 "affinity-same-terms-length-different-expressions-length-beta-changed": { 1431 isExpectedFailure: true, 1432 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1433 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1434 }, 1435 })), 1436 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1437 topologyPair{v1.LabelTopologyZone, "bar"}, 1438 topologyPair{v1.LabelFailureDomainBetaRegion, "bar"}, 1439 }, 1440 })), 1441 }, 1442 "affinity-different-terms-expressions-length-beta-changed": { 1443 isExpectedFailure: true, 1444 oldPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1445 topologyPair{v1.LabelFailureDomainBetaZone, "bar"}, 1446 }, 1447 })), 1448 newPV: testVolumeWithNodeAffinity(multipleVolumeNodeAffinity([][]topologyPair{{ 1449 topologyPair{v1.LabelTopologyZone, "bar"}, 1450 }, { 1451 topologyPair{v1.LabelArchStable, "bar"}, 1452 }, 1453 })), 1454 }, 1455 "nil-to-obj": { 1456 isExpectedFailure: false, 1457 oldPV: testVolumeWithNodeAffinity(nil), 1458 newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1459 }, 1460 "obj-to-nil": { 1461 isExpectedFailure: true, 1462 oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), 1463 newPV: testVolumeWithNodeAffinity(nil), 1464 }, 1465 } 1466 1467 for name, scenario := range scenarios { 1468 originalNewPV := scenario.newPV.DeepCopy() 1469 originalOldPV := scenario.oldPV.DeepCopy() 1470 opts := ValidationOptionsForPersistentVolume(scenario.newPV, scenario.oldPV) 1471 errs := ValidatePersistentVolumeUpdate(scenario.newPV, scenario.oldPV, opts) 1472 if len(errs) == 0 && scenario.isExpectedFailure { 1473 t.Errorf("Unexpected success for scenario: %s", name) 1474 } 1475 if len(errs) > 0 && !scenario.isExpectedFailure { 1476 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 1477 } 1478 if diff := cmp.Diff(originalNewPV, scenario.newPV); len(diff) > 0 { 1479 t.Errorf("newPV was modified: %s", diff) 1480 } 1481 if diff := cmp.Diff(originalOldPV, scenario.oldPV); len(diff) > 0 { 1482 t.Errorf("oldPV was modified: %s", diff) 1483 } 1484 } 1485 } 1486 1487 func TestValidatePeristentVolumeAttributesClassUpdate(t *testing.T) { 1488 scenarios := map[string]struct { 1489 isExpectedFailure bool 1490 enableVolumeAttributesClass bool 1491 oldPV *core.PersistentVolume 1492 newPV *core.PersistentVolume 1493 }{ 1494 "nil-nothing-changed": { 1495 isExpectedFailure: false, 1496 enableVolumeAttributesClass: true, 1497 oldPV: testVolumeWithVolumeAttributesClass(nil), 1498 newPV: testVolumeWithVolumeAttributesClass(nil), 1499 }, 1500 "vac-nothing-changed": { 1501 isExpectedFailure: false, 1502 enableVolumeAttributesClass: true, 1503 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1504 newPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1505 }, 1506 "vac-changed": { 1507 isExpectedFailure: false, 1508 enableVolumeAttributesClass: true, 1509 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1510 newPV: testVolumeWithVolumeAttributesClass(ptr.To("bar")), 1511 }, 1512 "nil-to-string": { 1513 isExpectedFailure: false, 1514 enableVolumeAttributesClass: true, 1515 oldPV: testVolumeWithVolumeAttributesClass(nil), 1516 newPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1517 }, 1518 "nil-to-empty-string": { 1519 isExpectedFailure: true, 1520 enableVolumeAttributesClass: true, 1521 oldPV: testVolumeWithVolumeAttributesClass(nil), 1522 newPV: testVolumeWithVolumeAttributesClass(ptr.To("")), 1523 }, 1524 "string-to-nil": { 1525 isExpectedFailure: true, 1526 enableVolumeAttributesClass: true, 1527 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1528 newPV: testVolumeWithVolumeAttributesClass(nil), 1529 }, 1530 "string-to-empty-string": { 1531 isExpectedFailure: true, 1532 enableVolumeAttributesClass: true, 1533 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1534 newPV: testVolumeWithVolumeAttributesClass(ptr.To("")), 1535 }, 1536 "vac-nothing-changed-when-feature-gate-is-off": { 1537 isExpectedFailure: false, 1538 enableVolumeAttributesClass: false, 1539 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1540 newPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1541 }, 1542 "vac-changed-when-feature-gate-is-off": { 1543 isExpectedFailure: true, 1544 enableVolumeAttributesClass: false, 1545 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1546 newPV: testVolumeWithVolumeAttributesClass(ptr.To("bar")), 1547 }, 1548 "nil-to-string-when-feature-gate-is-off": { 1549 isExpectedFailure: true, 1550 enableVolumeAttributesClass: false, 1551 oldPV: testVolumeWithVolumeAttributesClass(nil), 1552 newPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1553 }, 1554 "nil-to-empty-string-when-feature-gate-is-off": { 1555 isExpectedFailure: true, 1556 enableVolumeAttributesClass: false, 1557 oldPV: testVolumeWithVolumeAttributesClass(nil), 1558 newPV: testVolumeWithVolumeAttributesClass(ptr.To("")), 1559 }, 1560 "string-to-nil-when-feature-gate-is-off": { 1561 isExpectedFailure: true, 1562 enableVolumeAttributesClass: false, 1563 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1564 newPV: testVolumeWithVolumeAttributesClass(nil), 1565 }, 1566 "string-to-empty-string-when-feature-gate-is-off": { 1567 isExpectedFailure: true, 1568 enableVolumeAttributesClass: false, 1569 oldPV: testVolumeWithVolumeAttributesClass(ptr.To("foo")), 1570 newPV: testVolumeWithVolumeAttributesClass(ptr.To("")), 1571 }, 1572 } 1573 1574 for name, scenario := range scenarios { 1575 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass) 1576 1577 originalNewPV := scenario.newPV.DeepCopy() 1578 originalOldPV := scenario.oldPV.DeepCopy() 1579 opts := ValidationOptionsForPersistentVolume(scenario.newPV, scenario.oldPV) 1580 errs := ValidatePersistentVolumeUpdate(scenario.newPV, scenario.oldPV, opts) 1581 if len(errs) == 0 && scenario.isExpectedFailure { 1582 t.Errorf("Unexpected success for scenario: %s", name) 1583 } 1584 if len(errs) > 0 && !scenario.isExpectedFailure { 1585 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 1586 } 1587 if diff := cmp.Diff(originalNewPV, scenario.newPV); len(diff) > 0 { 1588 t.Errorf("newPV was modified: %s", diff) 1589 } 1590 if diff := cmp.Diff(originalOldPV, scenario.oldPV); len(diff) > 0 { 1591 t.Errorf("oldPV was modified: %s", diff) 1592 } 1593 } 1594 } 1595 1596 func testVolumeClaim(name string, namespace string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1597 return &core.PersistentVolumeClaim{ 1598 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, 1599 Spec: spec, 1600 } 1601 } 1602 1603 func testVolumeClaimWithStatus( 1604 name, namespace string, 1605 spec core.PersistentVolumeClaimSpec, 1606 status core.PersistentVolumeClaimStatus) *core.PersistentVolumeClaim { 1607 return &core.PersistentVolumeClaim{ 1608 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, 1609 Spec: spec, 1610 Status: status, 1611 } 1612 } 1613 1614 func testVolumeClaimStorageClass(name string, namespace string, annval string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1615 annotations := map[string]string{ 1616 v1.BetaStorageClassAnnotation: annval, 1617 } 1618 1619 return &core.PersistentVolumeClaim{ 1620 ObjectMeta: metav1.ObjectMeta{ 1621 Name: name, 1622 Namespace: namespace, 1623 Annotations: annotations, 1624 }, 1625 Spec: spec, 1626 } 1627 } 1628 1629 func testVolumeClaimAnnotation(name string, namespace string, ann string, annval string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1630 annotations := map[string]string{ 1631 ann: annval, 1632 } 1633 1634 return &core.PersistentVolumeClaim{ 1635 ObjectMeta: metav1.ObjectMeta{ 1636 Name: name, 1637 Namespace: namespace, 1638 Annotations: annotations, 1639 }, 1640 Spec: spec, 1641 } 1642 } 1643 1644 func testVolumeClaimStorageClassInSpec(name, namespace, scName string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1645 spec.StorageClassName = &scName 1646 return &core.PersistentVolumeClaim{ 1647 ObjectMeta: metav1.ObjectMeta{ 1648 Name: name, 1649 Namespace: namespace, 1650 }, 1651 Spec: spec, 1652 } 1653 } 1654 1655 func testVolumeClaimStorageClassNilInSpec(name, namespace string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1656 spec.StorageClassName = nil 1657 return &core.PersistentVolumeClaim{ 1658 ObjectMeta: metav1.ObjectMeta{ 1659 Name: name, 1660 Namespace: namespace, 1661 }, 1662 Spec: spec, 1663 } 1664 } 1665 1666 func testVolumeSnapshotDataSourceInSpec(name string, kind string, apiGroup string) *core.PersistentVolumeClaimSpec { 1667 scName := "csi-plugin" 1668 dataSourceInSpec := core.PersistentVolumeClaimSpec{ 1669 AccessModes: []core.PersistentVolumeAccessMode{ 1670 core.ReadOnlyMany, 1671 }, 1672 Resources: core.VolumeResourceRequirements{ 1673 Requests: core.ResourceList{ 1674 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1675 }, 1676 }, 1677 StorageClassName: &scName, 1678 DataSource: &core.TypedLocalObjectReference{ 1679 APIGroup: &apiGroup, 1680 Kind: kind, 1681 Name: name, 1682 }, 1683 } 1684 1685 return &dataSourceInSpec 1686 } 1687 1688 func TestAlphaVolumeSnapshotDataSource(t *testing.T) { 1689 successTestCases := []core.PersistentVolumeClaimSpec{ 1690 *testVolumeSnapshotDataSourceInSpec("test_snapshot", "VolumeSnapshot", "snapshot.storage.k8s.io"), 1691 } 1692 failedTestCases := []core.PersistentVolumeClaimSpec{ 1693 *testVolumeSnapshotDataSourceInSpec("", "VolumeSnapshot", "snapshot.storage.k8s.io"), 1694 *testVolumeSnapshotDataSourceInSpec("test_snapshot", "", "snapshot.storage.k8s.io"), 1695 } 1696 1697 for _, tc := range successTestCases { 1698 opts := PersistentVolumeClaimSpecValidationOptions{} 1699 if errs := ValidatePersistentVolumeClaimSpec(&tc, field.NewPath("spec"), opts); len(errs) != 0 { 1700 t.Errorf("expected success: %v", errs) 1701 } 1702 } 1703 for _, tc := range failedTestCases { 1704 opts := PersistentVolumeClaimSpecValidationOptions{} 1705 if errs := ValidatePersistentVolumeClaimSpec(&tc, field.NewPath("spec"), opts); len(errs) == 0 { 1706 t.Errorf("expected failure: %v", errs) 1707 } 1708 } 1709 } 1710 1711 func testVolumeClaimStorageClassInAnnotationAndSpec(name, namespace, scNameInAnn, scName string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1712 spec.StorageClassName = &scName 1713 return &core.PersistentVolumeClaim{ 1714 ObjectMeta: metav1.ObjectMeta{ 1715 Name: name, 1716 Namespace: namespace, 1717 Annotations: map[string]string{v1.BetaStorageClassAnnotation: scNameInAnn}, 1718 }, 1719 Spec: spec, 1720 } 1721 } 1722 1723 func testVolumeClaimStorageClassInAnnotationAndNilInSpec(name, namespace, scNameInAnn string, spec core.PersistentVolumeClaimSpec) *core.PersistentVolumeClaim { 1724 spec.StorageClassName = nil 1725 return &core.PersistentVolumeClaim{ 1726 ObjectMeta: metav1.ObjectMeta{ 1727 Name: name, 1728 Namespace: namespace, 1729 Annotations: map[string]string{v1.BetaStorageClassAnnotation: scNameInAnn}, 1730 }, 1731 Spec: spec, 1732 } 1733 } 1734 1735 func testValidatePVC(t *testing.T, ephemeral bool) { 1736 invalidClassName := "-invalid-" 1737 validClassName := "valid" 1738 invalidAPIGroup := "^invalid" 1739 invalidMode := core.PersistentVolumeMode("fakeVolumeMode") 1740 validMode := core.PersistentVolumeFilesystem 1741 goodName := "foo" 1742 goodNS := "ns" 1743 if ephemeral { 1744 // Must be empty for ephemeral inline volumes. 1745 goodName = "" 1746 goodNS = "" 1747 } 1748 goodClaimSpec := core.PersistentVolumeClaimSpec{ 1749 Selector: &metav1.LabelSelector{ 1750 MatchExpressions: []metav1.LabelSelectorRequirement{{ 1751 Key: "key2", 1752 Operator: "Exists", 1753 }}, 1754 }, 1755 AccessModes: []core.PersistentVolumeAccessMode{ 1756 core.ReadWriteOnce, 1757 core.ReadOnlyMany, 1758 }, 1759 Resources: core.VolumeResourceRequirements{ 1760 Requests: core.ResourceList{ 1761 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1762 }, 1763 }, 1764 StorageClassName: &validClassName, 1765 VolumeMode: &validMode, 1766 } 1767 now := metav1.Now() 1768 ten := int64(10) 1769 1770 scenarios := map[string]struct { 1771 isExpectedFailure bool 1772 enableVolumeAttributesClass bool 1773 claim *core.PersistentVolumeClaim 1774 }{ 1775 "good-claim": { 1776 isExpectedFailure: false, 1777 claim: testVolumeClaim(goodName, goodNS, goodClaimSpec), 1778 }, 1779 "missing-name": { 1780 isExpectedFailure: !ephemeral, 1781 claim: testVolumeClaim("", goodNS, goodClaimSpec), 1782 }, 1783 "missing-namespace": { 1784 isExpectedFailure: !ephemeral, 1785 claim: testVolumeClaim(goodName, "", goodClaimSpec), 1786 }, 1787 "with-generate-name": { 1788 isExpectedFailure: ephemeral, 1789 claim: func() *core.PersistentVolumeClaim { 1790 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1791 claim.GenerateName = "pvc-" 1792 return claim 1793 }(), 1794 }, 1795 "with-uid": { 1796 isExpectedFailure: ephemeral, 1797 claim: func() *core.PersistentVolumeClaim { 1798 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1799 claim.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d" 1800 return claim 1801 }(), 1802 }, 1803 "with-resource-version": { 1804 isExpectedFailure: ephemeral, 1805 claim: func() *core.PersistentVolumeClaim { 1806 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1807 claim.ResourceVersion = "1" 1808 return claim 1809 }(), 1810 }, 1811 "with-generation": { 1812 isExpectedFailure: ephemeral, 1813 claim: func() *core.PersistentVolumeClaim { 1814 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1815 claim.Generation = 100 1816 return claim 1817 }(), 1818 }, 1819 "with-creation-timestamp": { 1820 isExpectedFailure: ephemeral, 1821 claim: func() *core.PersistentVolumeClaim { 1822 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1823 claim.CreationTimestamp = now 1824 return claim 1825 }(), 1826 }, 1827 "with-deletion-grace-period-seconds": { 1828 isExpectedFailure: ephemeral, 1829 claim: func() *core.PersistentVolumeClaim { 1830 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1831 claim.DeletionGracePeriodSeconds = &ten 1832 return claim 1833 }(), 1834 }, 1835 "with-owner-references": { 1836 isExpectedFailure: ephemeral, 1837 claim: func() *core.PersistentVolumeClaim { 1838 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1839 claim.OwnerReferences = []metav1.OwnerReference{{ 1840 APIVersion: "v1", 1841 Kind: "pod", 1842 Name: "foo", 1843 UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d", 1844 }, 1845 } 1846 return claim 1847 }(), 1848 }, 1849 "with-finalizers": { 1850 isExpectedFailure: ephemeral, 1851 claim: func() *core.PersistentVolumeClaim { 1852 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1853 claim.Finalizers = []string{ 1854 "example.com/foo", 1855 } 1856 return claim 1857 }(), 1858 }, 1859 "with-managed-fields": { 1860 isExpectedFailure: ephemeral, 1861 claim: func() *core.PersistentVolumeClaim { 1862 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1863 claim.ManagedFields = []metav1.ManagedFieldsEntry{{ 1864 FieldsType: "FieldsV1", 1865 Operation: "Apply", 1866 APIVersion: "apps/v1", 1867 Manager: "foo", 1868 }, 1869 } 1870 return claim 1871 }(), 1872 }, 1873 "with-good-labels": { 1874 claim: func() *core.PersistentVolumeClaim { 1875 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1876 claim.Labels = map[string]string{ 1877 "apps.kubernetes.io/name": "test", 1878 } 1879 return claim 1880 }(), 1881 }, 1882 "with-bad-labels": { 1883 isExpectedFailure: true, 1884 claim: func() *core.PersistentVolumeClaim { 1885 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1886 claim.Labels = map[string]string{ 1887 "hello-world": "hyphen not allowed", 1888 } 1889 return claim 1890 }(), 1891 }, 1892 "with-good-annotations": { 1893 claim: func() *core.PersistentVolumeClaim { 1894 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1895 claim.Labels = map[string]string{ 1896 "foo": "bar", 1897 } 1898 return claim 1899 }(), 1900 }, 1901 "with-bad-annotations": { 1902 isExpectedFailure: true, 1903 claim: func() *core.PersistentVolumeClaim { 1904 claim := testVolumeClaim(goodName, goodNS, goodClaimSpec) 1905 claim.Labels = map[string]string{ 1906 "hello-world": "hyphen not allowed", 1907 } 1908 return claim 1909 }(), 1910 }, 1911 "with-read-write-once-pod": { 1912 isExpectedFailure: false, 1913 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1914 AccessModes: []core.PersistentVolumeAccessMode{"ReadWriteOncePod"}, 1915 Resources: core.VolumeResourceRequirements{ 1916 Requests: core.ResourceList{ 1917 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1918 }, 1919 }, 1920 }), 1921 }, 1922 "with-read-write-once-pod-and-others": { 1923 isExpectedFailure: true, 1924 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1925 AccessModes: []core.PersistentVolumeAccessMode{"ReadWriteOncePod", "ReadWriteMany"}, 1926 Resources: core.VolumeResourceRequirements{ 1927 Requests: core.ResourceList{ 1928 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1929 }, 1930 }, 1931 }), 1932 }, 1933 "invalid-claim-zero-capacity": { 1934 isExpectedFailure: true, 1935 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1936 Selector: &metav1.LabelSelector{ 1937 MatchExpressions: []metav1.LabelSelectorRequirement{{ 1938 Key: "key2", 1939 Operator: "Exists", 1940 }}, 1941 }, 1942 AccessModes: []core.PersistentVolumeAccessMode{ 1943 core.ReadWriteOnce, 1944 core.ReadOnlyMany, 1945 }, 1946 Resources: core.VolumeResourceRequirements{ 1947 Requests: core.ResourceList{ 1948 core.ResourceName(core.ResourceStorage): resource.MustParse("0G"), 1949 }, 1950 }, 1951 StorageClassName: &validClassName, 1952 }), 1953 }, 1954 "invalid-label-selector": { 1955 isExpectedFailure: true, 1956 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1957 Selector: &metav1.LabelSelector{ 1958 MatchExpressions: []metav1.LabelSelectorRequirement{{ 1959 Key: "key2", 1960 Operator: "InvalidOp", 1961 Values: []string{"value1", "value2"}, 1962 }}, 1963 }, 1964 AccessModes: []core.PersistentVolumeAccessMode{ 1965 core.ReadWriteOnce, 1966 core.ReadOnlyMany, 1967 }, 1968 Resources: core.VolumeResourceRequirements{ 1969 Requests: core.ResourceList{ 1970 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1971 }, 1972 }, 1973 }), 1974 }, 1975 "invalid-accessmode": { 1976 isExpectedFailure: true, 1977 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1978 AccessModes: []core.PersistentVolumeAccessMode{"fakemode"}, 1979 Resources: core.VolumeResourceRequirements{ 1980 Requests: core.ResourceList{ 1981 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1982 }, 1983 }, 1984 }), 1985 }, 1986 "no-access-modes": { 1987 isExpectedFailure: true, 1988 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1989 Resources: core.VolumeResourceRequirements{ 1990 Requests: core.ResourceList{ 1991 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 1992 }, 1993 }, 1994 }), 1995 }, 1996 "no-resource-requests": { 1997 isExpectedFailure: true, 1998 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 1999 AccessModes: []core.PersistentVolumeAccessMode{ 2000 core.ReadWriteOnce, 2001 }, 2002 }), 2003 }, 2004 "invalid-resource-requests": { 2005 isExpectedFailure: true, 2006 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2007 AccessModes: []core.PersistentVolumeAccessMode{ 2008 core.ReadWriteOnce, 2009 }, 2010 Resources: core.VolumeResourceRequirements{ 2011 Requests: core.ResourceList{ 2012 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 2013 }, 2014 }, 2015 }), 2016 }, 2017 "negative-storage-request": { 2018 isExpectedFailure: true, 2019 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2020 Selector: &metav1.LabelSelector{ 2021 MatchExpressions: []metav1.LabelSelectorRequirement{{ 2022 Key: "key2", 2023 Operator: "Exists", 2024 }}, 2025 }, 2026 AccessModes: []core.PersistentVolumeAccessMode{ 2027 core.ReadWriteOnce, 2028 core.ReadOnlyMany, 2029 }, 2030 Resources: core.VolumeResourceRequirements{ 2031 Requests: core.ResourceList{ 2032 core.ResourceName(core.ResourceStorage): resource.MustParse("-10G"), 2033 }, 2034 }, 2035 }), 2036 }, 2037 "zero-storage-request": { 2038 isExpectedFailure: true, 2039 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2040 Selector: &metav1.LabelSelector{ 2041 MatchExpressions: []metav1.LabelSelectorRequirement{{ 2042 Key: "key2", 2043 Operator: "Exists", 2044 }}, 2045 }, 2046 AccessModes: []core.PersistentVolumeAccessMode{ 2047 core.ReadWriteOnce, 2048 core.ReadOnlyMany, 2049 }, 2050 Resources: core.VolumeResourceRequirements{ 2051 Requests: core.ResourceList{ 2052 core.ResourceName(core.ResourceStorage): resource.MustParse("0G"), 2053 }, 2054 }, 2055 }), 2056 }, 2057 "invalid-storage-class-name": { 2058 isExpectedFailure: true, 2059 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2060 Selector: &metav1.LabelSelector{ 2061 MatchExpressions: []metav1.LabelSelectorRequirement{{ 2062 Key: "key2", 2063 Operator: "Exists", 2064 }}, 2065 }, 2066 AccessModes: []core.PersistentVolumeAccessMode{ 2067 core.ReadWriteOnce, 2068 core.ReadOnlyMany, 2069 }, 2070 Resources: core.VolumeResourceRequirements{ 2071 Requests: core.ResourceList{ 2072 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2073 }, 2074 }, 2075 StorageClassName: &invalidClassName, 2076 }), 2077 }, 2078 "invalid-volume-mode": { 2079 isExpectedFailure: true, 2080 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2081 AccessModes: []core.PersistentVolumeAccessMode{ 2082 core.ReadWriteOnce, 2083 core.ReadOnlyMany, 2084 }, 2085 Resources: core.VolumeResourceRequirements{ 2086 Requests: core.ResourceList{ 2087 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2088 }, 2089 }, 2090 VolumeMode: &invalidMode, 2091 }), 2092 }, 2093 "mismatch-data-source-and-ref": { 2094 isExpectedFailure: true, 2095 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2096 AccessModes: []core.PersistentVolumeAccessMode{ 2097 core.ReadWriteOnce, 2098 }, 2099 Resources: core.VolumeResourceRequirements{ 2100 Requests: core.ResourceList{ 2101 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2102 }, 2103 }, 2104 DataSource: &core.TypedLocalObjectReference{ 2105 Kind: "PersistentVolumeClaim", 2106 Name: "pvc1", 2107 }, 2108 DataSourceRef: &core.TypedObjectReference{ 2109 Kind: "PersistentVolumeClaim", 2110 Name: "pvc2", 2111 }, 2112 }), 2113 }, 2114 "invaild-apigroup-in-data-source": { 2115 isExpectedFailure: true, 2116 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2117 AccessModes: []core.PersistentVolumeAccessMode{ 2118 core.ReadWriteOnce, 2119 }, 2120 Resources: core.VolumeResourceRequirements{ 2121 Requests: core.ResourceList{ 2122 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2123 }, 2124 }, 2125 DataSource: &core.TypedLocalObjectReference{ 2126 APIGroup: &invalidAPIGroup, 2127 Kind: "Foo", 2128 Name: "foo1", 2129 }, 2130 }), 2131 }, 2132 "invaild-apigroup-in-data-source-ref": { 2133 isExpectedFailure: true, 2134 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2135 AccessModes: []core.PersistentVolumeAccessMode{ 2136 core.ReadWriteOnce, 2137 }, 2138 Resources: core.VolumeResourceRequirements{ 2139 Requests: core.ResourceList{ 2140 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2141 }, 2142 }, 2143 DataSourceRef: &core.TypedObjectReference{ 2144 APIGroup: &invalidAPIGroup, 2145 Kind: "Foo", 2146 Name: "foo1", 2147 }, 2148 }), 2149 }, 2150 "invalid-volume-attributes-class-name": { 2151 isExpectedFailure: true, 2152 enableVolumeAttributesClass: true, 2153 claim: testVolumeClaim(goodName, goodNS, core.PersistentVolumeClaimSpec{ 2154 Selector: &metav1.LabelSelector{ 2155 MatchExpressions: []metav1.LabelSelectorRequirement{{ 2156 Key: "key2", 2157 Operator: "Exists", 2158 }}, 2159 }, 2160 AccessModes: []core.PersistentVolumeAccessMode{ 2161 core.ReadWriteOnce, 2162 core.ReadOnlyMany, 2163 }, 2164 Resources: core.VolumeResourceRequirements{ 2165 Requests: core.ResourceList{ 2166 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2167 }, 2168 }, 2169 VolumeAttributesClassName: &invalidClassName, 2170 }), 2171 }, 2172 } 2173 2174 for name, scenario := range scenarios { 2175 t.Run(name, func(t *testing.T) { 2176 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass) 2177 2178 var errs field.ErrorList 2179 if ephemeral { 2180 volumes := []core.Volume{{ 2181 Name: "foo", 2182 VolumeSource: core.VolumeSource{ 2183 Ephemeral: &core.EphemeralVolumeSource{ 2184 VolumeClaimTemplate: &core.PersistentVolumeClaimTemplate{ 2185 ObjectMeta: scenario.claim.ObjectMeta, 2186 Spec: scenario.claim.Spec, 2187 }, 2188 }, 2189 }, 2190 }, 2191 } 2192 opts := PodValidationOptions{} 2193 _, errs = ValidateVolumes(volumes, nil, field.NewPath(""), opts) 2194 } else { 2195 opts := ValidationOptionsForPersistentVolumeClaim(scenario.claim, nil) 2196 errs = ValidatePersistentVolumeClaim(scenario.claim, opts) 2197 } 2198 if len(errs) == 0 && scenario.isExpectedFailure { 2199 t.Error("Unexpected success for scenario") 2200 } 2201 if len(errs) > 0 && !scenario.isExpectedFailure { 2202 t.Errorf("Unexpected failure: %+v", errs) 2203 } 2204 }) 2205 } 2206 } 2207 2208 func TestValidatePersistentVolumeClaim(t *testing.T) { 2209 testValidatePVC(t, false) 2210 } 2211 2212 func TestValidateEphemeralVolume(t *testing.T) { 2213 testValidatePVC(t, true) 2214 } 2215 2216 func TestAlphaPVVolumeModeUpdate(t *testing.T) { 2217 block := core.PersistentVolumeBlock 2218 file := core.PersistentVolumeFilesystem 2219 2220 scenarios := map[string]struct { 2221 isExpectedFailure bool 2222 oldPV *core.PersistentVolume 2223 newPV *core.PersistentVolume 2224 }{ 2225 "valid-update-volume-mode-block-to-block": { 2226 isExpectedFailure: false, 2227 oldPV: createTestVolModePV(&block), 2228 newPV: createTestVolModePV(&block), 2229 }, 2230 "valid-update-volume-mode-file-to-file": { 2231 isExpectedFailure: false, 2232 oldPV: createTestVolModePV(&file), 2233 newPV: createTestVolModePV(&file), 2234 }, 2235 "invalid-update-volume-mode-to-block": { 2236 isExpectedFailure: true, 2237 oldPV: createTestVolModePV(&file), 2238 newPV: createTestVolModePV(&block), 2239 }, 2240 "invalid-update-volume-mode-to-file": { 2241 isExpectedFailure: true, 2242 oldPV: createTestVolModePV(&block), 2243 newPV: createTestVolModePV(&file), 2244 }, 2245 "invalid-update-volume-mode-nil-to-file": { 2246 isExpectedFailure: true, 2247 oldPV: createTestVolModePV(nil), 2248 newPV: createTestVolModePV(&file), 2249 }, 2250 "invalid-update-volume-mode-nil-to-block": { 2251 isExpectedFailure: true, 2252 oldPV: createTestVolModePV(nil), 2253 newPV: createTestVolModePV(&block), 2254 }, 2255 "invalid-update-volume-mode-file-to-nil": { 2256 isExpectedFailure: true, 2257 oldPV: createTestVolModePV(&file), 2258 newPV: createTestVolModePV(nil), 2259 }, 2260 "invalid-update-volume-mode-block-to-nil": { 2261 isExpectedFailure: true, 2262 oldPV: createTestVolModePV(&block), 2263 newPV: createTestVolModePV(nil), 2264 }, 2265 "invalid-update-volume-mode-nil-to-nil": { 2266 isExpectedFailure: false, 2267 oldPV: createTestVolModePV(nil), 2268 newPV: createTestVolModePV(nil), 2269 }, 2270 "invalid-update-volume-mode-empty-to-mode": { 2271 isExpectedFailure: true, 2272 oldPV: createTestPV(), 2273 newPV: createTestVolModePV(&block), 2274 }, 2275 } 2276 2277 for name, scenario := range scenarios { 2278 t.Run(name, func(t *testing.T) { 2279 opts := ValidationOptionsForPersistentVolume(scenario.newPV, scenario.oldPV) 2280 // ensure we have a resource version specified for updates 2281 errs := ValidatePersistentVolumeUpdate(scenario.newPV, scenario.oldPV, opts) 2282 if len(errs) == 0 && scenario.isExpectedFailure { 2283 t.Errorf("Unexpected success for scenario: %s", name) 2284 } 2285 if len(errs) > 0 && !scenario.isExpectedFailure { 2286 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 2287 } 2288 }) 2289 } 2290 } 2291 2292 func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { 2293 block := core.PersistentVolumeBlock 2294 file := core.PersistentVolumeFilesystem 2295 invaildAPIGroup := "^invalid" 2296 2297 validClaim := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2298 AccessModes: []core.PersistentVolumeAccessMode{ 2299 core.ReadWriteOnce, 2300 core.ReadOnlyMany, 2301 }, 2302 Resources: core.VolumeResourceRequirements{ 2303 Requests: core.ResourceList{ 2304 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2305 }, 2306 }, 2307 }, core.PersistentVolumeClaimStatus{ 2308 Phase: core.ClaimBound, 2309 }) 2310 2311 validClaimStorageClass := testVolumeClaimStorageClass("foo", "ns", "fast", core.PersistentVolumeClaimSpec{ 2312 AccessModes: []core.PersistentVolumeAccessMode{ 2313 core.ReadOnlyMany, 2314 }, 2315 Resources: core.VolumeResourceRequirements{ 2316 Requests: core.ResourceList{ 2317 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2318 }, 2319 }, 2320 }) 2321 validClaimAnnotation := testVolumeClaimAnnotation("foo", "ns", "description", "foo-description", core.PersistentVolumeClaimSpec{ 2322 AccessModes: []core.PersistentVolumeAccessMode{ 2323 core.ReadOnlyMany, 2324 }, 2325 Resources: core.VolumeResourceRequirements{ 2326 Requests: core.ResourceList{ 2327 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2328 }, 2329 }, 2330 }) 2331 validUpdateClaim := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2332 AccessModes: []core.PersistentVolumeAccessMode{ 2333 core.ReadWriteOnce, 2334 core.ReadOnlyMany, 2335 }, 2336 Resources: core.VolumeResourceRequirements{ 2337 Requests: core.ResourceList{ 2338 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2339 }, 2340 }, 2341 VolumeName: "volume", 2342 }) 2343 invalidUpdateClaimResources := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2344 AccessModes: []core.PersistentVolumeAccessMode{ 2345 core.ReadWriteOnce, 2346 core.ReadOnlyMany, 2347 }, 2348 Resources: core.VolumeResourceRequirements{ 2349 Requests: core.ResourceList{ 2350 core.ResourceName(core.ResourceStorage): resource.MustParse("20G"), 2351 }, 2352 }, 2353 VolumeName: "volume", 2354 }) 2355 invalidUpdateClaimAccessModes := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2356 AccessModes: []core.PersistentVolumeAccessMode{ 2357 core.ReadWriteOnce, 2358 }, 2359 Resources: core.VolumeResourceRequirements{ 2360 Requests: core.ResourceList{ 2361 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2362 }, 2363 }, 2364 VolumeName: "volume", 2365 }) 2366 validClaimVolumeModeFile := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2367 AccessModes: []core.PersistentVolumeAccessMode{ 2368 core.ReadWriteOnce, 2369 }, 2370 VolumeMode: &file, 2371 Resources: core.VolumeResourceRequirements{ 2372 Requests: core.ResourceList{ 2373 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2374 }, 2375 }, 2376 VolumeName: "volume", 2377 }) 2378 validClaimVolumeModeBlock := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2379 AccessModes: []core.PersistentVolumeAccessMode{ 2380 core.ReadWriteOnce, 2381 }, 2382 VolumeMode: &block, 2383 Resources: core.VolumeResourceRequirements{ 2384 Requests: core.ResourceList{ 2385 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2386 }, 2387 }, 2388 VolumeName: "volume", 2389 }) 2390 invalidClaimVolumeModeNil := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2391 AccessModes: []core.PersistentVolumeAccessMode{ 2392 core.ReadWriteOnce, 2393 }, 2394 VolumeMode: nil, 2395 Resources: core.VolumeResourceRequirements{ 2396 Requests: core.ResourceList{ 2397 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2398 }, 2399 }, 2400 VolumeName: "volume", 2401 }) 2402 invalidUpdateClaimStorageClass := testVolumeClaimStorageClass("foo", "ns", "fast2", core.PersistentVolumeClaimSpec{ 2403 AccessModes: []core.PersistentVolumeAccessMode{ 2404 core.ReadOnlyMany, 2405 }, 2406 Resources: core.VolumeResourceRequirements{ 2407 Requests: core.ResourceList{ 2408 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2409 }, 2410 }, 2411 VolumeName: "volume", 2412 }) 2413 validUpdateClaimMutableAnnotation := testVolumeClaimAnnotation("foo", "ns", "description", "updated-or-added-foo-description", core.PersistentVolumeClaimSpec{ 2414 AccessModes: []core.PersistentVolumeAccessMode{ 2415 core.ReadOnlyMany, 2416 }, 2417 Resources: core.VolumeResourceRequirements{ 2418 Requests: core.ResourceList{ 2419 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2420 }, 2421 }, 2422 VolumeName: "volume", 2423 }) 2424 validAddClaimAnnotation := testVolumeClaimAnnotation("foo", "ns", "description", "updated-or-added-foo-description", core.PersistentVolumeClaimSpec{ 2425 AccessModes: []core.PersistentVolumeAccessMode{ 2426 core.ReadWriteOnce, 2427 core.ReadOnlyMany, 2428 }, 2429 Resources: core.VolumeResourceRequirements{ 2430 Requests: core.ResourceList{ 2431 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2432 }, 2433 }, 2434 VolumeName: "volume", 2435 }) 2436 validSizeUpdate := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2437 AccessModes: []core.PersistentVolumeAccessMode{ 2438 core.ReadWriteOnce, 2439 core.ReadOnlyMany, 2440 }, 2441 Resources: core.VolumeResourceRequirements{ 2442 Requests: core.ResourceList{ 2443 core.ResourceName(core.ResourceStorage): resource.MustParse("15G"), 2444 }, 2445 }, 2446 }, core.PersistentVolumeClaimStatus{ 2447 Phase: core.ClaimBound, 2448 }) 2449 2450 invalidSizeUpdate := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2451 AccessModes: []core.PersistentVolumeAccessMode{ 2452 core.ReadWriteOnce, 2453 core.ReadOnlyMany, 2454 }, 2455 Resources: core.VolumeResourceRequirements{ 2456 Requests: core.ResourceList{ 2457 core.ResourceName(core.ResourceStorage): resource.MustParse("5G"), 2458 }, 2459 }, 2460 }, core.PersistentVolumeClaimStatus{ 2461 Phase: core.ClaimBound, 2462 }) 2463 2464 unboundSizeUpdate := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2465 AccessModes: []core.PersistentVolumeAccessMode{ 2466 core.ReadWriteOnce, 2467 core.ReadOnlyMany, 2468 }, 2469 Resources: core.VolumeResourceRequirements{ 2470 Requests: core.ResourceList{ 2471 core.ResourceName(core.ResourceStorage): resource.MustParse("12G"), 2472 }, 2473 }, 2474 }, core.PersistentVolumeClaimStatus{ 2475 Phase: core.ClaimPending, 2476 }) 2477 2478 validClaimStorageClassInSpec := testVolumeClaimStorageClassInSpec("foo", "ns", "fast", core.PersistentVolumeClaimSpec{ 2479 AccessModes: []core.PersistentVolumeAccessMode{ 2480 core.ReadOnlyMany, 2481 }, 2482 Resources: core.VolumeResourceRequirements{ 2483 Requests: core.ResourceList{ 2484 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2485 }, 2486 }, 2487 }) 2488 2489 validClaimStorageClassInSpecChanged := testVolumeClaimStorageClassInSpec("foo", "ns", "fast2", core.PersistentVolumeClaimSpec{ 2490 AccessModes: []core.PersistentVolumeAccessMode{ 2491 core.ReadOnlyMany, 2492 }, 2493 Resources: core.VolumeResourceRequirements{ 2494 Requests: core.ResourceList{ 2495 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2496 }, 2497 }, 2498 }) 2499 2500 validClaimStorageClassNil := testVolumeClaimStorageClassNilInSpec("foo", "ns", core.PersistentVolumeClaimSpec{ 2501 AccessModes: []core.PersistentVolumeAccessMode{ 2502 core.ReadOnlyMany, 2503 }, 2504 Resources: core.VolumeResourceRequirements{ 2505 Requests: core.ResourceList{ 2506 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2507 }, 2508 }, 2509 }) 2510 2511 invalidClaimStorageClassInSpec := testVolumeClaimStorageClassInSpec("foo", "ns", "fast2", core.PersistentVolumeClaimSpec{ 2512 AccessModes: []core.PersistentVolumeAccessMode{ 2513 core.ReadOnlyMany, 2514 }, 2515 Resources: core.VolumeResourceRequirements{ 2516 Requests: core.ResourceList{ 2517 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2518 }, 2519 }, 2520 }) 2521 2522 validClaimStorageClassInAnnotationAndSpec := testVolumeClaimStorageClassInAnnotationAndSpec( 2523 "foo", "ns", "fast", "fast", core.PersistentVolumeClaimSpec{ 2524 AccessModes: []core.PersistentVolumeAccessMode{ 2525 core.ReadOnlyMany, 2526 }, 2527 Resources: core.VolumeResourceRequirements{ 2528 Requests: core.ResourceList{ 2529 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2530 }, 2531 }, 2532 }) 2533 2534 validClaimStorageClassInAnnotationAndNilInSpec := testVolumeClaimStorageClassInAnnotationAndNilInSpec( 2535 "foo", "ns", "fast", core.PersistentVolumeClaimSpec{ 2536 AccessModes: []core.PersistentVolumeAccessMode{ 2537 core.ReadOnlyMany, 2538 }, 2539 Resources: core.VolumeResourceRequirements{ 2540 Requests: core.ResourceList{ 2541 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2542 }, 2543 }, 2544 }) 2545 2546 invalidClaimStorageClassInAnnotationAndSpec := testVolumeClaimStorageClassInAnnotationAndSpec( 2547 "foo", "ns", "fast2", "fast", core.PersistentVolumeClaimSpec{ 2548 AccessModes: []core.PersistentVolumeAccessMode{ 2549 core.ReadOnlyMany, 2550 }, 2551 Resources: core.VolumeResourceRequirements{ 2552 Requests: core.ResourceList{ 2553 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2554 }, 2555 }, 2556 }) 2557 2558 validClaimRWOPAccessMode := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2559 AccessModes: []core.PersistentVolumeAccessMode{ 2560 core.ReadWriteOncePod, 2561 }, 2562 Resources: core.VolumeResourceRequirements{ 2563 Requests: core.ResourceList{ 2564 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2565 }, 2566 }, 2567 VolumeName: "volume", 2568 }) 2569 2570 validClaimRWOPAccessModeAddAnnotation := testVolumeClaimAnnotation("foo", "ns", "description", "updated-or-added-foo-description", core.PersistentVolumeClaimSpec{ 2571 AccessModes: []core.PersistentVolumeAccessMode{ 2572 core.ReadWriteOncePod, 2573 }, 2574 Resources: core.VolumeResourceRequirements{ 2575 Requests: core.ResourceList{ 2576 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2577 }, 2578 }, 2579 VolumeName: "volume", 2580 }) 2581 validClaimShrinkInitial := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2582 AccessModes: []core.PersistentVolumeAccessMode{ 2583 core.ReadWriteOnce, 2584 core.ReadOnlyMany, 2585 }, 2586 Resources: core.VolumeResourceRequirements{ 2587 Requests: core.ResourceList{ 2588 core.ResourceName(core.ResourceStorage): resource.MustParse("15G"), 2589 }, 2590 }, 2591 }, core.PersistentVolumeClaimStatus{ 2592 Phase: core.ClaimBound, 2593 Capacity: core.ResourceList{ 2594 core.ResourceStorage: resource.MustParse("10G"), 2595 }, 2596 }) 2597 2598 unboundShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2599 AccessModes: []core.PersistentVolumeAccessMode{ 2600 core.ReadWriteOnce, 2601 core.ReadOnlyMany, 2602 }, 2603 Resources: core.VolumeResourceRequirements{ 2604 Requests: core.ResourceList{ 2605 core.ResourceName(core.ResourceStorage): resource.MustParse("12G"), 2606 }, 2607 }, 2608 }, core.PersistentVolumeClaimStatus{ 2609 Phase: core.ClaimPending, 2610 Capacity: core.ResourceList{ 2611 core.ResourceStorage: resource.MustParse("10G"), 2612 }, 2613 }) 2614 2615 validClaimShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2616 AccessModes: []core.PersistentVolumeAccessMode{ 2617 core.ReadWriteOnce, 2618 core.ReadOnlyMany, 2619 }, 2620 Resources: core.VolumeResourceRequirements{ 2621 Requests: core.ResourceList{ 2622 core.ResourceStorage: resource.MustParse("13G"), 2623 }, 2624 }, 2625 }, core.PersistentVolumeClaimStatus{ 2626 Phase: core.ClaimBound, 2627 Capacity: core.ResourceList{ 2628 core.ResourceStorage: resource.MustParse("10G"), 2629 }, 2630 }) 2631 2632 invalidShrinkToStatus := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2633 AccessModes: []core.PersistentVolumeAccessMode{ 2634 core.ReadWriteOnce, 2635 core.ReadOnlyMany, 2636 }, 2637 Resources: core.VolumeResourceRequirements{ 2638 Requests: core.ResourceList{ 2639 core.ResourceStorage: resource.MustParse("10G"), 2640 }, 2641 }, 2642 }, core.PersistentVolumeClaimStatus{ 2643 Phase: core.ClaimBound, 2644 Capacity: core.ResourceList{ 2645 core.ResourceStorage: resource.MustParse("10G"), 2646 }, 2647 }) 2648 2649 invalidClaimShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2650 AccessModes: []core.PersistentVolumeAccessMode{ 2651 core.ReadWriteOnce, 2652 core.ReadOnlyMany, 2653 }, 2654 Resources: core.VolumeResourceRequirements{ 2655 Requests: core.ResourceList{ 2656 core.ResourceStorage: resource.MustParse("3G"), 2657 }, 2658 }, 2659 }, core.PersistentVolumeClaimStatus{ 2660 Phase: core.ClaimBound, 2661 Capacity: core.ResourceList{ 2662 core.ResourceStorage: resource.MustParse("10G"), 2663 }, 2664 }) 2665 2666 invalidClaimDataSourceAPIGroup := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2667 AccessModes: []core.PersistentVolumeAccessMode{ 2668 core.ReadWriteOnce, 2669 }, 2670 VolumeMode: &file, 2671 Resources: core.VolumeResourceRequirements{ 2672 Requests: core.ResourceList{ 2673 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2674 }, 2675 }, 2676 VolumeName: "volume", 2677 DataSource: &core.TypedLocalObjectReference{ 2678 APIGroup: &invaildAPIGroup, 2679 Kind: "Foo", 2680 Name: "foo", 2681 }, 2682 }) 2683 2684 invalidClaimDataSourceRefAPIGroup := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 2685 AccessModes: []core.PersistentVolumeAccessMode{ 2686 core.ReadWriteOnce, 2687 }, 2688 VolumeMode: &file, 2689 Resources: core.VolumeResourceRequirements{ 2690 Requests: core.ResourceList{ 2691 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2692 }, 2693 }, 2694 VolumeName: "volume", 2695 DataSourceRef: &core.TypedObjectReference{ 2696 APIGroup: &invaildAPIGroup, 2697 Kind: "Foo", 2698 Name: "foo", 2699 }, 2700 }) 2701 2702 validClaimNilVolumeAttributesClass := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2703 AccessModes: []core.PersistentVolumeAccessMode{ 2704 core.ReadWriteOnce, 2705 core.ReadOnlyMany, 2706 }, 2707 Resources: core.VolumeResourceRequirements{ 2708 Requests: core.ResourceList{ 2709 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2710 }, 2711 }, 2712 }, core.PersistentVolumeClaimStatus{ 2713 Phase: core.ClaimBound, 2714 }) 2715 validClaimEmptyVolumeAttributesClass := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2716 VolumeAttributesClassName: utilpointer.String(""), 2717 AccessModes: []core.PersistentVolumeAccessMode{ 2718 core.ReadWriteOnce, 2719 core.ReadOnlyMany, 2720 }, 2721 Resources: core.VolumeResourceRequirements{ 2722 Requests: core.ResourceList{ 2723 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2724 }, 2725 }, 2726 }, core.PersistentVolumeClaimStatus{ 2727 Phase: core.ClaimBound, 2728 }) 2729 validClaimVolumeAttributesClass1 := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2730 VolumeAttributesClassName: utilpointer.String("vac1"), 2731 AccessModes: []core.PersistentVolumeAccessMode{ 2732 core.ReadWriteOnce, 2733 core.ReadOnlyMany, 2734 }, 2735 Resources: core.VolumeResourceRequirements{ 2736 Requests: core.ResourceList{ 2737 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2738 }, 2739 }, 2740 }, core.PersistentVolumeClaimStatus{ 2741 Phase: core.ClaimBound, 2742 }) 2743 validClaimVolumeAttributesClass2 := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 2744 VolumeAttributesClassName: utilpointer.String("vac2"), 2745 AccessModes: []core.PersistentVolumeAccessMode{ 2746 core.ReadWriteOnce, 2747 core.ReadOnlyMany, 2748 }, 2749 Resources: core.VolumeResourceRequirements{ 2750 Requests: core.ResourceList{ 2751 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 2752 }, 2753 }, 2754 }, core.PersistentVolumeClaimStatus{ 2755 Phase: core.ClaimBound, 2756 }) 2757 2758 scenarios := map[string]struct { 2759 isExpectedFailure bool 2760 oldClaim *core.PersistentVolumeClaim 2761 newClaim *core.PersistentVolumeClaim 2762 enableRecoverFromExpansion bool 2763 enableVolumeAttributesClass bool 2764 }{ 2765 "valid-update-volumeName-only": { 2766 isExpectedFailure: false, 2767 oldClaim: validClaim, 2768 newClaim: validUpdateClaim, 2769 }, 2770 "valid-no-op-update": { 2771 isExpectedFailure: false, 2772 oldClaim: validUpdateClaim, 2773 newClaim: validUpdateClaim, 2774 }, 2775 "invalid-update-change-resources-on-bound-claim": { 2776 isExpectedFailure: true, 2777 oldClaim: validUpdateClaim, 2778 newClaim: invalidUpdateClaimResources, 2779 }, 2780 "invalid-update-change-access-modes-on-bound-claim": { 2781 isExpectedFailure: true, 2782 oldClaim: validUpdateClaim, 2783 newClaim: invalidUpdateClaimAccessModes, 2784 }, 2785 "valid-update-volume-mode-block-to-block": { 2786 isExpectedFailure: false, 2787 oldClaim: validClaimVolumeModeBlock, 2788 newClaim: validClaimVolumeModeBlock, 2789 }, 2790 "valid-update-volume-mode-file-to-file": { 2791 isExpectedFailure: false, 2792 oldClaim: validClaimVolumeModeFile, 2793 newClaim: validClaimVolumeModeFile, 2794 }, 2795 "invalid-update-volume-mode-to-block": { 2796 isExpectedFailure: true, 2797 oldClaim: validClaimVolumeModeFile, 2798 newClaim: validClaimVolumeModeBlock, 2799 }, 2800 "invalid-update-volume-mode-to-file": { 2801 isExpectedFailure: true, 2802 oldClaim: validClaimVolumeModeBlock, 2803 newClaim: validClaimVolumeModeFile, 2804 }, 2805 "invalid-update-volume-mode-nil-to-file": { 2806 isExpectedFailure: true, 2807 oldClaim: invalidClaimVolumeModeNil, 2808 newClaim: validClaimVolumeModeFile, 2809 }, 2810 "invalid-update-volume-mode-nil-to-block": { 2811 isExpectedFailure: true, 2812 oldClaim: invalidClaimVolumeModeNil, 2813 newClaim: validClaimVolumeModeBlock, 2814 }, 2815 "invalid-update-volume-mode-block-to-nil": { 2816 isExpectedFailure: true, 2817 oldClaim: validClaimVolumeModeBlock, 2818 newClaim: invalidClaimVolumeModeNil, 2819 }, 2820 "invalid-update-volume-mode-file-to-nil": { 2821 isExpectedFailure: true, 2822 oldClaim: validClaimVolumeModeFile, 2823 newClaim: invalidClaimVolumeModeNil, 2824 }, 2825 "invalid-update-volume-mode-empty-to-mode": { 2826 isExpectedFailure: true, 2827 oldClaim: validClaim, 2828 newClaim: validClaimVolumeModeBlock, 2829 }, 2830 "invalid-update-volume-mode-mode-to-empty": { 2831 isExpectedFailure: true, 2832 oldClaim: validClaimVolumeModeBlock, 2833 newClaim: validClaim, 2834 }, 2835 "invalid-update-change-storage-class-annotation-after-creation": { 2836 isExpectedFailure: true, 2837 oldClaim: validClaimStorageClass, 2838 newClaim: invalidUpdateClaimStorageClass, 2839 }, 2840 "valid-update-mutable-annotation": { 2841 isExpectedFailure: false, 2842 oldClaim: validClaimAnnotation, 2843 newClaim: validUpdateClaimMutableAnnotation, 2844 }, 2845 "valid-update-add-annotation": { 2846 isExpectedFailure: false, 2847 oldClaim: validClaim, 2848 newClaim: validAddClaimAnnotation, 2849 }, 2850 "valid-size-update-resize-disabled": { 2851 oldClaim: validClaim, 2852 newClaim: validSizeUpdate, 2853 }, 2854 "valid-size-update-resize-enabled": { 2855 isExpectedFailure: false, 2856 oldClaim: validClaim, 2857 newClaim: validSizeUpdate, 2858 }, 2859 "invalid-size-update-resize-enabled": { 2860 isExpectedFailure: true, 2861 oldClaim: validClaim, 2862 newClaim: invalidSizeUpdate, 2863 }, 2864 "unbound-size-update-resize-enabled": { 2865 isExpectedFailure: true, 2866 oldClaim: validClaim, 2867 newClaim: unboundSizeUpdate, 2868 }, 2869 "valid-upgrade-storage-class-annotation-to-spec": { 2870 isExpectedFailure: false, 2871 oldClaim: validClaimStorageClass, 2872 newClaim: validClaimStorageClassInSpec, 2873 }, 2874 "valid-upgrade-nil-storage-class-spec-to-spec": { 2875 isExpectedFailure: false, 2876 oldClaim: validClaimStorageClassNil, 2877 newClaim: validClaimStorageClassInSpec, 2878 }, 2879 "invalid-upgrade-not-nil-storage-class-spec-to-spec": { 2880 isExpectedFailure: true, 2881 oldClaim: validClaimStorageClassInSpec, 2882 newClaim: validClaimStorageClassInSpecChanged, 2883 }, 2884 "invalid-upgrade-to-nil-storage-class-spec-to-spec": { 2885 isExpectedFailure: true, 2886 oldClaim: validClaimStorageClassInSpec, 2887 newClaim: validClaimStorageClassNil, 2888 }, 2889 "valid-upgrade-storage-class-annotation-and-nil-spec-to-spec-retro": { 2890 isExpectedFailure: false, 2891 oldClaim: validClaimStorageClassInAnnotationAndNilInSpec, 2892 newClaim: validClaimStorageClassInAnnotationAndSpec, 2893 }, 2894 "invalid-upgrade-storage-class-annotation-and-spec-to-spec-retro": { 2895 isExpectedFailure: true, 2896 oldClaim: validClaimStorageClassInAnnotationAndSpec, 2897 newClaim: validClaimStorageClassInSpecChanged, 2898 }, 2899 "invalid-upgrade-storage-class-annotation-and-no-spec": { 2900 isExpectedFailure: true, 2901 oldClaim: validClaimStorageClassInAnnotationAndNilInSpec, 2902 newClaim: validClaimStorageClassInSpecChanged, 2903 }, 2904 "invalid-upgrade-storage-class-annotation-to-spec": { 2905 isExpectedFailure: true, 2906 oldClaim: validClaimStorageClass, 2907 newClaim: invalidClaimStorageClassInSpec, 2908 }, 2909 "valid-upgrade-storage-class-annotation-to-annotation-and-spec": { 2910 isExpectedFailure: false, 2911 oldClaim: validClaimStorageClass, 2912 newClaim: validClaimStorageClassInAnnotationAndSpec, 2913 }, 2914 "invalid-upgrade-storage-class-annotation-to-annotation-and-spec": { 2915 isExpectedFailure: true, 2916 oldClaim: validClaimStorageClass, 2917 newClaim: invalidClaimStorageClassInAnnotationAndSpec, 2918 }, 2919 "invalid-upgrade-storage-class-in-spec": { 2920 isExpectedFailure: true, 2921 oldClaim: validClaimStorageClassInSpec, 2922 newClaim: invalidClaimStorageClassInSpec, 2923 }, 2924 "invalid-downgrade-storage-class-spec-to-annotation": { 2925 isExpectedFailure: true, 2926 oldClaim: validClaimStorageClassInSpec, 2927 newClaim: validClaimStorageClass, 2928 }, 2929 "valid-update-rwop-used-and-rwop-feature-disabled": { 2930 isExpectedFailure: false, 2931 oldClaim: validClaimRWOPAccessMode, 2932 newClaim: validClaimRWOPAccessModeAddAnnotation, 2933 }, 2934 "valid-expand-shrink-resize-enabled": { 2935 oldClaim: validClaimShrinkInitial, 2936 newClaim: validClaimShrink, 2937 enableRecoverFromExpansion: true, 2938 }, 2939 "invalid-expand-shrink-resize-enabled": { 2940 oldClaim: validClaimShrinkInitial, 2941 newClaim: invalidClaimShrink, 2942 enableRecoverFromExpansion: true, 2943 isExpectedFailure: true, 2944 }, 2945 "invalid-expand-shrink-to-status-resize-enabled": { 2946 oldClaim: validClaimShrinkInitial, 2947 newClaim: invalidShrinkToStatus, 2948 enableRecoverFromExpansion: true, 2949 isExpectedFailure: true, 2950 }, 2951 "invalid-expand-shrink-recover-disabled": { 2952 oldClaim: validClaimShrinkInitial, 2953 newClaim: validClaimShrink, 2954 enableRecoverFromExpansion: false, 2955 isExpectedFailure: true, 2956 }, 2957 "unbound-size-shrink-resize-enabled": { 2958 oldClaim: validClaimShrinkInitial, 2959 newClaim: unboundShrink, 2960 enableRecoverFromExpansion: true, 2961 isExpectedFailure: true, 2962 }, 2963 "allow-update-pvc-when-data-source-used": { 2964 oldClaim: invalidClaimDataSourceAPIGroup, 2965 newClaim: invalidClaimDataSourceAPIGroup, 2966 isExpectedFailure: false, 2967 }, 2968 "allow-update-pvc-when-data-source-ref-used": { 2969 oldClaim: invalidClaimDataSourceRefAPIGroup, 2970 newClaim: invalidClaimDataSourceRefAPIGroup, 2971 isExpectedFailure: false, 2972 }, 2973 "valid-update-volume-attributes-class-from-nil": { 2974 oldClaim: validClaimNilVolumeAttributesClass, 2975 newClaim: validClaimVolumeAttributesClass1, 2976 enableVolumeAttributesClass: true, 2977 isExpectedFailure: false, 2978 }, 2979 "valid-update-volume-attributes-class-from-empty": { 2980 oldClaim: validClaimEmptyVolumeAttributesClass, 2981 newClaim: validClaimVolumeAttributesClass1, 2982 enableVolumeAttributesClass: true, 2983 isExpectedFailure: false, 2984 }, 2985 "valid-update-volume-attributes-class": { 2986 oldClaim: validClaimVolumeAttributesClass1, 2987 newClaim: validClaimVolumeAttributesClass2, 2988 enableVolumeAttributesClass: true, 2989 isExpectedFailure: false, 2990 }, 2991 "invalid-update-volume-attributes-class": { 2992 oldClaim: validClaimVolumeAttributesClass1, 2993 newClaim: validClaimNilVolumeAttributesClass, 2994 enableVolumeAttributesClass: true, 2995 isExpectedFailure: true, 2996 }, 2997 "invalid-update-volume-attributes-class-to-nil": { 2998 oldClaim: validClaimVolumeAttributesClass1, 2999 newClaim: validClaimNilVolumeAttributesClass, 3000 enableVolumeAttributesClass: true, 3001 isExpectedFailure: true, 3002 }, 3003 "invalid-update-volume-attributes-class-to-empty": { 3004 oldClaim: validClaimVolumeAttributesClass1, 3005 newClaim: validClaimEmptyVolumeAttributesClass, 3006 enableVolumeAttributesClass: true, 3007 isExpectedFailure: true, 3008 }, 3009 "invalid-update-volume-attributes-class-to-nil-without-featuregate-enabled": { 3010 oldClaim: validClaimVolumeAttributesClass1, 3011 newClaim: validClaimNilVolumeAttributesClass, 3012 enableVolumeAttributesClass: false, 3013 isExpectedFailure: true, 3014 }, 3015 "invalid-update-volume-attributes-class-without-featuregate-enabled": { 3016 oldClaim: validClaimVolumeAttributesClass1, 3017 newClaim: validClaimVolumeAttributesClass2, 3018 enableVolumeAttributesClass: false, 3019 isExpectedFailure: true, 3020 }, 3021 } 3022 3023 for name, scenario := range scenarios { 3024 t.Run(name, func(t *testing.T) { 3025 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, scenario.enableRecoverFromExpansion) 3026 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, scenario.enableVolumeAttributesClass) 3027 3028 scenario.oldClaim.ResourceVersion = "1" 3029 scenario.newClaim.ResourceVersion = "1" 3030 opts := ValidationOptionsForPersistentVolumeClaim(scenario.newClaim, scenario.oldClaim) 3031 errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim, opts) 3032 if len(errs) == 0 && scenario.isExpectedFailure { 3033 t.Errorf("Unexpected success for scenario: %s", name) 3034 } 3035 if len(errs) > 0 && !scenario.isExpectedFailure { 3036 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 3037 } 3038 }) 3039 } 3040 } 3041 3042 func TestValidationOptionsForPersistentVolumeClaim(t *testing.T) { 3043 invaildAPIGroup := "^invalid" 3044 3045 tests := map[string]struct { 3046 oldPvc *core.PersistentVolumeClaim 3047 enableVolumeAttributesClass bool 3048 expectValidationOpts PersistentVolumeClaimSpecValidationOptions 3049 }{ 3050 "nil pv": { 3051 oldPvc: nil, 3052 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3053 EnableRecoverFromExpansionFailure: false, 3054 EnableVolumeAttributesClass: false, 3055 }, 3056 }, 3057 "invaild apiGroup in dataSource allowed because the old pvc is used": { 3058 oldPvc: pvcWithDataSource(&core.TypedLocalObjectReference{APIGroup: &invaildAPIGroup}), 3059 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3060 AllowInvalidAPIGroupInDataSourceOrRef: true, 3061 }, 3062 }, 3063 "invaild apiGroup in dataSourceRef allowed because the old pvc is used": { 3064 oldPvc: pvcWithDataSourceRef(&core.TypedObjectReference{APIGroup: &invaildAPIGroup}), 3065 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3066 AllowInvalidAPIGroupInDataSourceOrRef: true, 3067 }, 3068 }, 3069 "volume attributes class allowed because feature enable": { 3070 oldPvc: pvcWithVolumeAttributesClassName(utilpointer.String("foo")), 3071 enableVolumeAttributesClass: true, 3072 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3073 EnableRecoverFromExpansionFailure: false, 3074 EnableVolumeAttributesClass: true, 3075 }, 3076 }, 3077 "volume attributes class validated because used and feature disabled": { 3078 oldPvc: pvcWithVolumeAttributesClassName(utilpointer.String("foo")), 3079 enableVolumeAttributesClass: false, 3080 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3081 EnableRecoverFromExpansionFailure: false, 3082 EnableVolumeAttributesClass: true, 3083 }, 3084 }, 3085 } 3086 3087 for name, tc := range tests { 3088 t.Run(name, func(t *testing.T) { 3089 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass) 3090 3091 opts := ValidationOptionsForPersistentVolumeClaim(nil, tc.oldPvc) 3092 if opts != tc.expectValidationOpts { 3093 t.Errorf("Expected opts: %+v, received: %+v", tc.expectValidationOpts, opts) 3094 } 3095 }) 3096 } 3097 } 3098 3099 func TestValidationOptionsForPersistentVolumeClaimTemplate(t *testing.T) { 3100 tests := map[string]struct { 3101 oldPvcTemplate *core.PersistentVolumeClaimTemplate 3102 enableVolumeAttributesClass bool 3103 expectValidationOpts PersistentVolumeClaimSpecValidationOptions 3104 }{ 3105 "nil pv": { 3106 oldPvcTemplate: nil, 3107 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{}, 3108 }, 3109 "volume attributes class allowed because feature enable": { 3110 oldPvcTemplate: pvcTemplateWithVolumeAttributesClassName(utilpointer.String("foo")), 3111 enableVolumeAttributesClass: true, 3112 expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{ 3113 EnableVolumeAttributesClass: true, 3114 }, 3115 }, 3116 } 3117 3118 for name, tc := range tests { 3119 t.Run(name, func(t *testing.T) { 3120 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.VolumeAttributesClass, tc.enableVolumeAttributesClass) 3121 3122 opts := ValidationOptionsForPersistentVolumeClaimTemplate(nil, tc.oldPvcTemplate) 3123 if opts != tc.expectValidationOpts { 3124 t.Errorf("Expected opts: %+v, received: %+v", opts, tc.expectValidationOpts) 3125 } 3126 }) 3127 } 3128 } 3129 3130 func TestValidateKeyToPath(t *testing.T) { 3131 testCases := []struct { 3132 kp core.KeyToPath 3133 ok bool 3134 errtype field.ErrorType 3135 }{{ 3136 kp: core.KeyToPath{Key: "k", Path: "p"}, 3137 ok: true, 3138 }, { 3139 kp: core.KeyToPath{Key: "k", Path: "p/p/p/p"}, 3140 ok: true, 3141 }, { 3142 kp: core.KeyToPath{Key: "k", Path: "p/..p/p../p..p"}, 3143 ok: true, 3144 }, { 3145 kp: core.KeyToPath{Key: "k", Path: "p", Mode: utilpointer.Int32(0644)}, 3146 ok: true, 3147 }, { 3148 kp: core.KeyToPath{Key: "", Path: "p"}, 3149 ok: false, 3150 errtype: field.ErrorTypeRequired, 3151 }, { 3152 kp: core.KeyToPath{Key: "k", Path: ""}, 3153 ok: false, 3154 errtype: field.ErrorTypeRequired, 3155 }, { 3156 kp: core.KeyToPath{Key: "k", Path: "..p"}, 3157 ok: false, 3158 errtype: field.ErrorTypeInvalid, 3159 }, { 3160 kp: core.KeyToPath{Key: "k", Path: "../p"}, 3161 ok: false, 3162 errtype: field.ErrorTypeInvalid, 3163 }, { 3164 kp: core.KeyToPath{Key: "k", Path: "p/../p"}, 3165 ok: false, 3166 errtype: field.ErrorTypeInvalid, 3167 }, { 3168 kp: core.KeyToPath{Key: "k", Path: "p/.."}, 3169 ok: false, 3170 errtype: field.ErrorTypeInvalid, 3171 }, { 3172 kp: core.KeyToPath{Key: "k", Path: "p", Mode: utilpointer.Int32(01000)}, 3173 ok: false, 3174 errtype: field.ErrorTypeInvalid, 3175 }, { 3176 kp: core.KeyToPath{Key: "k", Path: "p", Mode: utilpointer.Int32(-1)}, 3177 ok: false, 3178 errtype: field.ErrorTypeInvalid, 3179 }, 3180 } 3181 3182 for i, tc := range testCases { 3183 errs := validateKeyToPath(&tc.kp, field.NewPath("field")) 3184 if tc.ok && len(errs) > 0 { 3185 t.Errorf("[%d] unexpected errors: %v", i, errs) 3186 } else if !tc.ok && len(errs) == 0 { 3187 t.Errorf("[%d] expected error type %v", i, tc.errtype) 3188 } else if len(errs) > 1 { 3189 t.Errorf("[%d] expected only one error, got %d", i, len(errs)) 3190 } else if !tc.ok { 3191 if errs[0].Type != tc.errtype { 3192 t.Errorf("[%d] expected error type %v, got %v", i, tc.errtype, errs[0].Type) 3193 } 3194 } 3195 } 3196 } 3197 3198 func TestValidateNFSVolumeSource(t *testing.T) { 3199 testCases := []struct { 3200 name string 3201 nfs *core.NFSVolumeSource 3202 errtype field.ErrorType 3203 errfield string 3204 errdetail string 3205 }{{ 3206 name: "missing server", 3207 nfs: &core.NFSVolumeSource{Server: "", Path: "/tmp"}, 3208 errtype: field.ErrorTypeRequired, 3209 errfield: "server", 3210 }, { 3211 name: "missing path", 3212 nfs: &core.NFSVolumeSource{Server: "my-server", Path: ""}, 3213 errtype: field.ErrorTypeRequired, 3214 errfield: "path", 3215 }, { 3216 name: "abs path", 3217 nfs: &core.NFSVolumeSource{Server: "my-server", Path: "tmp"}, 3218 errtype: field.ErrorTypeInvalid, 3219 errfield: "path", 3220 errdetail: "must be an absolute path", 3221 }, 3222 } 3223 3224 for i, tc := range testCases { 3225 errs := validateNFSVolumeSource(tc.nfs, field.NewPath("field")) 3226 3227 if len(errs) > 0 && tc.errtype == "" { 3228 t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) 3229 } else if len(errs) == 0 && tc.errtype != "" { 3230 t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) 3231 } else if len(errs) >= 1 { 3232 if errs[0].Type != tc.errtype { 3233 t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) 3234 } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { 3235 t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) 3236 } else if !strings.Contains(errs[0].Detail, tc.errdetail) { 3237 t.Errorf("[%d: %q] expected error detail %q, got %q", i, tc.name, tc.errdetail, errs[0].Detail) 3238 } 3239 } 3240 } 3241 } 3242 3243 func TestValidateGlusterfs(t *testing.T) { 3244 testCases := []struct { 3245 name string 3246 gfs *core.GlusterfsVolumeSource 3247 errtype field.ErrorType 3248 errfield string 3249 }{{ 3250 name: "missing endpointname", 3251 gfs: &core.GlusterfsVolumeSource{EndpointsName: "", Path: "/tmp"}, 3252 errtype: field.ErrorTypeRequired, 3253 errfield: "endpoints", 3254 }, { 3255 name: "missing path", 3256 gfs: &core.GlusterfsVolumeSource{EndpointsName: "my-endpoint", Path: ""}, 3257 errtype: field.ErrorTypeRequired, 3258 errfield: "path", 3259 }, { 3260 name: "missing endpointname and path", 3261 gfs: &core.GlusterfsVolumeSource{EndpointsName: "", Path: ""}, 3262 errtype: field.ErrorTypeRequired, 3263 errfield: "endpoints", 3264 }, 3265 } 3266 3267 for i, tc := range testCases { 3268 errs := validateGlusterfsVolumeSource(tc.gfs, field.NewPath("field")) 3269 3270 if len(errs) > 0 && tc.errtype == "" { 3271 t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) 3272 } else if len(errs) == 0 && tc.errtype != "" { 3273 t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) 3274 } else if len(errs) >= 1 { 3275 if errs[0].Type != tc.errtype { 3276 t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) 3277 } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { 3278 t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) 3279 } 3280 } 3281 } 3282 } 3283 3284 func TestValidateGlusterfsPersistentVolumeSource(t *testing.T) { 3285 var epNs *string 3286 namespace := "" 3287 epNs = &namespace 3288 3289 testCases := []struct { 3290 name string 3291 gfs *core.GlusterfsPersistentVolumeSource 3292 errtype field.ErrorType 3293 errfield string 3294 }{{ 3295 name: "missing endpointname", 3296 gfs: &core.GlusterfsPersistentVolumeSource{EndpointsName: "", Path: "/tmp"}, 3297 errtype: field.ErrorTypeRequired, 3298 errfield: "endpoints", 3299 }, { 3300 name: "missing path", 3301 gfs: &core.GlusterfsPersistentVolumeSource{EndpointsName: "my-endpoint", Path: ""}, 3302 errtype: field.ErrorTypeRequired, 3303 errfield: "path", 3304 }, { 3305 name: "non null endpointnamespace with empty string", 3306 gfs: &core.GlusterfsPersistentVolumeSource{EndpointsName: "my-endpoint", Path: "/tmp", EndpointsNamespace: epNs}, 3307 errtype: field.ErrorTypeInvalid, 3308 errfield: "endpointsNamespace", 3309 }, { 3310 name: "missing endpointname and path", 3311 gfs: &core.GlusterfsPersistentVolumeSource{EndpointsName: "", Path: ""}, 3312 errtype: field.ErrorTypeRequired, 3313 errfield: "endpoints", 3314 }, 3315 } 3316 3317 for i, tc := range testCases { 3318 errs := validateGlusterfsPersistentVolumeSource(tc.gfs, field.NewPath("field")) 3319 3320 if len(errs) > 0 && tc.errtype == "" { 3321 t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) 3322 } else if len(errs) == 0 && tc.errtype != "" { 3323 t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) 3324 } else if len(errs) >= 1 { 3325 if errs[0].Type != tc.errtype { 3326 t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) 3327 } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { 3328 t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) 3329 } 3330 } 3331 } 3332 } 3333 3334 func TestValidateCSIVolumeSource(t *testing.T) { 3335 testCases := []struct { 3336 name string 3337 csi *core.CSIVolumeSource 3338 errtype field.ErrorType 3339 errfield string 3340 }{{ 3341 name: "all required fields ok", 3342 csi: &core.CSIVolumeSource{Driver: "test-driver"}, 3343 }, { 3344 name: "missing driver name", 3345 csi: &core.CSIVolumeSource{Driver: ""}, 3346 errtype: field.ErrorTypeRequired, 3347 errfield: "driver", 3348 }, { 3349 name: "driver name: ok no punctuations", 3350 csi: &core.CSIVolumeSource{Driver: "comgooglestoragecsigcepd"}, 3351 }, { 3352 name: "driver name: ok dot only", 3353 csi: &core.CSIVolumeSource{Driver: "io.kubernetes.storage.csi.flex"}, 3354 }, { 3355 name: "driver name: ok dash only", 3356 csi: &core.CSIVolumeSource{Driver: "io-kubernetes-storage-csi-flex"}, 3357 }, { 3358 name: "driver name: invalid underscore", 3359 csi: &core.CSIVolumeSource{Driver: "io_kubernetes_storage_csi_flex"}, 3360 errtype: field.ErrorTypeInvalid, 3361 errfield: "driver", 3362 }, { 3363 name: "driver name: invalid dot underscores", 3364 csi: &core.CSIVolumeSource{Driver: "io.kubernetes.storage_csi.flex"}, 3365 errtype: field.ErrorTypeInvalid, 3366 errfield: "driver", 3367 }, { 3368 name: "driver name: ok beginning with number", 3369 csi: &core.CSIVolumeSource{Driver: "2io.kubernetes.storage-csi.flex"}, 3370 }, { 3371 name: "driver name: ok ending with number", 3372 csi: &core.CSIVolumeSource{Driver: "io.kubernetes.storage-csi.flex2"}, 3373 }, { 3374 name: "driver name: invalid dot dash underscores", 3375 csi: &core.CSIVolumeSource{Driver: "io.kubernetes-storage.csi_flex"}, 3376 errtype: field.ErrorTypeInvalid, 3377 errfield: "driver", 3378 }, 3379 3380 { 3381 name: "driver name: ok length 1", 3382 csi: &core.CSIVolumeSource{Driver: "a"}, 3383 }, { 3384 name: "driver name: invalid length > 63", 3385 csi: &core.CSIVolumeSource{Driver: strings.Repeat("g", 65)}, 3386 errtype: field.ErrorTypeTooLong, 3387 errfield: "driver", 3388 }, { 3389 name: "driver name: invalid start char", 3390 csi: &core.CSIVolumeSource{Driver: "_comgooglestoragecsigcepd"}, 3391 errtype: field.ErrorTypeInvalid, 3392 errfield: "driver", 3393 }, { 3394 name: "driver name: invalid end char", 3395 csi: &core.CSIVolumeSource{Driver: "comgooglestoragecsigcepd/"}, 3396 errtype: field.ErrorTypeInvalid, 3397 errfield: "driver", 3398 }, { 3399 name: "driver name: invalid separators", 3400 csi: &core.CSIVolumeSource{Driver: "com/google/storage/csi~gcepd"}, 3401 errtype: field.ErrorTypeInvalid, 3402 errfield: "driver", 3403 }, { 3404 name: "valid nodePublishSecretRef", 3405 csi: &core.CSIVolumeSource{Driver: "com.google.gcepd", NodePublishSecretRef: &core.LocalObjectReference{Name: "foobar"}}, 3406 }, { 3407 name: "nodePublishSecretRef: invalid name missing", 3408 csi: &core.CSIVolumeSource{Driver: "com.google.gcepd", NodePublishSecretRef: &core.LocalObjectReference{Name: ""}}, 3409 errtype: field.ErrorTypeRequired, 3410 errfield: "nodePublishSecretRef.name", 3411 }, 3412 } 3413 3414 for i, tc := range testCases { 3415 errs := validateCSIVolumeSource(tc.csi, field.NewPath("field")) 3416 3417 if len(errs) > 0 && tc.errtype == "" { 3418 t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) 3419 } else if len(errs) == 0 && tc.errtype != "" { 3420 t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) 3421 } else if len(errs) >= 1 { 3422 if errs[0].Type != tc.errtype { 3423 t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) 3424 } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { 3425 t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) 3426 } 3427 } 3428 } 3429 } 3430 3431 func TestValidateCSIPersistentVolumeSource(t *testing.T) { 3432 testCases := []struct { 3433 name string 3434 csi *core.CSIPersistentVolumeSource 3435 errtype field.ErrorType 3436 errfield string 3437 }{{ 3438 name: "all required fields ok", 3439 csi: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123", ReadOnly: true}, 3440 }, { 3441 name: "with default values ok", 3442 csi: &core.CSIPersistentVolumeSource{Driver: "test-driver", VolumeHandle: "test-123"}, 3443 }, { 3444 name: "missing driver name", 3445 csi: &core.CSIPersistentVolumeSource{VolumeHandle: "test-123"}, 3446 errtype: field.ErrorTypeRequired, 3447 errfield: "driver", 3448 }, { 3449 name: "missing volume handle", 3450 csi: &core.CSIPersistentVolumeSource{Driver: "my-driver"}, 3451 errtype: field.ErrorTypeRequired, 3452 errfield: "volumeHandle", 3453 }, { 3454 name: "driver name: ok no punctuations", 3455 csi: &core.CSIPersistentVolumeSource{Driver: "comgooglestoragecsigcepd", VolumeHandle: "test-123"}, 3456 }, { 3457 name: "driver name: ok dot only", 3458 csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage.csi.flex", VolumeHandle: "test-123"}, 3459 }, { 3460 name: "driver name: ok dash only", 3461 csi: &core.CSIPersistentVolumeSource{Driver: "io-kubernetes-storage-csi-flex", VolumeHandle: "test-123"}, 3462 }, { 3463 name: "driver name: invalid underscore", 3464 csi: &core.CSIPersistentVolumeSource{Driver: "io_kubernetes_storage_csi_flex", VolumeHandle: "test-123"}, 3465 errtype: field.ErrorTypeInvalid, 3466 errfield: "driver", 3467 }, { 3468 name: "driver name: invalid dot underscores", 3469 csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage_csi.flex", VolumeHandle: "test-123"}, 3470 errtype: field.ErrorTypeInvalid, 3471 errfield: "driver", 3472 }, { 3473 name: "driver name: ok beginning with number", 3474 csi: &core.CSIPersistentVolumeSource{Driver: "2io.kubernetes.storage-csi.flex", VolumeHandle: "test-123"}, 3475 }, { 3476 name: "driver name: ok ending with number", 3477 csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes.storage-csi.flex2", VolumeHandle: "test-123"}, 3478 }, { 3479 name: "driver name: invalid dot dash underscores", 3480 csi: &core.CSIPersistentVolumeSource{Driver: "io.kubernetes-storage.csi_flex", VolumeHandle: "test-123"}, 3481 errtype: field.ErrorTypeInvalid, 3482 errfield: "driver", 3483 }, { 3484 name: "driver name: invalid length 0", 3485 csi: &core.CSIPersistentVolumeSource{Driver: "", VolumeHandle: "test-123"}, 3486 errtype: field.ErrorTypeRequired, 3487 errfield: "driver", 3488 }, { 3489 name: "driver name: ok length 1", 3490 csi: &core.CSIPersistentVolumeSource{Driver: "a", VolumeHandle: "test-123"}, 3491 }, { 3492 name: "driver name: invalid length > 63", 3493 csi: &core.CSIPersistentVolumeSource{Driver: strings.Repeat("g", 65), VolumeHandle: "test-123"}, 3494 errtype: field.ErrorTypeTooLong, 3495 errfield: "driver", 3496 }, { 3497 name: "driver name: invalid start char", 3498 csi: &core.CSIPersistentVolumeSource{Driver: "_comgooglestoragecsigcepd", VolumeHandle: "test-123"}, 3499 errtype: field.ErrorTypeInvalid, 3500 errfield: "driver", 3501 }, { 3502 name: "driver name: invalid end char", 3503 csi: &core.CSIPersistentVolumeSource{Driver: "comgooglestoragecsigcepd/", VolumeHandle: "test-123"}, 3504 errtype: field.ErrorTypeInvalid, 3505 errfield: "driver", 3506 }, { 3507 name: "driver name: invalid separators", 3508 csi: &core.CSIPersistentVolumeSource{Driver: "com/google/storage/csi~gcepd", VolumeHandle: "test-123"}, 3509 errtype: field.ErrorTypeInvalid, 3510 errfield: "driver", 3511 }, { 3512 name: "controllerExpandSecretRef: invalid name missing", 3513 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Namespace: "default"}}, 3514 errtype: field.ErrorTypeRequired, 3515 errfield: "controllerExpandSecretRef.name", 3516 }, { 3517 name: "controllerExpandSecretRef: invalid namespace missing", 3518 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Name: "foobar"}}, 3519 errtype: field.ErrorTypeRequired, 3520 errfield: "controllerExpandSecretRef.namespace", 3521 }, { 3522 name: "valid controllerExpandSecretRef", 3523 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Name: "foobar", Namespace: "default"}}, 3524 }, { 3525 name: "controllerPublishSecretRef: invalid name missing", 3526 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerPublishSecretRef: &core.SecretReference{Namespace: "default"}}, 3527 errtype: field.ErrorTypeRequired, 3528 errfield: "controllerPublishSecretRef.name", 3529 }, { 3530 name: "controllerPublishSecretRef: invalid namespace missing", 3531 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerPublishSecretRef: &core.SecretReference{Name: "foobar"}}, 3532 errtype: field.ErrorTypeRequired, 3533 errfield: "controllerPublishSecretRef.namespace", 3534 }, { 3535 name: "valid controllerPublishSecretRef", 3536 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerPublishSecretRef: &core.SecretReference{Name: "foobar", Namespace: "default"}}, 3537 }, { 3538 name: "valid nodePublishSecretRef", 3539 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: "foobar", Namespace: "default"}}, 3540 }, { 3541 name: "nodePublishSecretRef: invalid name missing", 3542 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Namespace: "foobar"}}, 3543 errtype: field.ErrorTypeRequired, 3544 errfield: "nodePublishSecretRef.name", 3545 }, { 3546 name: "nodePublishSecretRef: invalid namespace missing", 3547 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: "foobar"}}, 3548 errtype: field.ErrorTypeRequired, 3549 errfield: "nodePublishSecretRef.namespace", 3550 }, { 3551 name: "nodeExpandSecretRef: invalid name missing", 3552 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Namespace: "default"}}, 3553 errtype: field.ErrorTypeRequired, 3554 errfield: "nodeExpandSecretRef.name", 3555 }, { 3556 name: "nodeExpandSecretRef: invalid namespace missing", 3557 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Name: "foobar"}}, 3558 errtype: field.ErrorTypeRequired, 3559 errfield: "nodeExpandSecretRef.namespace", 3560 }, { 3561 name: "valid nodeExpandSecretRef", 3562 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Name: "foobar", Namespace: "default"}}, 3563 }, { 3564 name: "Invalid nodePublishSecretRef", 3565 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: "foobar", Namespace: "default"}}, 3566 }, 3567 3568 // tests with allowDNSSubDomainSecretName flag on/off 3569 { 3570 name: "valid nodeExpandSecretRef", 3571 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 63), Namespace: "default"}}, 3572 }, { 3573 name: "valid long nodeExpandSecretRef", 3574 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 65), Namespace: "default"}}, 3575 }, { 3576 name: "Invalid nodeExpandSecretRef", 3577 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodeExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 255), Namespace: "default"}}, 3578 errtype: field.ErrorTypeInvalid, 3579 errfield: "nodeExpandSecretRef.name", 3580 }, { 3581 name: "valid nodePublishSecretRef", 3582 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: strings.Repeat("g", 63), Namespace: "default"}}, 3583 }, { 3584 name: "valid long nodePublishSecretRef", 3585 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: strings.Repeat("g", 65), Namespace: "default"}}, 3586 }, { 3587 name: "Invalid nodePublishSecretRef", 3588 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", NodePublishSecretRef: &core.SecretReference{Name: strings.Repeat("g", 255), Namespace: "default"}}, 3589 errtype: field.ErrorTypeInvalid, 3590 errfield: "nodePublishSecretRef.name", 3591 }, { 3592 name: "valid ControllerExpandSecretRef", 3593 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 63), Namespace: "default"}}, 3594 }, { 3595 name: "valid long ControllerExpandSecretRef", 3596 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 65), Namespace: "default"}}, 3597 }, { 3598 name: "Invalid ControllerExpandSecretRef", 3599 csi: &core.CSIPersistentVolumeSource{Driver: "com.google.gcepd", VolumeHandle: "foobar", ControllerExpandSecretRef: &core.SecretReference{Name: strings.Repeat("g", 255), Namespace: "default"}}, 3600 errtype: field.ErrorTypeInvalid, 3601 errfield: "controllerExpandSecretRef.name", 3602 }, 3603 } 3604 3605 for i, tc := range testCases { 3606 errs := validateCSIPersistentVolumeSource(tc.csi, field.NewPath("field")) 3607 3608 if len(errs) > 0 && tc.errtype == "" { 3609 t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) 3610 } else if len(errs) == 0 && tc.errtype != "" { 3611 t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) 3612 } else if len(errs) >= 1 { 3613 if errs[0].Type != tc.errtype { 3614 t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) 3615 } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { 3616 t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) 3617 } 3618 } 3619 } 3620 } 3621 3622 // This test is a little too top-to-bottom. Ideally we would test each volume 3623 // type on its own, but we want to also make sure that the logic works through 3624 // the one-of wrapper, so we just do it all in one place. 3625 func TestValidateVolumes(t *testing.T) { 3626 validInitiatorName := "iqn.2015-02.example.com:init" 3627 invalidInitiatorName := "2015-02.example.com:init" 3628 3629 type verr struct { 3630 etype field.ErrorType 3631 field string 3632 detail string 3633 } 3634 3635 testCases := []struct { 3636 name string 3637 vol core.Volume 3638 errs []verr 3639 opts PodValidationOptions 3640 }{ 3641 // EmptyDir and basic volume names 3642 { 3643 name: "valid alpha name", 3644 vol: core.Volume{ 3645 Name: "empty", 3646 VolumeSource: core.VolumeSource{ 3647 EmptyDir: &core.EmptyDirVolumeSource{}, 3648 }, 3649 }, 3650 }, { 3651 name: "valid num name", 3652 vol: core.Volume{ 3653 Name: "123", 3654 VolumeSource: core.VolumeSource{ 3655 EmptyDir: &core.EmptyDirVolumeSource{}, 3656 }, 3657 }, 3658 }, { 3659 name: "valid alphanum name", 3660 vol: core.Volume{ 3661 Name: "empty-123", 3662 VolumeSource: core.VolumeSource{ 3663 EmptyDir: &core.EmptyDirVolumeSource{}, 3664 }, 3665 }, 3666 }, { 3667 name: "valid numalpha name", 3668 vol: core.Volume{ 3669 Name: "123-empty", 3670 VolumeSource: core.VolumeSource{ 3671 EmptyDir: &core.EmptyDirVolumeSource{}, 3672 }, 3673 }, 3674 }, { 3675 name: "zero-length name", 3676 vol: core.Volume{ 3677 Name: "", 3678 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}, 3679 }, 3680 errs: []verr{{ 3681 etype: field.ErrorTypeRequired, 3682 field: "name", 3683 }}, 3684 }, { 3685 name: "name > 63 characters", 3686 vol: core.Volume{ 3687 Name: strings.Repeat("a", 64), 3688 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}, 3689 }, 3690 errs: []verr{{ 3691 etype: field.ErrorTypeInvalid, 3692 field: "name", 3693 detail: "must be no more than", 3694 }}, 3695 }, { 3696 name: "name has dots", 3697 vol: core.Volume{ 3698 Name: "a.b.c", 3699 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}, 3700 }, 3701 errs: []verr{{ 3702 etype: field.ErrorTypeInvalid, 3703 field: "name", 3704 detail: "must not contain dots", 3705 }}, 3706 }, { 3707 name: "name not a DNS label", 3708 vol: core.Volume{ 3709 Name: "Not a DNS label!", 3710 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}, 3711 }, 3712 errs: []verr{{ 3713 etype: field.ErrorTypeInvalid, 3714 field: "name", 3715 detail: dnsLabelErrMsg, 3716 }}, 3717 }, 3718 // More than one source field specified. 3719 { 3720 name: "more than one source", 3721 vol: core.Volume{ 3722 Name: "dups", 3723 VolumeSource: core.VolumeSource{ 3724 EmptyDir: &core.EmptyDirVolumeSource{}, 3725 HostPath: &core.HostPathVolumeSource{ 3726 Path: "/mnt/path", 3727 Type: newHostPathType(string(core.HostPathDirectory)), 3728 }, 3729 }, 3730 }, 3731 errs: []verr{{ 3732 etype: field.ErrorTypeForbidden, 3733 field: "hostPath", 3734 detail: "may not specify more than 1 volume", 3735 }}, 3736 }, 3737 // HostPath Default 3738 { 3739 name: "default HostPath", 3740 vol: core.Volume{ 3741 Name: "hostpath", 3742 VolumeSource: core.VolumeSource{ 3743 HostPath: &core.HostPathVolumeSource{ 3744 Path: "/mnt/path", 3745 Type: newHostPathType(string(core.HostPathDirectory)), 3746 }, 3747 }, 3748 }, 3749 }, 3750 // HostPath Supported 3751 { 3752 name: "valid HostPath", 3753 vol: core.Volume{ 3754 Name: "hostpath", 3755 VolumeSource: core.VolumeSource{ 3756 HostPath: &core.HostPathVolumeSource{ 3757 Path: "/mnt/path", 3758 Type: newHostPathType(string(core.HostPathSocket)), 3759 }, 3760 }, 3761 }, 3762 }, 3763 // HostPath Invalid 3764 { 3765 name: "invalid HostPath", 3766 vol: core.Volume{ 3767 Name: "hostpath", 3768 VolumeSource: core.VolumeSource{ 3769 HostPath: &core.HostPathVolumeSource{ 3770 Path: "/mnt/path", 3771 Type: newHostPathType("invalid"), 3772 }, 3773 }, 3774 }, 3775 errs: []verr{{ 3776 etype: field.ErrorTypeNotSupported, 3777 field: "type", 3778 }}, 3779 }, { 3780 name: "invalid HostPath backsteps", 3781 vol: core.Volume{ 3782 Name: "hostpath", 3783 VolumeSource: core.VolumeSource{ 3784 HostPath: &core.HostPathVolumeSource{ 3785 Path: "/mnt/path/..", 3786 Type: newHostPathType(string(core.HostPathDirectory)), 3787 }, 3788 }, 3789 }, 3790 errs: []verr{{ 3791 etype: field.ErrorTypeInvalid, 3792 field: "path", 3793 detail: "must not contain '..'", 3794 }}, 3795 }, 3796 // GcePersistentDisk 3797 { 3798 name: "valid GcePersistentDisk", 3799 vol: core.Volume{ 3800 Name: "gce-pd", 3801 VolumeSource: core.VolumeSource{ 3802 GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{ 3803 PDName: "my-PD", 3804 FSType: "ext4", 3805 Partition: 1, 3806 ReadOnly: false, 3807 }, 3808 }, 3809 }, 3810 }, 3811 // AWSElasticBlockStore 3812 { 3813 name: "valid AWSElasticBlockStore", 3814 vol: core.Volume{ 3815 Name: "aws-ebs", 3816 VolumeSource: core.VolumeSource{ 3817 AWSElasticBlockStore: &core.AWSElasticBlockStoreVolumeSource{ 3818 VolumeID: "my-PD", 3819 FSType: "ext4", 3820 Partition: 1, 3821 ReadOnly: false, 3822 }, 3823 }, 3824 }, 3825 }, 3826 // GitRepo 3827 { 3828 name: "valid GitRepo", 3829 vol: core.Volume{ 3830 Name: "git-repo", 3831 VolumeSource: core.VolumeSource{ 3832 GitRepo: &core.GitRepoVolumeSource{ 3833 Repository: "my-repo", 3834 Revision: "hashstring", 3835 Directory: "target", 3836 }, 3837 }, 3838 }, 3839 }, { 3840 name: "valid GitRepo in .", 3841 vol: core.Volume{ 3842 Name: "git-repo-dot", 3843 VolumeSource: core.VolumeSource{ 3844 GitRepo: &core.GitRepoVolumeSource{ 3845 Repository: "my-repo", 3846 Directory: ".", 3847 }, 3848 }, 3849 }, 3850 }, { 3851 name: "valid GitRepo with .. in name", 3852 vol: core.Volume{ 3853 Name: "git-repo-dot-dot-foo", 3854 VolumeSource: core.VolumeSource{ 3855 GitRepo: &core.GitRepoVolumeSource{ 3856 Repository: "my-repo", 3857 Directory: "..foo", 3858 }, 3859 }, 3860 }, 3861 }, { 3862 name: "GitRepo starts with ../", 3863 vol: core.Volume{ 3864 Name: "gitrepo", 3865 VolumeSource: core.VolumeSource{ 3866 GitRepo: &core.GitRepoVolumeSource{ 3867 Repository: "foo", 3868 Directory: "../dots/bar", 3869 }, 3870 }, 3871 }, 3872 errs: []verr{{ 3873 etype: field.ErrorTypeInvalid, 3874 field: "gitRepo.directory", 3875 detail: `must not contain '..'`, 3876 }}, 3877 }, { 3878 name: "GitRepo contains ..", 3879 vol: core.Volume{ 3880 Name: "gitrepo", 3881 VolumeSource: core.VolumeSource{ 3882 GitRepo: &core.GitRepoVolumeSource{ 3883 Repository: "foo", 3884 Directory: "dots/../bar", 3885 }, 3886 }, 3887 }, 3888 errs: []verr{{ 3889 etype: field.ErrorTypeInvalid, 3890 field: "gitRepo.directory", 3891 detail: `must not contain '..'`, 3892 }}, 3893 }, { 3894 name: "GitRepo absolute target", 3895 vol: core.Volume{ 3896 Name: "gitrepo", 3897 VolumeSource: core.VolumeSource{ 3898 GitRepo: &core.GitRepoVolumeSource{ 3899 Repository: "foo", 3900 Directory: "/abstarget", 3901 }, 3902 }, 3903 }, 3904 errs: []verr{{ 3905 etype: field.ErrorTypeInvalid, 3906 field: "gitRepo.directory", 3907 }}, 3908 }, 3909 // ISCSI 3910 { 3911 name: "valid ISCSI", 3912 vol: core.Volume{ 3913 Name: "iscsi", 3914 VolumeSource: core.VolumeSource{ 3915 ISCSI: &core.ISCSIVolumeSource{ 3916 TargetPortal: "127.0.0.1", 3917 IQN: "iqn.2015-02.example.com:test", 3918 Lun: 1, 3919 FSType: "ext4", 3920 ReadOnly: false, 3921 }, 3922 }, 3923 }, 3924 }, { 3925 name: "valid IQN: eui format", 3926 vol: core.Volume{ 3927 Name: "iscsi", 3928 VolumeSource: core.VolumeSource{ 3929 ISCSI: &core.ISCSIVolumeSource{ 3930 TargetPortal: "127.0.0.1", 3931 IQN: "eui.0123456789ABCDEF", 3932 Lun: 1, 3933 FSType: "ext4", 3934 ReadOnly: false, 3935 }, 3936 }, 3937 }, 3938 }, { 3939 name: "valid IQN: naa format", 3940 vol: core.Volume{ 3941 Name: "iscsi", 3942 VolumeSource: core.VolumeSource{ 3943 ISCSI: &core.ISCSIVolumeSource{ 3944 TargetPortal: "127.0.0.1", 3945 IQN: "naa.62004567BA64678D0123456789ABCDEF", 3946 Lun: 1, 3947 FSType: "ext4", 3948 ReadOnly: false, 3949 }, 3950 }, 3951 }, 3952 }, { 3953 name: "empty portal", 3954 vol: core.Volume{ 3955 Name: "iscsi", 3956 VolumeSource: core.VolumeSource{ 3957 ISCSI: &core.ISCSIVolumeSource{ 3958 TargetPortal: "", 3959 IQN: "iqn.2015-02.example.com:test", 3960 Lun: 1, 3961 FSType: "ext4", 3962 ReadOnly: false, 3963 }, 3964 }, 3965 }, 3966 errs: []verr{{ 3967 etype: field.ErrorTypeRequired, 3968 field: "iscsi.targetPortal", 3969 }}, 3970 }, { 3971 name: "empty iqn", 3972 vol: core.Volume{ 3973 Name: "iscsi", 3974 VolumeSource: core.VolumeSource{ 3975 ISCSI: &core.ISCSIVolumeSource{ 3976 TargetPortal: "127.0.0.1", 3977 IQN: "", 3978 Lun: 1, 3979 FSType: "ext4", 3980 ReadOnly: false, 3981 }, 3982 }, 3983 }, 3984 errs: []verr{{ 3985 etype: field.ErrorTypeRequired, 3986 field: "iscsi.iqn", 3987 }}, 3988 }, { 3989 name: "invalid IQN: iqn format", 3990 vol: core.Volume{ 3991 Name: "iscsi", 3992 VolumeSource: core.VolumeSource{ 3993 ISCSI: &core.ISCSIVolumeSource{ 3994 TargetPortal: "127.0.0.1", 3995 IQN: "iqn.2015-02.example.com:test;ls;", 3996 Lun: 1, 3997 FSType: "ext4", 3998 ReadOnly: false, 3999 }, 4000 }, 4001 }, 4002 errs: []verr{{ 4003 etype: field.ErrorTypeInvalid, 4004 field: "iscsi.iqn", 4005 }}, 4006 }, { 4007 name: "invalid IQN: eui format", 4008 vol: core.Volume{ 4009 Name: "iscsi", 4010 VolumeSource: core.VolumeSource{ 4011 ISCSI: &core.ISCSIVolumeSource{ 4012 TargetPortal: "127.0.0.1", 4013 IQN: "eui.0123456789ABCDEFGHIJ", 4014 Lun: 1, 4015 FSType: "ext4", 4016 ReadOnly: false, 4017 }, 4018 }, 4019 }, 4020 errs: []verr{{ 4021 etype: field.ErrorTypeInvalid, 4022 field: "iscsi.iqn", 4023 }}, 4024 }, { 4025 name: "invalid IQN: naa format", 4026 vol: core.Volume{ 4027 Name: "iscsi", 4028 VolumeSource: core.VolumeSource{ 4029 ISCSI: &core.ISCSIVolumeSource{ 4030 TargetPortal: "127.0.0.1", 4031 IQN: "naa.62004567BA_4-78D.123456789ABCDEF", 4032 Lun: 1, 4033 FSType: "ext4", 4034 ReadOnly: false, 4035 }, 4036 }, 4037 }, 4038 errs: []verr{{ 4039 etype: field.ErrorTypeInvalid, 4040 field: "iscsi.iqn", 4041 }}, 4042 }, { 4043 name: "valid initiatorName", 4044 vol: core.Volume{ 4045 Name: "iscsi", 4046 VolumeSource: core.VolumeSource{ 4047 ISCSI: &core.ISCSIVolumeSource{ 4048 TargetPortal: "127.0.0.1", 4049 IQN: "iqn.2015-02.example.com:test", 4050 Lun: 1, 4051 InitiatorName: &validInitiatorName, 4052 FSType: "ext4", 4053 ReadOnly: false, 4054 }, 4055 }, 4056 }, 4057 }, { 4058 name: "invalid initiatorName", 4059 vol: core.Volume{ 4060 Name: "iscsi", 4061 VolumeSource: core.VolumeSource{ 4062 ISCSI: &core.ISCSIVolumeSource{ 4063 TargetPortal: "127.0.0.1", 4064 IQN: "iqn.2015-02.example.com:test", 4065 Lun: 1, 4066 InitiatorName: &invalidInitiatorName, 4067 FSType: "ext4", 4068 ReadOnly: false, 4069 }, 4070 }, 4071 }, 4072 errs: []verr{{ 4073 etype: field.ErrorTypeInvalid, 4074 field: "iscsi.initiatorname", 4075 }}, 4076 }, { 4077 name: "empty secret", 4078 vol: core.Volume{ 4079 Name: "iscsi", 4080 VolumeSource: core.VolumeSource{ 4081 ISCSI: &core.ISCSIVolumeSource{ 4082 TargetPortal: "127.0.0.1", 4083 IQN: "iqn.2015-02.example.com:test", 4084 Lun: 1, 4085 FSType: "ext4", 4086 ReadOnly: false, 4087 DiscoveryCHAPAuth: true, 4088 }, 4089 }, 4090 }, 4091 errs: []verr{{ 4092 etype: field.ErrorTypeRequired, 4093 field: "iscsi.secretRef", 4094 }}, 4095 }, { 4096 name: "empty secret", 4097 vol: core.Volume{ 4098 Name: "iscsi", 4099 VolumeSource: core.VolumeSource{ 4100 ISCSI: &core.ISCSIVolumeSource{ 4101 TargetPortal: "127.0.0.1", 4102 IQN: "iqn.2015-02.example.com:test", 4103 Lun: 1, 4104 FSType: "ext4", 4105 ReadOnly: false, 4106 SessionCHAPAuth: true, 4107 }, 4108 }, 4109 }, 4110 errs: []verr{{ 4111 etype: field.ErrorTypeRequired, 4112 field: "iscsi.secretRef", 4113 }}, 4114 }, 4115 // Secret 4116 { 4117 name: "valid Secret", 4118 vol: core.Volume{ 4119 Name: "secret", 4120 VolumeSource: core.VolumeSource{ 4121 Secret: &core.SecretVolumeSource{ 4122 SecretName: "my-secret", 4123 }, 4124 }, 4125 }, 4126 }, { 4127 name: "valid Secret with defaultMode", 4128 vol: core.Volume{ 4129 Name: "secret", 4130 VolumeSource: core.VolumeSource{ 4131 Secret: &core.SecretVolumeSource{ 4132 SecretName: "my-secret", 4133 DefaultMode: utilpointer.Int32(0644), 4134 }, 4135 }, 4136 }, 4137 }, { 4138 name: "valid Secret with projection and mode", 4139 vol: core.Volume{ 4140 Name: "secret", 4141 VolumeSource: core.VolumeSource{ 4142 Secret: &core.SecretVolumeSource{ 4143 SecretName: "my-secret", 4144 Items: []core.KeyToPath{{ 4145 Key: "key", 4146 Path: "filename", 4147 Mode: utilpointer.Int32(0644), 4148 }}, 4149 }, 4150 }, 4151 }, 4152 }, { 4153 name: "valid Secret with subdir projection", 4154 vol: core.Volume{ 4155 Name: "secret", 4156 VolumeSource: core.VolumeSource{ 4157 Secret: &core.SecretVolumeSource{ 4158 SecretName: "my-secret", 4159 Items: []core.KeyToPath{{ 4160 Key: "key", 4161 Path: "dir/filename", 4162 }}, 4163 }, 4164 }, 4165 }, 4166 }, { 4167 name: "secret with missing path", 4168 vol: core.Volume{ 4169 Name: "secret", 4170 VolumeSource: core.VolumeSource{ 4171 Secret: &core.SecretVolumeSource{ 4172 SecretName: "s", 4173 Items: []core.KeyToPath{{Key: "key", Path: ""}}, 4174 }, 4175 }, 4176 }, 4177 errs: []verr{{ 4178 etype: field.ErrorTypeRequired, 4179 field: "secret.items[0].path", 4180 }}, 4181 }, { 4182 name: "secret with leading ..", 4183 vol: core.Volume{ 4184 Name: "secret", 4185 VolumeSource: core.VolumeSource{ 4186 Secret: &core.SecretVolumeSource{ 4187 SecretName: "s", 4188 Items: []core.KeyToPath{{Key: "key", Path: "../foo"}}, 4189 }, 4190 }, 4191 }, 4192 errs: []verr{{ 4193 etype: field.ErrorTypeInvalid, 4194 field: "secret.items[0].path", 4195 }}, 4196 }, { 4197 name: "secret with .. inside", 4198 vol: core.Volume{ 4199 Name: "secret", 4200 VolumeSource: core.VolumeSource{ 4201 Secret: &core.SecretVolumeSource{ 4202 SecretName: "s", 4203 Items: []core.KeyToPath{{Key: "key", Path: "foo/../bar"}}, 4204 }, 4205 }, 4206 }, 4207 errs: []verr{{ 4208 etype: field.ErrorTypeInvalid, 4209 field: "secret.items[0].path", 4210 }}, 4211 }, { 4212 name: "secret with invalid positive defaultMode", 4213 vol: core.Volume{ 4214 Name: "secret", 4215 VolumeSource: core.VolumeSource{ 4216 Secret: &core.SecretVolumeSource{ 4217 SecretName: "s", 4218 DefaultMode: utilpointer.Int32(01000), 4219 }, 4220 }, 4221 }, 4222 errs: []verr{{ 4223 etype: field.ErrorTypeInvalid, 4224 field: "secret.defaultMode", 4225 }}, 4226 }, { 4227 name: "secret with invalid negative defaultMode", 4228 vol: core.Volume{ 4229 Name: "secret", 4230 VolumeSource: core.VolumeSource{ 4231 Secret: &core.SecretVolumeSource{ 4232 SecretName: "s", 4233 DefaultMode: utilpointer.Int32(-1), 4234 }, 4235 }, 4236 }, 4237 errs: []verr{{ 4238 etype: field.ErrorTypeInvalid, 4239 field: "secret.defaultMode", 4240 }}, 4241 }, 4242 // ConfigMap 4243 { 4244 name: "valid ConfigMap", 4245 vol: core.Volume{ 4246 Name: "cfgmap", 4247 VolumeSource: core.VolumeSource{ 4248 ConfigMap: &core.ConfigMapVolumeSource{ 4249 LocalObjectReference: core.LocalObjectReference{ 4250 Name: "my-cfgmap", 4251 }, 4252 }, 4253 }, 4254 }, 4255 }, { 4256 name: "valid ConfigMap with defaultMode", 4257 vol: core.Volume{ 4258 Name: "cfgmap", 4259 VolumeSource: core.VolumeSource{ 4260 ConfigMap: &core.ConfigMapVolumeSource{ 4261 LocalObjectReference: core.LocalObjectReference{ 4262 Name: "my-cfgmap", 4263 }, 4264 DefaultMode: utilpointer.Int32(0644), 4265 }, 4266 }, 4267 }, 4268 }, { 4269 name: "valid ConfigMap with projection and mode", 4270 vol: core.Volume{ 4271 Name: "cfgmap", 4272 VolumeSource: core.VolumeSource{ 4273 ConfigMap: &core.ConfigMapVolumeSource{ 4274 LocalObjectReference: core.LocalObjectReference{ 4275 Name: "my-cfgmap"}, 4276 Items: []core.KeyToPath{{ 4277 Key: "key", 4278 Path: "filename", 4279 Mode: utilpointer.Int32(0644), 4280 }}, 4281 }, 4282 }, 4283 }, 4284 }, { 4285 name: "valid ConfigMap with subdir projection", 4286 vol: core.Volume{ 4287 Name: "cfgmap", 4288 VolumeSource: core.VolumeSource{ 4289 ConfigMap: &core.ConfigMapVolumeSource{ 4290 LocalObjectReference: core.LocalObjectReference{ 4291 Name: "my-cfgmap"}, 4292 Items: []core.KeyToPath{{ 4293 Key: "key", 4294 Path: "dir/filename", 4295 }}, 4296 }, 4297 }, 4298 }, 4299 }, { 4300 name: "configmap with missing path", 4301 vol: core.Volume{ 4302 Name: "cfgmap", 4303 VolumeSource: core.VolumeSource{ 4304 ConfigMap: &core.ConfigMapVolumeSource{ 4305 LocalObjectReference: core.LocalObjectReference{Name: "c"}, 4306 Items: []core.KeyToPath{{Key: "key", Path: ""}}, 4307 }, 4308 }, 4309 }, 4310 errs: []verr{{ 4311 etype: field.ErrorTypeRequired, 4312 field: "configMap.items[0].path", 4313 }}, 4314 }, { 4315 name: "configmap with leading ..", 4316 vol: core.Volume{ 4317 Name: "cfgmap", 4318 VolumeSource: core.VolumeSource{ 4319 ConfigMap: &core.ConfigMapVolumeSource{ 4320 LocalObjectReference: core.LocalObjectReference{Name: "c"}, 4321 Items: []core.KeyToPath{{Key: "key", Path: "../foo"}}, 4322 }, 4323 }, 4324 }, 4325 errs: []verr{{ 4326 etype: field.ErrorTypeInvalid, 4327 field: "configMap.items[0].path", 4328 }}, 4329 }, { 4330 name: "configmap with .. inside", 4331 vol: core.Volume{ 4332 Name: "cfgmap", 4333 VolumeSource: core.VolumeSource{ 4334 ConfigMap: &core.ConfigMapVolumeSource{ 4335 LocalObjectReference: core.LocalObjectReference{Name: "c"}, 4336 Items: []core.KeyToPath{{Key: "key", Path: "foo/../bar"}}, 4337 }, 4338 }, 4339 }, 4340 errs: []verr{{ 4341 etype: field.ErrorTypeInvalid, 4342 field: "configMap.items[0].path", 4343 }}, 4344 }, { 4345 name: "configmap with invalid positive defaultMode", 4346 vol: core.Volume{ 4347 Name: "cfgmap", 4348 VolumeSource: core.VolumeSource{ 4349 ConfigMap: &core.ConfigMapVolumeSource{ 4350 LocalObjectReference: core.LocalObjectReference{Name: "c"}, 4351 DefaultMode: utilpointer.Int32(01000), 4352 }, 4353 }, 4354 }, 4355 errs: []verr{{ 4356 etype: field.ErrorTypeInvalid, 4357 field: "configMap.defaultMode", 4358 }}, 4359 }, { 4360 name: "configmap with invalid negative defaultMode", 4361 vol: core.Volume{ 4362 Name: "cfgmap", 4363 VolumeSource: core.VolumeSource{ 4364 ConfigMap: &core.ConfigMapVolumeSource{ 4365 LocalObjectReference: core.LocalObjectReference{Name: "c"}, 4366 DefaultMode: utilpointer.Int32(-1), 4367 }, 4368 }, 4369 }, 4370 errs: []verr{{ 4371 etype: field.ErrorTypeInvalid, 4372 field: "configMap.defaultMode", 4373 }}, 4374 }, 4375 // Glusterfs 4376 { 4377 name: "valid Glusterfs", 4378 vol: core.Volume{ 4379 Name: "glusterfs", 4380 VolumeSource: core.VolumeSource{ 4381 Glusterfs: &core.GlusterfsVolumeSource{ 4382 EndpointsName: "host1", 4383 Path: "path", 4384 ReadOnly: false, 4385 }, 4386 }, 4387 }, 4388 }, { 4389 name: "empty hosts", 4390 vol: core.Volume{ 4391 Name: "glusterfs", 4392 VolumeSource: core.VolumeSource{ 4393 Glusterfs: &core.GlusterfsVolumeSource{ 4394 EndpointsName: "", 4395 Path: "path", 4396 ReadOnly: false, 4397 }, 4398 }, 4399 }, 4400 errs: []verr{{ 4401 etype: field.ErrorTypeRequired, 4402 field: "glusterfs.endpoints", 4403 }}, 4404 }, { 4405 name: "empty path", 4406 vol: core.Volume{ 4407 Name: "glusterfs", 4408 VolumeSource: core.VolumeSource{ 4409 Glusterfs: &core.GlusterfsVolumeSource{ 4410 EndpointsName: "host", 4411 Path: "", 4412 ReadOnly: false, 4413 }, 4414 }, 4415 }, 4416 errs: []verr{{ 4417 etype: field.ErrorTypeRequired, 4418 field: "glusterfs.path", 4419 }}, 4420 }, 4421 // Flocker 4422 { 4423 name: "valid Flocker -- datasetUUID", 4424 vol: core.Volume{ 4425 Name: "flocker", 4426 VolumeSource: core.VolumeSource{ 4427 Flocker: &core.FlockerVolumeSource{ 4428 DatasetUUID: "d846b09d-223d-43df-ab5b-d6db2206a0e4", 4429 }, 4430 }, 4431 }, 4432 }, { 4433 name: "valid Flocker -- datasetName", 4434 vol: core.Volume{ 4435 Name: "flocker", 4436 VolumeSource: core.VolumeSource{ 4437 Flocker: &core.FlockerVolumeSource{ 4438 DatasetName: "datasetName", 4439 }, 4440 }, 4441 }, 4442 }, { 4443 name: "both empty", 4444 vol: core.Volume{ 4445 Name: "flocker", 4446 VolumeSource: core.VolumeSource{ 4447 Flocker: &core.FlockerVolumeSource{ 4448 DatasetName: "", 4449 }, 4450 }, 4451 }, 4452 errs: []verr{{ 4453 etype: field.ErrorTypeRequired, 4454 field: "flocker", 4455 }}, 4456 }, { 4457 name: "both specified", 4458 vol: core.Volume{ 4459 Name: "flocker", 4460 VolumeSource: core.VolumeSource{ 4461 Flocker: &core.FlockerVolumeSource{ 4462 DatasetName: "datasetName", 4463 DatasetUUID: "d846b09d-223d-43df-ab5b-d6db2206a0e4", 4464 }, 4465 }, 4466 }, 4467 errs: []verr{{ 4468 etype: field.ErrorTypeInvalid, 4469 field: "flocker", 4470 }}, 4471 }, { 4472 name: "slash in flocker datasetName", 4473 vol: core.Volume{ 4474 Name: "flocker", 4475 VolumeSource: core.VolumeSource{ 4476 Flocker: &core.FlockerVolumeSource{ 4477 DatasetName: "foo/bar", 4478 }, 4479 }, 4480 }, 4481 errs: []verr{{ 4482 etype: field.ErrorTypeInvalid, 4483 field: "flocker.datasetName", 4484 detail: "must not contain '/'", 4485 }}, 4486 }, 4487 // RBD 4488 { 4489 name: "valid RBD", 4490 vol: core.Volume{ 4491 Name: "rbd", 4492 VolumeSource: core.VolumeSource{ 4493 RBD: &core.RBDVolumeSource{ 4494 CephMonitors: []string{"foo"}, 4495 RBDImage: "bar", 4496 FSType: "ext4", 4497 }, 4498 }, 4499 }, 4500 }, { 4501 name: "empty rbd monitors", 4502 vol: core.Volume{ 4503 Name: "rbd", 4504 VolumeSource: core.VolumeSource{ 4505 RBD: &core.RBDVolumeSource{ 4506 CephMonitors: []string{}, 4507 RBDImage: "bar", 4508 FSType: "ext4", 4509 }, 4510 }, 4511 }, 4512 errs: []verr{{ 4513 etype: field.ErrorTypeRequired, 4514 field: "rbd.monitors", 4515 }}, 4516 }, { 4517 name: "empty image", 4518 vol: core.Volume{ 4519 Name: "rbd", 4520 VolumeSource: core.VolumeSource{ 4521 RBD: &core.RBDVolumeSource{ 4522 CephMonitors: []string{"foo"}, 4523 RBDImage: "", 4524 FSType: "ext4", 4525 }, 4526 }, 4527 }, 4528 errs: []verr{{ 4529 etype: field.ErrorTypeRequired, 4530 field: "rbd.image", 4531 }}, 4532 }, 4533 // Cinder 4534 { 4535 name: "valid Cinder", 4536 vol: core.Volume{ 4537 Name: "cinder", 4538 VolumeSource: core.VolumeSource{ 4539 Cinder: &core.CinderVolumeSource{ 4540 VolumeID: "29ea5088-4f60-4757-962e-dba678767887", 4541 FSType: "ext4", 4542 ReadOnly: false, 4543 }, 4544 }, 4545 }, 4546 }, 4547 // CephFS 4548 { 4549 name: "valid CephFS", 4550 vol: core.Volume{ 4551 Name: "cephfs", 4552 VolumeSource: core.VolumeSource{ 4553 CephFS: &core.CephFSVolumeSource{ 4554 Monitors: []string{"foo"}, 4555 }, 4556 }, 4557 }, 4558 }, { 4559 name: "empty cephfs monitors", 4560 vol: core.Volume{ 4561 Name: "cephfs", 4562 VolumeSource: core.VolumeSource{ 4563 CephFS: &core.CephFSVolumeSource{ 4564 Monitors: []string{}, 4565 }, 4566 }, 4567 }, 4568 errs: []verr{{ 4569 etype: field.ErrorTypeRequired, 4570 field: "cephfs.monitors", 4571 }}, 4572 }, 4573 // DownwardAPI 4574 { 4575 name: "valid DownwardAPI", 4576 vol: core.Volume{ 4577 Name: "downwardapi", 4578 VolumeSource: core.VolumeSource{ 4579 DownwardAPI: &core.DownwardAPIVolumeSource{ 4580 Items: []core.DownwardAPIVolumeFile{{ 4581 Path: "labels", 4582 FieldRef: &core.ObjectFieldSelector{ 4583 APIVersion: "v1", 4584 FieldPath: "metadata.labels", 4585 }, 4586 }, { 4587 Path: "labels with subscript", 4588 FieldRef: &core.ObjectFieldSelector{ 4589 APIVersion: "v1", 4590 FieldPath: "metadata.labels['key']", 4591 }, 4592 }, { 4593 Path: "labels with complex subscript", 4594 FieldRef: &core.ObjectFieldSelector{ 4595 APIVersion: "v1", 4596 FieldPath: "metadata.labels['test.example.com/key']", 4597 }, 4598 }, { 4599 Path: "annotations", 4600 FieldRef: &core.ObjectFieldSelector{ 4601 APIVersion: "v1", 4602 FieldPath: "metadata.annotations", 4603 }, 4604 }, { 4605 Path: "annotations with subscript", 4606 FieldRef: &core.ObjectFieldSelector{ 4607 APIVersion: "v1", 4608 FieldPath: "metadata.annotations['key']", 4609 }, 4610 }, { 4611 Path: "annotations with complex subscript", 4612 FieldRef: &core.ObjectFieldSelector{ 4613 APIVersion: "v1", 4614 FieldPath: "metadata.annotations['TEST.EXAMPLE.COM/key']", 4615 }, 4616 }, { 4617 Path: "namespace", 4618 FieldRef: &core.ObjectFieldSelector{ 4619 APIVersion: "v1", 4620 FieldPath: "metadata.namespace", 4621 }, 4622 }, { 4623 Path: "name", 4624 FieldRef: &core.ObjectFieldSelector{ 4625 APIVersion: "v1", 4626 FieldPath: "metadata.name", 4627 }, 4628 }, { 4629 Path: "path/with/subdirs", 4630 FieldRef: &core.ObjectFieldSelector{ 4631 APIVersion: "v1", 4632 FieldPath: "metadata.labels", 4633 }, 4634 }, { 4635 Path: "path/./withdot", 4636 FieldRef: &core.ObjectFieldSelector{ 4637 APIVersion: "v1", 4638 FieldPath: "metadata.labels", 4639 }, 4640 }, { 4641 Path: "path/with/embedded..dotdot", 4642 FieldRef: &core.ObjectFieldSelector{ 4643 APIVersion: "v1", 4644 FieldPath: "metadata.labels", 4645 }, 4646 }, { 4647 Path: "path/with/leading/..dotdot", 4648 FieldRef: &core.ObjectFieldSelector{ 4649 APIVersion: "v1", 4650 FieldPath: "metadata.labels", 4651 }, 4652 }, { 4653 Path: "cpu_limit", 4654 ResourceFieldRef: &core.ResourceFieldSelector{ 4655 ContainerName: "test-container", 4656 Resource: "limits.cpu", 4657 }, 4658 }, { 4659 Path: "cpu_request", 4660 ResourceFieldRef: &core.ResourceFieldSelector{ 4661 ContainerName: "test-container", 4662 Resource: "requests.cpu", 4663 }, 4664 }, { 4665 Path: "memory_limit", 4666 ResourceFieldRef: &core.ResourceFieldSelector{ 4667 ContainerName: "test-container", 4668 Resource: "limits.memory", 4669 }, 4670 }, { 4671 Path: "memory_request", 4672 ResourceFieldRef: &core.ResourceFieldSelector{ 4673 ContainerName: "test-container", 4674 Resource: "requests.memory", 4675 }, 4676 }}, 4677 }, 4678 }, 4679 }, 4680 }, { 4681 name: "hugepages-downwardAPI-enabled", 4682 vol: core.Volume{ 4683 Name: "downwardapi", 4684 VolumeSource: core.VolumeSource{ 4685 DownwardAPI: &core.DownwardAPIVolumeSource{ 4686 Items: []core.DownwardAPIVolumeFile{{ 4687 Path: "hugepages_request", 4688 ResourceFieldRef: &core.ResourceFieldSelector{ 4689 ContainerName: "test-container", 4690 Resource: "requests.hugepages-2Mi", 4691 }, 4692 }, { 4693 Path: "hugepages_limit", 4694 ResourceFieldRef: &core.ResourceFieldSelector{ 4695 ContainerName: "test-container", 4696 Resource: "limits.hugepages-2Mi", 4697 }, 4698 }}, 4699 }, 4700 }, 4701 }, 4702 }, { 4703 name: "downapi valid defaultMode", 4704 vol: core.Volume{ 4705 Name: "downapi", 4706 VolumeSource: core.VolumeSource{ 4707 DownwardAPI: &core.DownwardAPIVolumeSource{ 4708 DefaultMode: utilpointer.Int32(0644), 4709 }, 4710 }, 4711 }, 4712 }, { 4713 name: "downapi valid item mode", 4714 vol: core.Volume{ 4715 Name: "downapi", 4716 VolumeSource: core.VolumeSource{ 4717 DownwardAPI: &core.DownwardAPIVolumeSource{ 4718 Items: []core.DownwardAPIVolumeFile{{ 4719 Mode: utilpointer.Int32(0644), 4720 Path: "path", 4721 FieldRef: &core.ObjectFieldSelector{ 4722 APIVersion: "v1", 4723 FieldPath: "metadata.labels", 4724 }, 4725 }}, 4726 }, 4727 }, 4728 }, 4729 }, { 4730 name: "downapi invalid positive item mode", 4731 vol: core.Volume{ 4732 Name: "downapi", 4733 VolumeSource: core.VolumeSource{ 4734 DownwardAPI: &core.DownwardAPIVolumeSource{ 4735 Items: []core.DownwardAPIVolumeFile{{ 4736 Mode: utilpointer.Int32(01000), 4737 Path: "path", 4738 FieldRef: &core.ObjectFieldSelector{ 4739 APIVersion: "v1", 4740 FieldPath: "metadata.labels", 4741 }, 4742 }}, 4743 }, 4744 }, 4745 }, 4746 errs: []verr{{ 4747 etype: field.ErrorTypeInvalid, 4748 field: "downwardAPI.mode", 4749 }}, 4750 }, { 4751 name: "downapi invalid negative item mode", 4752 vol: core.Volume{ 4753 Name: "downapi", 4754 VolumeSource: core.VolumeSource{ 4755 DownwardAPI: &core.DownwardAPIVolumeSource{ 4756 Items: []core.DownwardAPIVolumeFile{{ 4757 Mode: utilpointer.Int32(-1), 4758 Path: "path", 4759 FieldRef: &core.ObjectFieldSelector{ 4760 APIVersion: "v1", 4761 FieldPath: "metadata.labels", 4762 }, 4763 }}, 4764 }, 4765 }, 4766 }, 4767 errs: []verr{{ 4768 etype: field.ErrorTypeInvalid, 4769 field: "downwardAPI.mode", 4770 }}, 4771 }, { 4772 name: "downapi empty metatada path", 4773 vol: core.Volume{ 4774 Name: "downapi", 4775 VolumeSource: core.VolumeSource{ 4776 DownwardAPI: &core.DownwardAPIVolumeSource{ 4777 Items: []core.DownwardAPIVolumeFile{{ 4778 Path: "", 4779 FieldRef: &core.ObjectFieldSelector{ 4780 APIVersion: "v1", 4781 FieldPath: "metadata.labels", 4782 }, 4783 }}, 4784 }, 4785 }, 4786 }, 4787 errs: []verr{{ 4788 etype: field.ErrorTypeRequired, 4789 field: "downwardAPI.path", 4790 }}, 4791 }, { 4792 name: "downapi absolute path", 4793 vol: core.Volume{ 4794 Name: "downapi", 4795 VolumeSource: core.VolumeSource{ 4796 DownwardAPI: &core.DownwardAPIVolumeSource{ 4797 Items: []core.DownwardAPIVolumeFile{{ 4798 Path: "/absolutepath", 4799 FieldRef: &core.ObjectFieldSelector{ 4800 APIVersion: "v1", 4801 FieldPath: "metadata.labels", 4802 }, 4803 }}, 4804 }, 4805 }, 4806 }, 4807 errs: []verr{{ 4808 etype: field.ErrorTypeInvalid, 4809 field: "downwardAPI.path", 4810 }}, 4811 }, { 4812 name: "downapi dot dot path", 4813 vol: core.Volume{ 4814 Name: "downapi", 4815 VolumeSource: core.VolumeSource{ 4816 DownwardAPI: &core.DownwardAPIVolumeSource{ 4817 Items: []core.DownwardAPIVolumeFile{{ 4818 Path: "../../passwd", 4819 FieldRef: &core.ObjectFieldSelector{ 4820 APIVersion: "v1", 4821 FieldPath: "metadata.labels", 4822 }, 4823 }}, 4824 }, 4825 }, 4826 }, 4827 errs: []verr{{ 4828 etype: field.ErrorTypeInvalid, 4829 field: "downwardAPI.path", 4830 detail: `must not contain '..'`, 4831 }}, 4832 }, { 4833 name: "downapi dot dot file name", 4834 vol: core.Volume{ 4835 Name: "downapi", 4836 VolumeSource: core.VolumeSource{ 4837 DownwardAPI: &core.DownwardAPIVolumeSource{ 4838 Items: []core.DownwardAPIVolumeFile{{ 4839 Path: "..badFileName", 4840 FieldRef: &core.ObjectFieldSelector{ 4841 APIVersion: "v1", 4842 FieldPath: "metadata.labels", 4843 }, 4844 }}, 4845 }, 4846 }, 4847 }, 4848 errs: []verr{{ 4849 etype: field.ErrorTypeInvalid, 4850 field: "downwardAPI.path", 4851 detail: `must not start with '..'`, 4852 }}, 4853 }, { 4854 name: "downapi dot dot first level dirent", 4855 vol: core.Volume{ 4856 Name: "downapi", 4857 VolumeSource: core.VolumeSource{ 4858 DownwardAPI: &core.DownwardAPIVolumeSource{ 4859 Items: []core.DownwardAPIVolumeFile{{ 4860 Path: "..badDirName/goodFileName", 4861 FieldRef: &core.ObjectFieldSelector{ 4862 APIVersion: "v1", 4863 FieldPath: "metadata.labels", 4864 }, 4865 }}, 4866 }, 4867 }, 4868 }, 4869 errs: []verr{{ 4870 etype: field.ErrorTypeInvalid, 4871 field: "downwardAPI.path", 4872 detail: `must not start with '..'`, 4873 }}, 4874 }, { 4875 name: "downapi fieldRef and ResourceFieldRef together", 4876 vol: core.Volume{ 4877 Name: "downapi", 4878 VolumeSource: core.VolumeSource{ 4879 DownwardAPI: &core.DownwardAPIVolumeSource{ 4880 Items: []core.DownwardAPIVolumeFile{{ 4881 Path: "test", 4882 FieldRef: &core.ObjectFieldSelector{ 4883 APIVersion: "v1", 4884 FieldPath: "metadata.labels", 4885 }, 4886 ResourceFieldRef: &core.ResourceFieldSelector{ 4887 ContainerName: "test-container", 4888 Resource: "requests.memory", 4889 }, 4890 }}, 4891 }, 4892 }, 4893 }, 4894 errs: []verr{{ 4895 etype: field.ErrorTypeInvalid, 4896 field: "downwardAPI", 4897 detail: "fieldRef and resourceFieldRef can not be specified simultaneously", 4898 }}, 4899 }, { 4900 name: "downapi invalid positive defaultMode", 4901 vol: core.Volume{ 4902 Name: "downapi", 4903 VolumeSource: core.VolumeSource{ 4904 DownwardAPI: &core.DownwardAPIVolumeSource{ 4905 DefaultMode: utilpointer.Int32(01000), 4906 }, 4907 }, 4908 }, 4909 errs: []verr{{ 4910 etype: field.ErrorTypeInvalid, 4911 field: "downwardAPI.defaultMode", 4912 }}, 4913 }, { 4914 name: "downapi invalid negative defaultMode", 4915 vol: core.Volume{ 4916 Name: "downapi", 4917 VolumeSource: core.VolumeSource{ 4918 DownwardAPI: &core.DownwardAPIVolumeSource{ 4919 DefaultMode: utilpointer.Int32(-1), 4920 }, 4921 }, 4922 }, 4923 errs: []verr{{ 4924 etype: field.ErrorTypeInvalid, 4925 field: "downwardAPI.defaultMode", 4926 }}, 4927 }, 4928 // FC 4929 { 4930 name: "FC valid targetWWNs and lun", 4931 vol: core.Volume{ 4932 Name: "fc", 4933 VolumeSource: core.VolumeSource{ 4934 FC: &core.FCVolumeSource{ 4935 TargetWWNs: []string{"some_wwn"}, 4936 Lun: utilpointer.Int32(1), 4937 FSType: "ext4", 4938 ReadOnly: false, 4939 }, 4940 }, 4941 }, 4942 }, { 4943 name: "FC valid wwids", 4944 vol: core.Volume{ 4945 Name: "fc", 4946 VolumeSource: core.VolumeSource{ 4947 FC: &core.FCVolumeSource{ 4948 WWIDs: []string{"some_wwid"}, 4949 FSType: "ext4", 4950 ReadOnly: false, 4951 }, 4952 }, 4953 }, 4954 }, { 4955 name: "FC empty targetWWNs and wwids", 4956 vol: core.Volume{ 4957 Name: "fc", 4958 VolumeSource: core.VolumeSource{ 4959 FC: &core.FCVolumeSource{ 4960 TargetWWNs: []string{}, 4961 Lun: utilpointer.Int32(1), 4962 WWIDs: []string{}, 4963 FSType: "ext4", 4964 ReadOnly: false, 4965 }, 4966 }, 4967 }, 4968 errs: []verr{{ 4969 etype: field.ErrorTypeRequired, 4970 field: "fc.targetWWNs", 4971 detail: "must specify either targetWWNs or wwids", 4972 }}, 4973 }, { 4974 name: "FC invalid: both targetWWNs and wwids simultaneously", 4975 vol: core.Volume{ 4976 Name: "fc", 4977 VolumeSource: core.VolumeSource{ 4978 FC: &core.FCVolumeSource{ 4979 TargetWWNs: []string{"some_wwn"}, 4980 Lun: utilpointer.Int32(1), 4981 WWIDs: []string{"some_wwid"}, 4982 FSType: "ext4", 4983 ReadOnly: false, 4984 }, 4985 }, 4986 }, 4987 errs: []verr{{ 4988 etype: field.ErrorTypeInvalid, 4989 field: "fc.targetWWNs", 4990 detail: "targetWWNs and wwids can not be specified simultaneously", 4991 }}, 4992 }, { 4993 name: "FC valid targetWWNs and empty lun", 4994 vol: core.Volume{ 4995 Name: "fc", 4996 VolumeSource: core.VolumeSource{ 4997 FC: &core.FCVolumeSource{ 4998 TargetWWNs: []string{"wwn"}, 4999 Lun: nil, 5000 FSType: "ext4", 5001 ReadOnly: false, 5002 }, 5003 }, 5004 }, 5005 errs: []verr{{ 5006 etype: field.ErrorTypeRequired, 5007 field: "fc.lun", 5008 detail: "lun is required if targetWWNs is specified", 5009 }}, 5010 }, { 5011 name: "FC valid targetWWNs and invalid lun", 5012 vol: core.Volume{ 5013 Name: "fc", 5014 VolumeSource: core.VolumeSource{ 5015 FC: &core.FCVolumeSource{ 5016 TargetWWNs: []string{"wwn"}, 5017 Lun: utilpointer.Int32(256), 5018 FSType: "ext4", 5019 ReadOnly: false, 5020 }, 5021 }, 5022 }, 5023 errs: []verr{{ 5024 etype: field.ErrorTypeInvalid, 5025 field: "fc.lun", 5026 detail: validation.InclusiveRangeError(0, 255), 5027 }}, 5028 }, 5029 // FlexVolume 5030 { 5031 name: "valid FlexVolume", 5032 vol: core.Volume{ 5033 Name: "flex-volume", 5034 VolumeSource: core.VolumeSource{ 5035 FlexVolume: &core.FlexVolumeSource{ 5036 Driver: "kubernetes.io/blue", 5037 FSType: "ext4", 5038 }, 5039 }, 5040 }, 5041 }, 5042 // AzureFile 5043 { 5044 name: "valid AzureFile", 5045 vol: core.Volume{ 5046 Name: "azure-file", 5047 VolumeSource: core.VolumeSource{ 5048 AzureFile: &core.AzureFileVolumeSource{ 5049 SecretName: "key", 5050 ShareName: "share", 5051 ReadOnly: false, 5052 }, 5053 }, 5054 }, 5055 }, { 5056 name: "AzureFile empty secret", 5057 vol: core.Volume{ 5058 Name: "azure-file", 5059 VolumeSource: core.VolumeSource{ 5060 AzureFile: &core.AzureFileVolumeSource{ 5061 SecretName: "", 5062 ShareName: "share", 5063 ReadOnly: false, 5064 }, 5065 }, 5066 }, 5067 errs: []verr{{ 5068 etype: field.ErrorTypeRequired, 5069 field: "azureFile.secretName", 5070 }}, 5071 }, { 5072 name: "AzureFile empty share", 5073 vol: core.Volume{ 5074 Name: "azure-file", 5075 VolumeSource: core.VolumeSource{ 5076 AzureFile: &core.AzureFileVolumeSource{ 5077 SecretName: "name", 5078 ShareName: "", 5079 ReadOnly: false, 5080 }, 5081 }, 5082 }, 5083 errs: []verr{{ 5084 etype: field.ErrorTypeRequired, 5085 field: "azureFile.shareName", 5086 }}, 5087 }, 5088 // Quobyte 5089 { 5090 name: "valid Quobyte", 5091 vol: core.Volume{ 5092 Name: "quobyte", 5093 VolumeSource: core.VolumeSource{ 5094 Quobyte: &core.QuobyteVolumeSource{ 5095 Registry: "registry:7861", 5096 Volume: "volume", 5097 ReadOnly: false, 5098 User: "root", 5099 Group: "root", 5100 Tenant: "ThisIsSomeTenantUUID", 5101 }, 5102 }, 5103 }, 5104 }, { 5105 name: "empty registry quobyte", 5106 vol: core.Volume{ 5107 Name: "quobyte", 5108 VolumeSource: core.VolumeSource{ 5109 Quobyte: &core.QuobyteVolumeSource{ 5110 Volume: "/test", 5111 Tenant: "ThisIsSomeTenantUUID", 5112 }, 5113 }, 5114 }, 5115 errs: []verr{{ 5116 etype: field.ErrorTypeRequired, 5117 field: "quobyte.registry", 5118 }}, 5119 }, { 5120 name: "wrong format registry quobyte", 5121 vol: core.Volume{ 5122 Name: "quobyte", 5123 VolumeSource: core.VolumeSource{ 5124 Quobyte: &core.QuobyteVolumeSource{ 5125 Registry: "registry7861", 5126 Volume: "/test", 5127 Tenant: "ThisIsSomeTenantUUID", 5128 }, 5129 }, 5130 }, 5131 errs: []verr{{ 5132 etype: field.ErrorTypeInvalid, 5133 field: "quobyte.registry", 5134 }}, 5135 }, { 5136 name: "wrong format multiple registries quobyte", 5137 vol: core.Volume{ 5138 Name: "quobyte", 5139 VolumeSource: core.VolumeSource{ 5140 Quobyte: &core.QuobyteVolumeSource{ 5141 Registry: "registry:7861,reg2", 5142 Volume: "/test", 5143 Tenant: "ThisIsSomeTenantUUID", 5144 }, 5145 }, 5146 }, 5147 errs: []verr{{ 5148 etype: field.ErrorTypeInvalid, 5149 field: "quobyte.registry", 5150 }}, 5151 }, { 5152 name: "empty volume quobyte", 5153 vol: core.Volume{ 5154 Name: "quobyte", 5155 VolumeSource: core.VolumeSource{ 5156 Quobyte: &core.QuobyteVolumeSource{ 5157 Registry: "registry:7861", 5158 Tenant: "ThisIsSomeTenantUUID", 5159 }, 5160 }, 5161 }, 5162 errs: []verr{{ 5163 etype: field.ErrorTypeRequired, 5164 field: "quobyte.volume", 5165 }}, 5166 }, { 5167 name: "empty tenant quobyte", 5168 vol: core.Volume{ 5169 Name: "quobyte", 5170 VolumeSource: core.VolumeSource{ 5171 Quobyte: &core.QuobyteVolumeSource{ 5172 Registry: "registry:7861", 5173 Volume: "/test", 5174 Tenant: "", 5175 }, 5176 }, 5177 }, 5178 }, { 5179 name: "too long tenant quobyte", 5180 vol: core.Volume{ 5181 Name: "quobyte", 5182 VolumeSource: core.VolumeSource{ 5183 Quobyte: &core.QuobyteVolumeSource{ 5184 Registry: "registry:7861", 5185 Volume: "/test", 5186 Tenant: "this is too long to be a valid uuid so this test has to fail on the maximum length validation of the tenant.", 5187 }, 5188 }, 5189 }, 5190 errs: []verr{{ 5191 etype: field.ErrorTypeRequired, 5192 field: "quobyte.tenant", 5193 }}, 5194 }, 5195 // AzureDisk 5196 { 5197 name: "valid AzureDisk", 5198 vol: core.Volume{ 5199 Name: "azure-disk", 5200 VolumeSource: core.VolumeSource{ 5201 AzureDisk: &core.AzureDiskVolumeSource{ 5202 DiskName: "foo", 5203 DataDiskURI: "https://blob/vhds/bar.vhd", 5204 }, 5205 }, 5206 }, 5207 }, { 5208 name: "AzureDisk empty disk name", 5209 vol: core.Volume{ 5210 Name: "azure-disk", 5211 VolumeSource: core.VolumeSource{ 5212 AzureDisk: &core.AzureDiskVolumeSource{ 5213 DiskName: "", 5214 DataDiskURI: "https://blob/vhds/bar.vhd", 5215 }, 5216 }, 5217 }, 5218 errs: []verr{{ 5219 etype: field.ErrorTypeRequired, 5220 field: "azureDisk.diskName", 5221 }}, 5222 }, { 5223 name: "AzureDisk empty disk uri", 5224 vol: core.Volume{ 5225 Name: "azure-disk", 5226 VolumeSource: core.VolumeSource{ 5227 AzureDisk: &core.AzureDiskVolumeSource{ 5228 DiskName: "foo", 5229 DataDiskURI: "", 5230 }, 5231 }, 5232 }, 5233 errs: []verr{{ 5234 etype: field.ErrorTypeRequired, 5235 field: "azureDisk.diskURI", 5236 }}, 5237 }, 5238 // ScaleIO 5239 { 5240 name: "valid scaleio volume", 5241 vol: core.Volume{ 5242 Name: "scaleio-volume", 5243 VolumeSource: core.VolumeSource{ 5244 ScaleIO: &core.ScaleIOVolumeSource{ 5245 Gateway: "http://abcd/efg", 5246 System: "test-system", 5247 VolumeName: "test-vol-1", 5248 }, 5249 }, 5250 }, 5251 }, { 5252 name: "ScaleIO with empty name", 5253 vol: core.Volume{ 5254 Name: "scaleio-volume", 5255 VolumeSource: core.VolumeSource{ 5256 ScaleIO: &core.ScaleIOVolumeSource{ 5257 Gateway: "http://abcd/efg", 5258 System: "test-system", 5259 VolumeName: "", 5260 }, 5261 }, 5262 }, 5263 errs: []verr{{ 5264 etype: field.ErrorTypeRequired, 5265 field: "scaleIO.volumeName", 5266 }}, 5267 }, { 5268 name: "ScaleIO with empty gateway", 5269 vol: core.Volume{ 5270 Name: "scaleio-volume", 5271 VolumeSource: core.VolumeSource{ 5272 ScaleIO: &core.ScaleIOVolumeSource{ 5273 Gateway: "", 5274 System: "test-system", 5275 VolumeName: "test-vol-1", 5276 }, 5277 }, 5278 }, 5279 errs: []verr{{ 5280 etype: field.ErrorTypeRequired, 5281 field: "scaleIO.gateway", 5282 }}, 5283 }, { 5284 name: "ScaleIO with empty system", 5285 vol: core.Volume{ 5286 Name: "scaleio-volume", 5287 VolumeSource: core.VolumeSource{ 5288 ScaleIO: &core.ScaleIOVolumeSource{ 5289 Gateway: "http://agc/efg/gateway", 5290 System: "", 5291 VolumeName: "test-vol-1", 5292 }, 5293 }, 5294 }, 5295 errs: []verr{{ 5296 etype: field.ErrorTypeRequired, 5297 field: "scaleIO.system", 5298 }}, 5299 }, 5300 // ProjectedVolumeSource 5301 { 5302 name: "ProjectedVolumeSource more than one projection in a source", 5303 vol: core.Volume{ 5304 Name: "projected-volume", 5305 VolumeSource: core.VolumeSource{ 5306 Projected: &core.ProjectedVolumeSource{ 5307 Sources: []core.VolumeProjection{{ 5308 Secret: &core.SecretProjection{ 5309 LocalObjectReference: core.LocalObjectReference{ 5310 Name: "foo", 5311 }, 5312 }, 5313 }, { 5314 Secret: &core.SecretProjection{ 5315 LocalObjectReference: core.LocalObjectReference{ 5316 Name: "foo", 5317 }, 5318 }, 5319 DownwardAPI: &core.DownwardAPIProjection{}, 5320 }}, 5321 }, 5322 }, 5323 }, 5324 errs: []verr{{ 5325 etype: field.ErrorTypeForbidden, 5326 field: "projected.sources[1]", 5327 }}, 5328 }, { 5329 name: "ProjectedVolumeSource more than one projection in a source", 5330 vol: core.Volume{ 5331 Name: "projected-volume", 5332 VolumeSource: core.VolumeSource{ 5333 Projected: &core.ProjectedVolumeSource{ 5334 Sources: []core.VolumeProjection{{ 5335 Secret: &core.SecretProjection{}, 5336 }, { 5337 Secret: &core.SecretProjection{}, 5338 DownwardAPI: &core.DownwardAPIProjection{}, 5339 }}, 5340 }, 5341 }, 5342 }, 5343 errs: []verr{{ 5344 etype: field.ErrorTypeRequired, 5345 field: "projected.sources[0].secret.name", 5346 }, { 5347 etype: field.ErrorTypeRequired, 5348 field: "projected.sources[1].secret.name", 5349 }, { 5350 etype: field.ErrorTypeForbidden, 5351 field: "projected.sources[1]", 5352 }}, 5353 }, 5354 } 5355 5356 for _, tc := range testCases { 5357 t.Run(tc.name, func(t *testing.T) { 5358 names, errs := ValidateVolumes([]core.Volume{tc.vol}, nil, field.NewPath("field"), tc.opts) 5359 if len(errs) != len(tc.errs) { 5360 t.Fatalf("unexpected error(s): got %d, want %d: %v", len(tc.errs), len(errs), errs) 5361 } 5362 if len(errs) == 0 && (len(names) > 1 || !IsMatchedVolume(tc.vol.Name, names)) { 5363 t.Errorf("wrong names result: %v", names) 5364 } 5365 for i, err := range errs { 5366 expErr := tc.errs[i] 5367 if err.Type != expErr.etype { 5368 t.Errorf("unexpected error type:\n\twant: %q\n\t got: %q", expErr.etype, err.Type) 5369 } 5370 if !strings.HasSuffix(err.Field, "."+expErr.field) { 5371 t.Errorf("unexpected error field:\n\twant: %q\n\t got: %q", expErr.field, err.Field) 5372 } 5373 if !strings.Contains(err.Detail, expErr.detail) { 5374 t.Errorf("unexpected error detail:\n\twant: %q\n\t got: %q", expErr.detail, err.Detail) 5375 } 5376 } 5377 }) 5378 } 5379 5380 dupsCase := []core.Volume{ 5381 {Name: "abc", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 5382 {Name: "abc", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 5383 } 5384 _, errs := ValidateVolumes(dupsCase, nil, field.NewPath("field"), PodValidationOptions{}) 5385 if len(errs) == 0 { 5386 t.Errorf("expected error") 5387 } else if len(errs) != 1 { 5388 t.Errorf("expected 1 error, got %d: %v", len(errs), errs) 5389 } else if errs[0].Type != field.ErrorTypeDuplicate { 5390 t.Errorf("expected error type %v, got %v", field.ErrorTypeDuplicate, errs[0].Type) 5391 } 5392 5393 // Validate HugePages medium type for EmptyDir 5394 hugePagesCase := core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{Medium: core.StorageMediumHugePages}} 5395 5396 // Enable HugePages 5397 if errs := validateVolumeSource(&hugePagesCase, field.NewPath("field").Index(0), "working", nil, PodValidationOptions{}); len(errs) != 0 { 5398 t.Errorf("Unexpected error when HugePages feature is enabled.") 5399 } 5400 5401 } 5402 5403 func TestHugePagesIsolation(t *testing.T) { 5404 testCases := map[string]struct { 5405 pod *core.Pod 5406 expectError bool 5407 }{ 5408 "Valid: request hugepages-2Mi": { 5409 pod: &core.Pod{ 5410 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 5411 Spec: core.PodSpec{ 5412 Containers: []core.Container{{ 5413 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 5414 Resources: core.ResourceRequirements{ 5415 Requests: core.ResourceList{ 5416 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5417 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5418 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5419 }, 5420 Limits: core.ResourceList{ 5421 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5422 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5423 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5424 }, 5425 }, 5426 }}, 5427 RestartPolicy: core.RestartPolicyAlways, 5428 DNSPolicy: core.DNSClusterFirst, 5429 }, 5430 }, 5431 }, 5432 "Valid: request more than one hugepages size": { 5433 pod: &core.Pod{ 5434 ObjectMeta: metav1.ObjectMeta{Name: "hugepages-shared", Namespace: "ns"}, 5435 Spec: core.PodSpec{ 5436 Containers: []core.Container{{ 5437 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 5438 Resources: core.ResourceRequirements{ 5439 Requests: core.ResourceList{ 5440 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5441 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5442 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5443 core.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"), 5444 }, 5445 Limits: core.ResourceList{ 5446 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5447 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5448 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5449 core.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"), 5450 }, 5451 }, 5452 }}, 5453 RestartPolicy: core.RestartPolicyAlways, 5454 DNSPolicy: core.DNSClusterFirst, 5455 }, 5456 }, 5457 expectError: false, 5458 }, 5459 "Valid: request hugepages-1Gi, limit hugepages-2Mi and hugepages-1Gi": { 5460 pod: &core.Pod{ 5461 ObjectMeta: metav1.ObjectMeta{Name: "hugepages-multiple", Namespace: "ns"}, 5462 Spec: core.PodSpec{ 5463 Containers: []core.Container{{ 5464 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 5465 Resources: core.ResourceRequirements{ 5466 Requests: core.ResourceList{ 5467 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5468 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5469 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5470 core.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"), 5471 }, 5472 Limits: core.ResourceList{ 5473 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5474 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5475 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5476 core.ResourceName("hugepages-1Gi"): resource.MustParse("2Gi"), 5477 }, 5478 }, 5479 }}, 5480 RestartPolicy: core.RestartPolicyAlways, 5481 DNSPolicy: core.DNSClusterFirst, 5482 }, 5483 }, 5484 }, 5485 "Invalid: not requesting cpu and memory": { 5486 pod: &core.Pod{ 5487 ObjectMeta: metav1.ObjectMeta{Name: "hugepages-requireCpuOrMemory", Namespace: "ns"}, 5488 Spec: core.PodSpec{ 5489 Containers: []core.Container{{ 5490 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 5491 Resources: core.ResourceRequirements{ 5492 Requests: core.ResourceList{ 5493 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5494 }, 5495 Limits: core.ResourceList{ 5496 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5497 }, 5498 }, 5499 }}, 5500 RestartPolicy: core.RestartPolicyAlways, 5501 DNSPolicy: core.DNSClusterFirst, 5502 }, 5503 }, 5504 expectError: true, 5505 }, 5506 "Invalid: request 1Gi hugepages-2Mi but limit 2Gi": { 5507 pod: &core.Pod{ 5508 ObjectMeta: metav1.ObjectMeta{Name: "hugepages-shared", Namespace: "ns"}, 5509 Spec: core.PodSpec{ 5510 Containers: []core.Container{{ 5511 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 5512 Resources: core.ResourceRequirements{ 5513 Requests: core.ResourceList{ 5514 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5515 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5516 core.ResourceName("hugepages-2Mi"): resource.MustParse("1Gi"), 5517 }, 5518 Limits: core.ResourceList{ 5519 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 5520 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 5521 core.ResourceName("hugepages-2Mi"): resource.MustParse("2Gi"), 5522 }, 5523 }, 5524 }}, 5525 RestartPolicy: core.RestartPolicyAlways, 5526 DNSPolicy: core.DNSClusterFirst, 5527 }, 5528 }, 5529 expectError: true, 5530 }, 5531 } 5532 for tcName, tc := range testCases { 5533 t.Run(tcName, func(t *testing.T) { 5534 errs := ValidatePodCreate(tc.pod, PodValidationOptions{}) 5535 if tc.expectError && len(errs) == 0 { 5536 t.Errorf("Unexpected success") 5537 } 5538 if !tc.expectError && len(errs) != 0 { 5539 t.Errorf("Unexpected error(s): %v", errs) 5540 } 5541 }) 5542 } 5543 } 5544 5545 func TestPVCVolumeMode(t *testing.T) { 5546 block := core.PersistentVolumeBlock 5547 file := core.PersistentVolumeFilesystem 5548 fake := core.PersistentVolumeMode("fake") 5549 empty := core.PersistentVolumeMode("") 5550 5551 // Success Cases 5552 successCasesPVC := map[string]*core.PersistentVolumeClaim{ 5553 "valid block value": createTestVolModePVC(&block), 5554 "valid filesystem value": createTestVolModePVC(&file), 5555 "valid nil value": createTestVolModePVC(nil), 5556 } 5557 for k, v := range successCasesPVC { 5558 opts := ValidationOptionsForPersistentVolumeClaim(v, nil) 5559 if errs := ValidatePersistentVolumeClaim(v, opts); len(errs) != 0 { 5560 t.Errorf("expected success for %s", k) 5561 } 5562 } 5563 5564 // Error Cases 5565 errorCasesPVC := map[string]*core.PersistentVolumeClaim{ 5566 "invalid value": createTestVolModePVC(&fake), 5567 "empty value": createTestVolModePVC(&empty), 5568 } 5569 for k, v := range errorCasesPVC { 5570 opts := ValidationOptionsForPersistentVolumeClaim(v, nil) 5571 if errs := ValidatePersistentVolumeClaim(v, opts); len(errs) == 0 { 5572 t.Errorf("expected failure for %s", k) 5573 } 5574 } 5575 } 5576 5577 func TestPVVolumeMode(t *testing.T) { 5578 block := core.PersistentVolumeBlock 5579 file := core.PersistentVolumeFilesystem 5580 fake := core.PersistentVolumeMode("fake") 5581 empty := core.PersistentVolumeMode("") 5582 5583 // Success Cases 5584 successCasesPV := map[string]*core.PersistentVolume{ 5585 "valid block value": createTestVolModePV(&block), 5586 "valid filesystem value": createTestVolModePV(&file), 5587 "valid nil value": createTestVolModePV(nil), 5588 } 5589 for k, v := range successCasesPV { 5590 opts := ValidationOptionsForPersistentVolume(v, nil) 5591 if errs := ValidatePersistentVolume(v, opts); len(errs) != 0 { 5592 t.Errorf("expected success for %s", k) 5593 } 5594 } 5595 5596 // Error Cases 5597 errorCasesPV := map[string]*core.PersistentVolume{ 5598 "invalid value": createTestVolModePV(&fake), 5599 "empty value": createTestVolModePV(&empty), 5600 } 5601 for k, v := range errorCasesPV { 5602 opts := ValidationOptionsForPersistentVolume(v, nil) 5603 if errs := ValidatePersistentVolume(v, opts); len(errs) == 0 { 5604 t.Errorf("expected failure for %s", k) 5605 } 5606 } 5607 } 5608 5609 func createTestVolModePVC(vmode *core.PersistentVolumeMode) *core.PersistentVolumeClaim { 5610 validName := "valid-storage-class" 5611 5612 pvc := core.PersistentVolumeClaim{ 5613 ObjectMeta: metav1.ObjectMeta{ 5614 Name: "foo", 5615 Namespace: "default", 5616 }, 5617 Spec: core.PersistentVolumeClaimSpec{ 5618 Resources: core.VolumeResourceRequirements{ 5619 Requests: core.ResourceList{ 5620 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 5621 }, 5622 }, 5623 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 5624 StorageClassName: &validName, 5625 VolumeMode: vmode, 5626 }, 5627 } 5628 return &pvc 5629 } 5630 5631 func createTestVolModePV(vmode *core.PersistentVolumeMode) *core.PersistentVolume { 5632 5633 // PersistentVolume with VolumeMode set (valid and invalid) 5634 pv := core.PersistentVolume{ 5635 ObjectMeta: metav1.ObjectMeta{ 5636 Name: "foo", 5637 Namespace: "", 5638 }, 5639 Spec: core.PersistentVolumeSpec{ 5640 Capacity: core.ResourceList{ 5641 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 5642 }, 5643 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 5644 PersistentVolumeSource: core.PersistentVolumeSource{ 5645 HostPath: &core.HostPathVolumeSource{ 5646 Path: "/foo", 5647 Type: newHostPathType(string(core.HostPathDirectory)), 5648 }, 5649 }, 5650 StorageClassName: "test-storage-class", 5651 VolumeMode: vmode, 5652 }, 5653 } 5654 return &pv 5655 } 5656 5657 func createTestPV() *core.PersistentVolume { 5658 5659 // PersistentVolume with VolumeMode set (valid and invalid) 5660 pv := core.PersistentVolume{ 5661 ObjectMeta: metav1.ObjectMeta{ 5662 Name: "foo", 5663 Namespace: "", 5664 }, 5665 Spec: core.PersistentVolumeSpec{ 5666 Capacity: core.ResourceList{ 5667 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 5668 }, 5669 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 5670 PersistentVolumeSource: core.PersistentVolumeSource{ 5671 HostPath: &core.HostPathVolumeSource{ 5672 Path: "/foo", 5673 Type: newHostPathType(string(core.HostPathDirectory)), 5674 }, 5675 }, 5676 StorageClassName: "test-storage-class", 5677 }, 5678 } 5679 return &pv 5680 } 5681 5682 func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { 5683 5684 testCases := []core.VolumeSource{ 5685 {EmptyDir: &core.EmptyDirVolumeSource{SizeLimit: resource.NewQuantity(int64(5), resource.BinarySI)}}, 5686 } 5687 5688 for _, tc := range testCases { 5689 if errs := validateVolumeSource(&tc, field.NewPath("spec"), "tmpvol", nil, PodValidationOptions{}); len(errs) != 0 { 5690 t.Errorf("expected success: %v", errs) 5691 } 5692 } 5693 5694 containerLimitCase := core.ResourceRequirements{ 5695 Limits: core.ResourceList{ 5696 core.ResourceEphemeralStorage: *resource.NewMilliQuantity( 5697 int64(40000), 5698 resource.BinarySI), 5699 }, 5700 } 5701 if errs := ValidateResourceRequirements(&containerLimitCase, nil, field.NewPath("resources"), PodValidationOptions{}); len(errs) != 0 { 5702 t.Errorf("expected success: %v", errs) 5703 } 5704 } 5705 5706 func TestValidateResourceQuotaWithAlphaLocalStorageCapacityIsolation(t *testing.T) { 5707 spec := core.ResourceQuotaSpec{ 5708 Hard: core.ResourceList{ 5709 core.ResourceCPU: resource.MustParse("100"), 5710 core.ResourceMemory: resource.MustParse("10000"), 5711 core.ResourceRequestsCPU: resource.MustParse("100"), 5712 core.ResourceRequestsMemory: resource.MustParse("10000"), 5713 core.ResourceLimitsCPU: resource.MustParse("100"), 5714 core.ResourceLimitsMemory: resource.MustParse("10000"), 5715 core.ResourcePods: resource.MustParse("10"), 5716 core.ResourceServices: resource.MustParse("0"), 5717 core.ResourceReplicationControllers: resource.MustParse("10"), 5718 core.ResourceQuotas: resource.MustParse("10"), 5719 core.ResourceConfigMaps: resource.MustParse("10"), 5720 core.ResourceSecrets: resource.MustParse("10"), 5721 core.ResourceEphemeralStorage: resource.MustParse("10000"), 5722 core.ResourceRequestsEphemeralStorage: resource.MustParse("10000"), 5723 core.ResourceLimitsEphemeralStorage: resource.MustParse("10000"), 5724 }, 5725 } 5726 resourceQuota := &core.ResourceQuota{ 5727 ObjectMeta: metav1.ObjectMeta{ 5728 Name: "abc", 5729 Namespace: "foo", 5730 }, 5731 Spec: spec, 5732 } 5733 5734 if errs := ValidateResourceQuota(resourceQuota); len(errs) != 0 { 5735 t.Errorf("expected success: %v", errs) 5736 } 5737 } 5738 5739 func TestValidatePorts(t *testing.T) { 5740 successCase := []core.ContainerPort{ 5741 {Name: "abc", ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, 5742 {Name: "easy", ContainerPort: 82, Protocol: "TCP"}, 5743 {Name: "as", ContainerPort: 83, Protocol: "UDP"}, 5744 {Name: "do-re-me", ContainerPort: 84, Protocol: "SCTP"}, 5745 {ContainerPort: 85, Protocol: "TCP"}, 5746 } 5747 if errs := validateContainerPorts(successCase, field.NewPath("field")); len(errs) != 0 { 5748 t.Errorf("expected success: %v", errs) 5749 } 5750 5751 nonCanonicalCase := []core.ContainerPort{ 5752 {ContainerPort: 80, Protocol: "TCP"}, 5753 } 5754 if errs := validateContainerPorts(nonCanonicalCase, field.NewPath("field")); len(errs) != 0 { 5755 t.Errorf("expected success: %v", errs) 5756 } 5757 5758 errorCases := map[string]struct { 5759 P []core.ContainerPort 5760 T field.ErrorType 5761 F string 5762 D string 5763 }{ 5764 "name > 15 characters": { 5765 []core.ContainerPort{{Name: strings.Repeat("a", 16), ContainerPort: 80, Protocol: "TCP"}}, 5766 field.ErrorTypeInvalid, 5767 "name", "15", 5768 }, 5769 "name contains invalid characters": { 5770 []core.ContainerPort{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}}, 5771 field.ErrorTypeInvalid, 5772 "name", "alpha-numeric", 5773 }, 5774 "name is a number": { 5775 []core.ContainerPort{{Name: "80", ContainerPort: 80, Protocol: "TCP"}}, 5776 field.ErrorTypeInvalid, 5777 "name", "at least one letter", 5778 }, 5779 "name not unique": { 5780 []core.ContainerPort{ 5781 {Name: "abc", ContainerPort: 80, Protocol: "TCP"}, 5782 {Name: "abc", ContainerPort: 81, Protocol: "TCP"}, 5783 }, 5784 field.ErrorTypeDuplicate, 5785 "[1].name", "", 5786 }, 5787 "zero container port": { 5788 []core.ContainerPort{{ContainerPort: 0, Protocol: "TCP"}}, 5789 field.ErrorTypeRequired, 5790 "containerPort", "", 5791 }, 5792 "invalid container port": { 5793 []core.ContainerPort{{ContainerPort: 65536, Protocol: "TCP"}}, 5794 field.ErrorTypeInvalid, 5795 "containerPort", "between", 5796 }, 5797 "invalid host port": { 5798 []core.ContainerPort{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}}, 5799 field.ErrorTypeInvalid, 5800 "hostPort", "between", 5801 }, 5802 "invalid protocol case": { 5803 []core.ContainerPort{{ContainerPort: 80, Protocol: "tcp"}}, 5804 field.ErrorTypeNotSupported, 5805 "protocol", `supported values: "SCTP", "TCP", "UDP"`, 5806 }, 5807 "invalid protocol": { 5808 []core.ContainerPort{{ContainerPort: 80, Protocol: "ICMP"}}, 5809 field.ErrorTypeNotSupported, 5810 "protocol", `supported values: "SCTP", "TCP", "UDP"`, 5811 }, 5812 "protocol required": { 5813 []core.ContainerPort{{Name: "abc", ContainerPort: 80}}, 5814 field.ErrorTypeRequired, 5815 "protocol", "", 5816 }, 5817 } 5818 for k, v := range errorCases { 5819 errs := validateContainerPorts(v.P, field.NewPath("field")) 5820 if len(errs) == 0 { 5821 t.Errorf("expected failure for %s", k) 5822 } 5823 for i := range errs { 5824 if errs[i].Type != v.T { 5825 t.Errorf("%s: expected error to have type %q: %q", k, v.T, errs[i].Type) 5826 } 5827 if !strings.Contains(errs[i].Field, v.F) { 5828 t.Errorf("%s: expected error field %q: %q", k, v.F, errs[i].Field) 5829 } 5830 if !strings.Contains(errs[i].Detail, v.D) { 5831 t.Errorf("%s: expected error detail %q, got %q", k, v.D, errs[i].Detail) 5832 } 5833 } 5834 } 5835 } 5836 5837 func TestLocalStorageEnvWithFeatureGate(t *testing.T) { 5838 testCases := []core.EnvVar{{ 5839 Name: "ephemeral-storage-limits", 5840 ValueFrom: &core.EnvVarSource{ 5841 ResourceFieldRef: &core.ResourceFieldSelector{ 5842 ContainerName: "test-container", 5843 Resource: "limits.ephemeral-storage", 5844 }, 5845 }, 5846 }, { 5847 Name: "ephemeral-storage-requests", 5848 ValueFrom: &core.EnvVarSource{ 5849 ResourceFieldRef: &core.ResourceFieldSelector{ 5850 ContainerName: "test-container", 5851 Resource: "requests.ephemeral-storage", 5852 }, 5853 }, 5854 }, 5855 } 5856 for _, testCase := range testCases { 5857 if errs := validateEnvVarValueFrom(testCase, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { 5858 t.Errorf("expected success, got: %v", errs) 5859 } 5860 } 5861 } 5862 5863 func TestHugePagesEnv(t *testing.T) { 5864 testCases := []core.EnvVar{{ 5865 Name: "hugepages-limits", 5866 ValueFrom: &core.EnvVarSource{ 5867 ResourceFieldRef: &core.ResourceFieldSelector{ 5868 ContainerName: "test-container", 5869 Resource: "limits.hugepages-2Mi", 5870 }, 5871 }, 5872 }, { 5873 Name: "hugepages-requests", 5874 ValueFrom: &core.EnvVarSource{ 5875 ResourceFieldRef: &core.ResourceFieldSelector{ 5876 ContainerName: "test-container", 5877 Resource: "requests.hugepages-2Mi", 5878 }, 5879 }, 5880 }, 5881 } 5882 // enable gate 5883 for _, testCase := range testCases { 5884 t.Run(testCase.Name, func(t *testing.T) { 5885 opts := PodValidationOptions{} 5886 if errs := validateEnvVarValueFrom(testCase, field.NewPath("field"), opts); len(errs) != 0 { 5887 t.Errorf("expected success, got: %v", errs) 5888 } 5889 }) 5890 } 5891 } 5892 5893 func TestRelaxedValidateEnv(t *testing.T) { 5894 successCase := []core.EnvVar{ 5895 {Name: "!\"#$%&'()", Value: "value"}, 5896 {Name: "* +,-./0123456789", Value: "value"}, 5897 {Name: ":;<>?@", Value: "value"}, 5898 {Name: "ABCDEFG", Value: "value"}, 5899 {Name: "abcdefghijklmn", Value: "value"}, 5900 {Name: "[\\]^_`{}|~", Value: "value"}, 5901 { 5902 Name: "!\"#$%&'()", 5903 ValueFrom: &core.EnvVarSource{ 5904 FieldRef: &core.ObjectFieldSelector{ 5905 APIVersion: "v1", 5906 FieldPath: "metadata.annotations['key']", 5907 }, 5908 }, 5909 }, { 5910 Name: "!\"#$%&'()", 5911 ValueFrom: &core.EnvVarSource{ 5912 FieldRef: &core.ObjectFieldSelector{ 5913 APIVersion: "v1", 5914 FieldPath: "metadata.labels['key']", 5915 }, 5916 }, 5917 }, { 5918 Name: "* +,-./0123456789", 5919 ValueFrom: &core.EnvVarSource{ 5920 FieldRef: &core.ObjectFieldSelector{ 5921 APIVersion: "v1", 5922 FieldPath: "metadata.name", 5923 }, 5924 }, 5925 }, { 5926 Name: "* +,-./0123456789", 5927 ValueFrom: &core.EnvVarSource{ 5928 FieldRef: &core.ObjectFieldSelector{ 5929 APIVersion: "v1", 5930 FieldPath: "metadata.namespace", 5931 }, 5932 }, 5933 }, { 5934 Name: "* +,-./0123456789", 5935 ValueFrom: &core.EnvVarSource{ 5936 FieldRef: &core.ObjectFieldSelector{ 5937 APIVersion: "v1", 5938 FieldPath: "metadata.uid", 5939 }, 5940 }, 5941 }, { 5942 Name: ":;<>?@", 5943 ValueFrom: &core.EnvVarSource{ 5944 FieldRef: &core.ObjectFieldSelector{ 5945 APIVersion: "v1", 5946 FieldPath: "spec.nodeName", 5947 }, 5948 }, 5949 }, { 5950 Name: ":;<>?@", 5951 ValueFrom: &core.EnvVarSource{ 5952 FieldRef: &core.ObjectFieldSelector{ 5953 APIVersion: "v1", 5954 FieldPath: "spec.serviceAccountName", 5955 }, 5956 }, 5957 }, { 5958 Name: ":;<>?@", 5959 ValueFrom: &core.EnvVarSource{ 5960 FieldRef: &core.ObjectFieldSelector{ 5961 APIVersion: "v1", 5962 FieldPath: "status.hostIP", 5963 }, 5964 }, 5965 }, { 5966 Name: ":;<>?@", 5967 ValueFrom: &core.EnvVarSource{ 5968 FieldRef: &core.ObjectFieldSelector{ 5969 APIVersion: "v1", 5970 FieldPath: "status.podIP", 5971 }, 5972 }, 5973 }, { 5974 Name: "abcdefghijklmn", 5975 ValueFrom: &core.EnvVarSource{ 5976 FieldRef: &core.ObjectFieldSelector{ 5977 APIVersion: "v1", 5978 FieldPath: "status.podIPs", 5979 }, 5980 }, 5981 }, 5982 { 5983 Name: "abcdefghijklmn", 5984 ValueFrom: &core.EnvVarSource{ 5985 SecretKeyRef: &core.SecretKeySelector{ 5986 LocalObjectReference: core.LocalObjectReference{ 5987 Name: "some-secret", 5988 }, 5989 Key: "secret-key", 5990 }, 5991 }, 5992 }, { 5993 Name: "!\"#$%&'()", 5994 ValueFrom: &core.EnvVarSource{ 5995 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 5996 LocalObjectReference: core.LocalObjectReference{ 5997 Name: "some-config-map", 5998 }, 5999 Key: "some-key", 6000 }, 6001 }, 6002 }, 6003 } 6004 if errs := ValidateEnv(successCase, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) != 0 { 6005 t.Errorf("expected success, got: %v", errs) 6006 } 6007 6008 errorCases := []struct { 6009 name string 6010 envs []core.EnvVar 6011 expectedError string 6012 }{{ 6013 name: "illegal character", 6014 envs: []core.EnvVar{{Name: "=abc"}}, 6015 expectedError: `[0].name: Invalid value: "=abc": ` + relaxedEnvVarNameFmtErrMsg, 6016 }, { 6017 name: "zero-length name", 6018 envs: []core.EnvVar{{Name: ""}}, 6019 expectedError: "[0].name: Required value", 6020 }, { 6021 name: "value and valueFrom specified", 6022 envs: []core.EnvVar{{ 6023 Name: "abc", 6024 Value: "foo", 6025 ValueFrom: &core.EnvVarSource{ 6026 FieldRef: &core.ObjectFieldSelector{ 6027 APIVersion: "v1", 6028 FieldPath: "metadata.name", 6029 }, 6030 }, 6031 }}, 6032 expectedError: "[0].valueFrom: Invalid value: \"\": may not be specified when `value` is not empty", 6033 }, { 6034 name: "valueFrom without a source", 6035 envs: []core.EnvVar{{ 6036 Name: "abc", 6037 ValueFrom: &core.EnvVarSource{}, 6038 }}, 6039 expectedError: "[0].valueFrom: Invalid value: \"\": must specify one of: `fieldRef`, `resourceFieldRef`, `configMapKeyRef` or `secretKeyRef`", 6040 }, { 6041 name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified", 6042 envs: []core.EnvVar{{ 6043 Name: "abc", 6044 ValueFrom: &core.EnvVarSource{ 6045 FieldRef: &core.ObjectFieldSelector{ 6046 APIVersion: "v1", 6047 FieldPath: "metadata.name", 6048 }, 6049 SecretKeyRef: &core.SecretKeySelector{ 6050 LocalObjectReference: core.LocalObjectReference{ 6051 Name: "a-secret", 6052 }, 6053 Key: "a-key", 6054 }, 6055 }, 6056 }}, 6057 expectedError: "[0].valueFrom: Invalid value: \"\": may not have more than one field specified at a time", 6058 }, { 6059 name: "valueFrom.fieldRef and valueFrom.configMapKeyRef set", 6060 envs: []core.EnvVar{{ 6061 Name: "some_var_name", 6062 ValueFrom: &core.EnvVarSource{ 6063 FieldRef: &core.ObjectFieldSelector{ 6064 APIVersion: "v1", 6065 FieldPath: "metadata.name", 6066 }, 6067 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6068 LocalObjectReference: core.LocalObjectReference{ 6069 Name: "some-config-map", 6070 }, 6071 Key: "some-key", 6072 }, 6073 }, 6074 }}, 6075 expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`, 6076 }, { 6077 name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified", 6078 envs: []core.EnvVar{{ 6079 Name: "abc", 6080 ValueFrom: &core.EnvVarSource{ 6081 FieldRef: &core.ObjectFieldSelector{ 6082 APIVersion: "v1", 6083 FieldPath: "metadata.name", 6084 }, 6085 SecretKeyRef: &core.SecretKeySelector{ 6086 LocalObjectReference: core.LocalObjectReference{ 6087 Name: "a-secret", 6088 }, 6089 Key: "a-key", 6090 }, 6091 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6092 LocalObjectReference: core.LocalObjectReference{ 6093 Name: "some-config-map", 6094 }, 6095 Key: "some-key", 6096 }, 6097 }, 6098 }}, 6099 expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`, 6100 }, { 6101 name: "valueFrom.secretKeyRef.name invalid", 6102 envs: []core.EnvVar{{ 6103 Name: "abc", 6104 ValueFrom: &core.EnvVarSource{ 6105 SecretKeyRef: &core.SecretKeySelector{ 6106 LocalObjectReference: core.LocalObjectReference{ 6107 Name: "$%^&*#", 6108 }, 6109 Key: "a-key", 6110 }, 6111 }, 6112 }}, 6113 }, { 6114 name: "valueFrom.configMapKeyRef.name invalid", 6115 envs: []core.EnvVar{{ 6116 Name: "abc", 6117 ValueFrom: &core.EnvVarSource{ 6118 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6119 LocalObjectReference: core.LocalObjectReference{ 6120 Name: "$%^&*#", 6121 }, 6122 Key: "some-key", 6123 }, 6124 }, 6125 }}, 6126 }, { 6127 name: "missing FieldPath on ObjectFieldSelector", 6128 envs: []core.EnvVar{{ 6129 Name: "abc", 6130 ValueFrom: &core.EnvVarSource{ 6131 FieldRef: &core.ObjectFieldSelector{ 6132 APIVersion: "v1", 6133 }, 6134 }, 6135 }}, 6136 expectedError: `[0].valueFrom.fieldRef.fieldPath: Required value`, 6137 }, { 6138 name: "missing APIVersion on ObjectFieldSelector", 6139 envs: []core.EnvVar{{ 6140 Name: "abc", 6141 ValueFrom: &core.EnvVarSource{ 6142 FieldRef: &core.ObjectFieldSelector{ 6143 FieldPath: "metadata.name", 6144 }, 6145 }, 6146 }}, 6147 expectedError: `[0].valueFrom.fieldRef.apiVersion: Required value`, 6148 }, { 6149 name: "invalid fieldPath", 6150 envs: []core.EnvVar{{ 6151 Name: "abc", 6152 ValueFrom: &core.EnvVarSource{ 6153 FieldRef: &core.ObjectFieldSelector{ 6154 FieldPath: "metadata.whoops", 6155 APIVersion: "v1", 6156 }, 6157 }, 6158 }}, 6159 expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`, 6160 }, { 6161 name: "metadata.name with subscript", 6162 envs: []core.EnvVar{{ 6163 Name: "labels", 6164 ValueFrom: &core.EnvVarSource{ 6165 FieldRef: &core.ObjectFieldSelector{ 6166 FieldPath: "metadata.name['key']", 6167 APIVersion: "v1", 6168 }, 6169 }, 6170 }}, 6171 expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.name['key']": error converting fieldPath: field label does not support subscript`, 6172 }, { 6173 name: "metadata.labels without subscript", 6174 envs: []core.EnvVar{{ 6175 Name: "labels", 6176 ValueFrom: &core.EnvVarSource{ 6177 FieldRef: &core.ObjectFieldSelector{ 6178 FieldPath: "metadata.labels", 6179 APIVersion: "v1", 6180 }, 6181 }, 6182 }}, 6183 expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6184 }, { 6185 name: "metadata.annotations without subscript", 6186 envs: []core.EnvVar{{ 6187 Name: "abc", 6188 ValueFrom: &core.EnvVarSource{ 6189 FieldRef: &core.ObjectFieldSelector{ 6190 FieldPath: "metadata.annotations", 6191 APIVersion: "v1", 6192 }, 6193 }, 6194 }}, 6195 expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6196 }, { 6197 name: "metadata.annotations with invalid key", 6198 envs: []core.EnvVar{{ 6199 Name: "abc", 6200 ValueFrom: &core.EnvVarSource{ 6201 FieldRef: &core.ObjectFieldSelector{ 6202 FieldPath: "metadata.annotations['invalid~key']", 6203 APIVersion: "v1", 6204 }, 6205 }, 6206 }}, 6207 expectedError: `field[0].valueFrom.fieldRef: Invalid value: "invalid~key"`, 6208 }, { 6209 name: "metadata.labels with invalid key", 6210 envs: []core.EnvVar{{ 6211 Name: "abc", 6212 ValueFrom: &core.EnvVarSource{ 6213 FieldRef: &core.ObjectFieldSelector{ 6214 FieldPath: "metadata.labels['Www.k8s.io/test']", 6215 APIVersion: "v1", 6216 }, 6217 }, 6218 }}, 6219 expectedError: `field[0].valueFrom.fieldRef: Invalid value: "Www.k8s.io/test"`, 6220 }, { 6221 name: "unsupported fieldPath", 6222 envs: []core.EnvVar{{ 6223 Name: "abc", 6224 ValueFrom: &core.EnvVarSource{ 6225 FieldRef: &core.ObjectFieldSelector{ 6226 FieldPath: "status.phase", 6227 APIVersion: "v1", 6228 }, 6229 }, 6230 }}, 6231 expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6232 }, 6233 } 6234 for _, tc := range errorCases { 6235 if errs := ValidateEnv(tc.envs, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) == 0 { 6236 t.Errorf("expected failure for %s", tc.name) 6237 } else { 6238 for i := range errs { 6239 str := errs[i].Error() 6240 if str != "" && !strings.Contains(str, tc.expectedError) { 6241 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6242 } 6243 } 6244 } 6245 } 6246 } 6247 6248 func TestValidateEnv(t *testing.T) { 6249 successCase := []core.EnvVar{ 6250 {Name: "abc", Value: "value"}, 6251 {Name: "ABC", Value: "value"}, 6252 {Name: "AbC_123", Value: "value"}, 6253 {Name: "abc", Value: ""}, 6254 {Name: "a.b.c", Value: "value"}, 6255 {Name: "a-b-c", Value: "value"}, { 6256 Name: "abc", 6257 ValueFrom: &core.EnvVarSource{ 6258 FieldRef: &core.ObjectFieldSelector{ 6259 APIVersion: "v1", 6260 FieldPath: "metadata.annotations['key']", 6261 }, 6262 }, 6263 }, { 6264 Name: "abc", 6265 ValueFrom: &core.EnvVarSource{ 6266 FieldRef: &core.ObjectFieldSelector{ 6267 APIVersion: "v1", 6268 FieldPath: "metadata.labels['key']", 6269 }, 6270 }, 6271 }, { 6272 Name: "abc", 6273 ValueFrom: &core.EnvVarSource{ 6274 FieldRef: &core.ObjectFieldSelector{ 6275 APIVersion: "v1", 6276 FieldPath: "metadata.name", 6277 }, 6278 }, 6279 }, { 6280 Name: "abc", 6281 ValueFrom: &core.EnvVarSource{ 6282 FieldRef: &core.ObjectFieldSelector{ 6283 APIVersion: "v1", 6284 FieldPath: "metadata.namespace", 6285 }, 6286 }, 6287 }, { 6288 Name: "abc", 6289 ValueFrom: &core.EnvVarSource{ 6290 FieldRef: &core.ObjectFieldSelector{ 6291 APIVersion: "v1", 6292 FieldPath: "metadata.uid", 6293 }, 6294 }, 6295 }, { 6296 Name: "abc", 6297 ValueFrom: &core.EnvVarSource{ 6298 FieldRef: &core.ObjectFieldSelector{ 6299 APIVersion: "v1", 6300 FieldPath: "spec.nodeName", 6301 }, 6302 }, 6303 }, { 6304 Name: "abc", 6305 ValueFrom: &core.EnvVarSource{ 6306 FieldRef: &core.ObjectFieldSelector{ 6307 APIVersion: "v1", 6308 FieldPath: "spec.serviceAccountName", 6309 }, 6310 }, 6311 }, { 6312 Name: "abc", 6313 ValueFrom: &core.EnvVarSource{ 6314 FieldRef: &core.ObjectFieldSelector{ 6315 APIVersion: "v1", 6316 FieldPath: "status.hostIP", 6317 }, 6318 }, 6319 }, { 6320 Name: "abc", 6321 ValueFrom: &core.EnvVarSource{ 6322 FieldRef: &core.ObjectFieldSelector{ 6323 APIVersion: "v1", 6324 FieldPath: "status.podIP", 6325 }, 6326 }, 6327 }, { 6328 Name: "abc", 6329 ValueFrom: &core.EnvVarSource{ 6330 FieldRef: &core.ObjectFieldSelector{ 6331 APIVersion: "v1", 6332 FieldPath: "status.podIPs", 6333 }, 6334 }, 6335 }, { 6336 Name: "secret_value", 6337 ValueFrom: &core.EnvVarSource{ 6338 SecretKeyRef: &core.SecretKeySelector{ 6339 LocalObjectReference: core.LocalObjectReference{ 6340 Name: "some-secret", 6341 }, 6342 Key: "secret-key", 6343 }, 6344 }, 6345 }, { 6346 Name: "ENV_VAR_1", 6347 ValueFrom: &core.EnvVarSource{ 6348 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6349 LocalObjectReference: core.LocalObjectReference{ 6350 Name: "some-config-map", 6351 }, 6352 Key: "some-key", 6353 }, 6354 }, 6355 }, 6356 } 6357 if errs := ValidateEnv(successCase, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { 6358 t.Errorf("expected success, got: %v", errs) 6359 } 6360 6361 updateSuccessCase := []core.EnvVar{ 6362 {Name: "!\"#$%&'()", Value: "value"}, 6363 {Name: "* +,-./0123456789", Value: "value"}, 6364 {Name: ":;<>?@", Value: "value"}, 6365 {Name: "ABCDEFG", Value: "value"}, 6366 {Name: "abcdefghijklmn", Value: "value"}, 6367 {Name: "[\\]^_`{}|~", Value: "value"}, 6368 } 6369 6370 if errs := ValidateEnv(updateSuccessCase, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) != 0 { 6371 t.Errorf("expected success, got: %v", errs) 6372 } 6373 6374 updateErrorCase := []struct { 6375 name string 6376 envs []core.EnvVar 6377 expectedError string 6378 }{ 6379 { 6380 name: "invalid name a", 6381 envs: []core.EnvVar{ 6382 {Name: "!\"#$%&'()", Value: "value"}, 6383 }, 6384 expectedError: `field[0].name: Invalid value: ` + "\"!\\\"#$%&'()\": " + envVarNameErrMsg, 6385 }, 6386 { 6387 name: "invalid name b", 6388 envs: []core.EnvVar{ 6389 {Name: "* +,-./0123456789", Value: "value"}, 6390 }, 6391 expectedError: `field[0].name: Invalid value: ` + "\"* +,-./0123456789\": " + envVarNameErrMsg, 6392 }, 6393 { 6394 name: "invalid name c", 6395 envs: []core.EnvVar{ 6396 {Name: ":;<>?@", Value: "value"}, 6397 }, 6398 expectedError: `field[0].name: Invalid value: ` + "\":;<>?@\": " + envVarNameErrMsg, 6399 }, 6400 { 6401 name: "invalid name d", 6402 envs: []core.EnvVar{ 6403 {Name: "[\\]^_{}|~", Value: "value"}, 6404 }, 6405 expectedError: `field[0].name: Invalid value: ` + "\"[\\\\]^_{}|~\": " + envVarNameErrMsg, 6406 }, 6407 } 6408 6409 for _, tc := range updateErrorCase { 6410 if errs := ValidateEnv(tc.envs, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { 6411 t.Errorf("expected failure for %s", tc.name) 6412 } else { 6413 for i := range errs { 6414 str := errs[i].Error() 6415 if str != "" && !strings.Contains(str, tc.expectedError) { 6416 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6417 } 6418 } 6419 } 6420 } 6421 6422 errorCases := []struct { 6423 name string 6424 envs []core.EnvVar 6425 expectedError string 6426 }{{ 6427 name: "zero-length name", 6428 envs: []core.EnvVar{{Name: ""}}, 6429 expectedError: "[0].name: Required value", 6430 }, { 6431 name: "value and valueFrom specified", 6432 envs: []core.EnvVar{{ 6433 Name: "abc", 6434 Value: "foo", 6435 ValueFrom: &core.EnvVarSource{ 6436 FieldRef: &core.ObjectFieldSelector{ 6437 APIVersion: "v1", 6438 FieldPath: "metadata.name", 6439 }, 6440 }, 6441 }}, 6442 expectedError: "[0].valueFrom: Invalid value: \"\": may not be specified when `value` is not empty", 6443 }, { 6444 name: "valueFrom without a source", 6445 envs: []core.EnvVar{{ 6446 Name: "abc", 6447 ValueFrom: &core.EnvVarSource{}, 6448 }}, 6449 expectedError: "[0].valueFrom: Invalid value: \"\": must specify one of: `fieldRef`, `resourceFieldRef`, `configMapKeyRef` or `secretKeyRef`", 6450 }, { 6451 name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified", 6452 envs: []core.EnvVar{{ 6453 Name: "abc", 6454 ValueFrom: &core.EnvVarSource{ 6455 FieldRef: &core.ObjectFieldSelector{ 6456 APIVersion: "v1", 6457 FieldPath: "metadata.name", 6458 }, 6459 SecretKeyRef: &core.SecretKeySelector{ 6460 LocalObjectReference: core.LocalObjectReference{ 6461 Name: "a-secret", 6462 }, 6463 Key: "a-key", 6464 }, 6465 }, 6466 }}, 6467 expectedError: "[0].valueFrom: Invalid value: \"\": may not have more than one field specified at a time", 6468 }, { 6469 name: "valueFrom.fieldRef and valueFrom.configMapKeyRef set", 6470 envs: []core.EnvVar{{ 6471 Name: "some_var_name", 6472 ValueFrom: &core.EnvVarSource{ 6473 FieldRef: &core.ObjectFieldSelector{ 6474 APIVersion: "v1", 6475 FieldPath: "metadata.name", 6476 }, 6477 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6478 LocalObjectReference: core.LocalObjectReference{ 6479 Name: "some-config-map", 6480 }, 6481 Key: "some-key", 6482 }, 6483 }, 6484 }}, 6485 expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`, 6486 }, { 6487 name: "valueFrom.fieldRef and valueFrom.secretKeyRef specified", 6488 envs: []core.EnvVar{{ 6489 Name: "abc", 6490 ValueFrom: &core.EnvVarSource{ 6491 FieldRef: &core.ObjectFieldSelector{ 6492 APIVersion: "v1", 6493 FieldPath: "metadata.name", 6494 }, 6495 SecretKeyRef: &core.SecretKeySelector{ 6496 LocalObjectReference: core.LocalObjectReference{ 6497 Name: "a-secret", 6498 }, 6499 Key: "a-key", 6500 }, 6501 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6502 LocalObjectReference: core.LocalObjectReference{ 6503 Name: "some-config-map", 6504 }, 6505 Key: "some-key", 6506 }, 6507 }, 6508 }}, 6509 expectedError: `[0].valueFrom: Invalid value: "": may not have more than one field specified at a time`, 6510 }, { 6511 name: "valueFrom.secretKeyRef.name invalid", 6512 envs: []core.EnvVar{{ 6513 Name: "abc", 6514 ValueFrom: &core.EnvVarSource{ 6515 SecretKeyRef: &core.SecretKeySelector{ 6516 LocalObjectReference: core.LocalObjectReference{ 6517 Name: "$%^&*#", 6518 }, 6519 Key: "a-key", 6520 }, 6521 }, 6522 }}, 6523 }, { 6524 name: "valueFrom.configMapKeyRef.name invalid", 6525 envs: []core.EnvVar{{ 6526 Name: "abc", 6527 ValueFrom: &core.EnvVarSource{ 6528 ConfigMapKeyRef: &core.ConfigMapKeySelector{ 6529 LocalObjectReference: core.LocalObjectReference{ 6530 Name: "$%^&*#", 6531 }, 6532 Key: "some-key", 6533 }, 6534 }, 6535 }}, 6536 }, { 6537 name: "missing FieldPath on ObjectFieldSelector", 6538 envs: []core.EnvVar{{ 6539 Name: "abc", 6540 ValueFrom: &core.EnvVarSource{ 6541 FieldRef: &core.ObjectFieldSelector{ 6542 APIVersion: "v1", 6543 }, 6544 }, 6545 }}, 6546 expectedError: `[0].valueFrom.fieldRef.fieldPath: Required value`, 6547 }, { 6548 name: "missing APIVersion on ObjectFieldSelector", 6549 envs: []core.EnvVar{{ 6550 Name: "abc", 6551 ValueFrom: &core.EnvVarSource{ 6552 FieldRef: &core.ObjectFieldSelector{ 6553 FieldPath: "metadata.name", 6554 }, 6555 }, 6556 }}, 6557 expectedError: `[0].valueFrom.fieldRef.apiVersion: Required value`, 6558 }, { 6559 name: "invalid fieldPath", 6560 envs: []core.EnvVar{{ 6561 Name: "abc", 6562 ValueFrom: &core.EnvVarSource{ 6563 FieldRef: &core.ObjectFieldSelector{ 6564 FieldPath: "metadata.whoops", 6565 APIVersion: "v1", 6566 }, 6567 }, 6568 }}, 6569 expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.whoops": error converting fieldPath`, 6570 }, { 6571 name: "metadata.name with subscript", 6572 envs: []core.EnvVar{{ 6573 Name: "labels", 6574 ValueFrom: &core.EnvVarSource{ 6575 FieldRef: &core.ObjectFieldSelector{ 6576 FieldPath: "metadata.name['key']", 6577 APIVersion: "v1", 6578 }, 6579 }, 6580 }}, 6581 expectedError: `[0].valueFrom.fieldRef.fieldPath: Invalid value: "metadata.name['key']": error converting fieldPath: field label does not support subscript`, 6582 }, { 6583 name: "metadata.labels without subscript", 6584 envs: []core.EnvVar{{ 6585 Name: "labels", 6586 ValueFrom: &core.EnvVarSource{ 6587 FieldRef: &core.ObjectFieldSelector{ 6588 FieldPath: "metadata.labels", 6589 APIVersion: "v1", 6590 }, 6591 }, 6592 }}, 6593 expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.labels": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6594 }, { 6595 name: "metadata.annotations without subscript", 6596 envs: []core.EnvVar{{ 6597 Name: "abc", 6598 ValueFrom: &core.EnvVarSource{ 6599 FieldRef: &core.ObjectFieldSelector{ 6600 FieldPath: "metadata.annotations", 6601 APIVersion: "v1", 6602 }, 6603 }, 6604 }}, 6605 expectedError: `[0].valueFrom.fieldRef.fieldPath: Unsupported value: "metadata.annotations": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6606 }, { 6607 name: "metadata.annotations with invalid key", 6608 envs: []core.EnvVar{{ 6609 Name: "abc", 6610 ValueFrom: &core.EnvVarSource{ 6611 FieldRef: &core.ObjectFieldSelector{ 6612 FieldPath: "metadata.annotations['invalid~key']", 6613 APIVersion: "v1", 6614 }, 6615 }, 6616 }}, 6617 expectedError: `field[0].valueFrom.fieldRef: Invalid value: "invalid~key"`, 6618 }, { 6619 name: "metadata.labels with invalid key", 6620 envs: []core.EnvVar{{ 6621 Name: "abc", 6622 ValueFrom: &core.EnvVarSource{ 6623 FieldRef: &core.ObjectFieldSelector{ 6624 FieldPath: "metadata.labels['Www.k8s.io/test']", 6625 APIVersion: "v1", 6626 }, 6627 }, 6628 }}, 6629 expectedError: `field[0].valueFrom.fieldRef: Invalid value: "Www.k8s.io/test"`, 6630 }, { 6631 name: "unsupported fieldPath", 6632 envs: []core.EnvVar{{ 6633 Name: "abc", 6634 ValueFrom: &core.EnvVarSource{ 6635 FieldRef: &core.ObjectFieldSelector{ 6636 FieldPath: "status.phase", 6637 APIVersion: "v1", 6638 }, 6639 }, 6640 }}, 6641 expectedError: `valueFrom.fieldRef.fieldPath: Unsupported value: "status.phase": supported values: "metadata.name", "metadata.namespace", "metadata.uid", "spec.nodeName", "spec.serviceAccountName", "status.hostIP", "status.hostIPs", "status.podIP", "status.podIPs"`, 6642 }, 6643 } 6644 for _, tc := range errorCases { 6645 if errs := ValidateEnv(tc.envs, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { 6646 t.Errorf("expected failure for %s", tc.name) 6647 } else { 6648 for i := range errs { 6649 str := errs[i].Error() 6650 if str != "" && !strings.Contains(str, tc.expectedError) { 6651 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6652 } 6653 } 6654 } 6655 } 6656 } 6657 6658 func TestValidateEnvFrom(t *testing.T) { 6659 successCase := []core.EnvFromSource{{ 6660 ConfigMapRef: &core.ConfigMapEnvSource{ 6661 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6662 }, 6663 }, { 6664 Prefix: "pre_", 6665 ConfigMapRef: &core.ConfigMapEnvSource{ 6666 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6667 }, 6668 }, { 6669 Prefix: "a.b", 6670 ConfigMapRef: &core.ConfigMapEnvSource{ 6671 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6672 }, 6673 }, { 6674 SecretRef: &core.SecretEnvSource{ 6675 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6676 }, 6677 }, { 6678 Prefix: "pre_", 6679 SecretRef: &core.SecretEnvSource{ 6680 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6681 }, 6682 }, { 6683 Prefix: "a.b", 6684 SecretRef: &core.SecretEnvSource{ 6685 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6686 }, 6687 }, 6688 } 6689 if errs := ValidateEnvFrom(successCase, nil, PodValidationOptions{}); len(errs) != 0 { 6690 t.Errorf("expected success: %v", errs) 6691 } 6692 6693 updateSuccessCase := []core.EnvFromSource{{ 6694 ConfigMapRef: &core.ConfigMapEnvSource{ 6695 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6696 }, 6697 }, { 6698 Prefix: "* +,-./0123456789", 6699 ConfigMapRef: &core.ConfigMapEnvSource{ 6700 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6701 }, 6702 }, { 6703 Prefix: ":;<>?@", 6704 ConfigMapRef: &core.ConfigMapEnvSource{ 6705 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6706 }, 6707 }, { 6708 SecretRef: &core.SecretEnvSource{ 6709 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6710 }, 6711 }, { 6712 Prefix: "abcdefghijklmn", 6713 SecretRef: &core.SecretEnvSource{ 6714 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6715 }, 6716 }, { 6717 Prefix: "[\\]^_`{}|~", 6718 SecretRef: &core.SecretEnvSource{ 6719 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6720 }, 6721 }} 6722 6723 if errs := ValidateEnvFrom(updateSuccessCase, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) != 0 { 6724 t.Errorf("expected success, got: %v", errs) 6725 } 6726 6727 updateErrorCase := []struct { 6728 name string 6729 envs []core.EnvFromSource 6730 expectedError string 6731 }{ 6732 { 6733 name: "invalid name a", 6734 envs: []core.EnvFromSource{ 6735 { 6736 Prefix: "!\"#$%&'()", 6737 SecretRef: &core.SecretEnvSource{ 6738 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6739 }, 6740 }, 6741 }, 6742 expectedError: `field[0].prefix: Invalid value: ` + "\"!\\\"#$%&'()\": " + envVarNameErrMsg, 6743 }, 6744 { 6745 name: "invalid name b", 6746 envs: []core.EnvFromSource{ 6747 { 6748 Prefix: "* +,-./0123456789", 6749 SecretRef: &core.SecretEnvSource{ 6750 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6751 }, 6752 }, 6753 }, 6754 expectedError: `field[0].prefix: Invalid value: ` + "\"* +,-./0123456789\": " + envVarNameErrMsg, 6755 }, 6756 { 6757 name: "invalid name c", 6758 envs: []core.EnvFromSource{ 6759 { 6760 Prefix: ":;<>?@", 6761 SecretRef: &core.SecretEnvSource{ 6762 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6763 }, 6764 }, 6765 }, 6766 expectedError: `field[0].prefix: Invalid value: ` + "\":;<>?@\": " + envVarNameErrMsg, 6767 }, 6768 { 6769 name: "invalid name d", 6770 envs: []core.EnvFromSource{ 6771 { 6772 Prefix: "[\\]^_{}|~", 6773 SecretRef: &core.SecretEnvSource{ 6774 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6775 }, 6776 }, 6777 }, 6778 expectedError: `field[0].prefix: Invalid value: ` + "\"[\\\\]^_{}|~\": " + envVarNameErrMsg, 6779 }, 6780 } 6781 6782 for _, tc := range updateErrorCase { 6783 if errs := ValidateEnvFrom(tc.envs, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { 6784 t.Errorf("expected failure for %s", tc.name) 6785 } else { 6786 for i := range errs { 6787 str := errs[i].Error() 6788 if str != "" && !strings.Contains(str, tc.expectedError) { 6789 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6790 } 6791 } 6792 } 6793 } 6794 6795 errorCases := []struct { 6796 name string 6797 envs []core.EnvFromSource 6798 expectedError string 6799 }{{ 6800 name: "zero-length name", 6801 envs: []core.EnvFromSource{{ 6802 ConfigMapRef: &core.ConfigMapEnvSource{ 6803 LocalObjectReference: core.LocalObjectReference{Name: ""}}, 6804 }}, 6805 expectedError: "field[0].configMapRef.name: Required value", 6806 }, { 6807 name: "invalid name", 6808 envs: []core.EnvFromSource{{ 6809 ConfigMapRef: &core.ConfigMapEnvSource{ 6810 LocalObjectReference: core.LocalObjectReference{Name: "$"}}, 6811 }}, 6812 expectedError: "field[0].configMapRef.name: Invalid value", 6813 }, { 6814 name: "zero-length name", 6815 envs: []core.EnvFromSource{{ 6816 SecretRef: &core.SecretEnvSource{ 6817 LocalObjectReference: core.LocalObjectReference{Name: ""}}, 6818 }}, 6819 expectedError: "field[0].secretRef.name: Required value", 6820 }, { 6821 name: "invalid name", 6822 envs: []core.EnvFromSource{{ 6823 SecretRef: &core.SecretEnvSource{ 6824 LocalObjectReference: core.LocalObjectReference{Name: "&"}}, 6825 }}, 6826 expectedError: "field[0].secretRef.name: Invalid value", 6827 }, { 6828 name: "no refs", 6829 envs: []core.EnvFromSource{ 6830 {}, 6831 }, 6832 expectedError: "field: Invalid value: \"\": must specify one of: `configMapRef` or `secretRef`", 6833 }, { 6834 name: "multiple refs", 6835 envs: []core.EnvFromSource{{ 6836 SecretRef: &core.SecretEnvSource{ 6837 LocalObjectReference: core.LocalObjectReference{Name: "abc"}}, 6838 ConfigMapRef: &core.ConfigMapEnvSource{ 6839 LocalObjectReference: core.LocalObjectReference{Name: "abc"}}, 6840 }}, 6841 expectedError: "field: Invalid value: \"\": may not have more than one field specified at a time", 6842 }, { 6843 name: "invalid secret ref name", 6844 envs: []core.EnvFromSource{{ 6845 SecretRef: &core.SecretEnvSource{ 6846 LocalObjectReference: core.LocalObjectReference{Name: "$%^&*#"}}, 6847 }}, 6848 expectedError: "field[0].secretRef.name: Invalid value: \"$%^&*#\": " + dnsSubdomainLabelErrMsg, 6849 }, { 6850 name: "invalid config ref name", 6851 envs: []core.EnvFromSource{{ 6852 ConfigMapRef: &core.ConfigMapEnvSource{ 6853 LocalObjectReference: core.LocalObjectReference{Name: "$%^&*#"}}, 6854 }}, 6855 expectedError: "field[0].configMapRef.name: Invalid value: \"$%^&*#\": " + dnsSubdomainLabelErrMsg, 6856 }, 6857 } 6858 for _, tc := range errorCases { 6859 if errs := ValidateEnvFrom(tc.envs, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { 6860 t.Errorf("expected failure for %s", tc.name) 6861 } else { 6862 for i := range errs { 6863 str := errs[i].Error() 6864 if str != "" && !strings.Contains(str, tc.expectedError) { 6865 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6866 } 6867 } 6868 } 6869 } 6870 } 6871 6872 func TestRelaxedValidateEnvFrom(t *testing.T) { 6873 successCase := []core.EnvFromSource{{ 6874 ConfigMapRef: &core.ConfigMapEnvSource{ 6875 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6876 }, 6877 }, { 6878 Prefix: "!\"#$%&'()", 6879 ConfigMapRef: &core.ConfigMapEnvSource{ 6880 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6881 }, 6882 }, { 6883 Prefix: "* +,-./0123456789", 6884 ConfigMapRef: &core.ConfigMapEnvSource{ 6885 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6886 }, 6887 }, { 6888 SecretRef: &core.SecretEnvSource{ 6889 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6890 }, 6891 }, { 6892 Prefix: ":;<>?@", 6893 SecretRef: &core.SecretEnvSource{ 6894 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6895 }, 6896 }, { 6897 Prefix: "[\\]^_`{}|~", 6898 SecretRef: &core.SecretEnvSource{ 6899 LocalObjectReference: core.LocalObjectReference{Name: "abc"}, 6900 }, 6901 }, 6902 } 6903 if errs := ValidateEnvFrom(successCase, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) != 0 { 6904 t.Errorf("expected success: %v", errs) 6905 } 6906 6907 errorCases := []struct { 6908 name string 6909 envs []core.EnvFromSource 6910 expectedError string 6911 }{ 6912 { 6913 name: "zero-length name", 6914 envs: []core.EnvFromSource{{ 6915 ConfigMapRef: &core.ConfigMapEnvSource{ 6916 LocalObjectReference: core.LocalObjectReference{Name: ""}}, 6917 }}, 6918 expectedError: "field[0].configMapRef.name: Required value", 6919 }, 6920 { 6921 name: "invalid prefix", 6922 envs: []core.EnvFromSource{{ 6923 Prefix: "=abc", 6924 ConfigMapRef: &core.ConfigMapEnvSource{ 6925 LocalObjectReference: core.LocalObjectReference{Name: "abc"}}, 6926 }}, 6927 expectedError: `field[0].prefix: Invalid value: "=abc": ` + relaxedEnvVarNameFmtErrMsg, 6928 }, 6929 { 6930 name: "zero-length name", 6931 envs: []core.EnvFromSource{{ 6932 SecretRef: &core.SecretEnvSource{ 6933 LocalObjectReference: core.LocalObjectReference{Name: ""}}, 6934 }}, 6935 expectedError: "field[0].secretRef.name: Required value", 6936 }, { 6937 name: "invalid name", 6938 envs: []core.EnvFromSource{{ 6939 SecretRef: &core.SecretEnvSource{ 6940 LocalObjectReference: core.LocalObjectReference{Name: "&"}}, 6941 }}, 6942 expectedError: "field[0].secretRef.name: Invalid value", 6943 }, { 6944 name: "no refs", 6945 envs: []core.EnvFromSource{ 6946 {}, 6947 }, 6948 expectedError: "field: Invalid value: \"\": must specify one of: `configMapRef` or `secretRef`", 6949 }, { 6950 name: "multiple refs", 6951 envs: []core.EnvFromSource{{ 6952 SecretRef: &core.SecretEnvSource{ 6953 LocalObjectReference: core.LocalObjectReference{Name: "abc"}}, 6954 ConfigMapRef: &core.ConfigMapEnvSource{ 6955 LocalObjectReference: core.LocalObjectReference{Name: "abc"}}, 6956 }}, 6957 expectedError: "field: Invalid value: \"\": may not have more than one field specified at a time", 6958 }, { 6959 name: "invalid secret ref name", 6960 envs: []core.EnvFromSource{{ 6961 SecretRef: &core.SecretEnvSource{ 6962 LocalObjectReference: core.LocalObjectReference{Name: "$%^&*#"}}, 6963 }}, 6964 expectedError: "field[0].secretRef.name: Invalid value: \"$%^&*#\": " + dnsSubdomainLabelErrMsg, 6965 }, { 6966 name: "invalid config ref name", 6967 envs: []core.EnvFromSource{{ 6968 ConfigMapRef: &core.ConfigMapEnvSource{ 6969 LocalObjectReference: core.LocalObjectReference{Name: "$%^&*#"}}, 6970 }}, 6971 expectedError: "field[0].configMapRef.name: Invalid value: \"$%^&*#\": " + dnsSubdomainLabelErrMsg, 6972 }, 6973 } 6974 for _, tc := range errorCases { 6975 if errs := ValidateEnvFrom(tc.envs, field.NewPath("field"), PodValidationOptions{AllowRelaxedEnvironmentVariableValidation: true}); len(errs) == 0 { 6976 t.Errorf("expected failure for %s", tc.name) 6977 } else { 6978 for i := range errs { 6979 str := errs[i].Error() 6980 if str != "" && !strings.Contains(str, tc.expectedError) { 6981 t.Errorf("%s: expected error detail either empty or %q, got %q", tc.name, tc.expectedError, str) 6982 } 6983 } 6984 } 6985 } 6986 } 6987 6988 func TestValidateVolumeMounts(t *testing.T) { 6989 volumes := []core.Volume{ 6990 {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, 6991 {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, 6992 {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, 6993 {Name: "ephemeral", VolumeSource: core.VolumeSource{Ephemeral: &core.EphemeralVolumeSource{VolumeClaimTemplate: &core.PersistentVolumeClaimTemplate{ 6994 Spec: core.PersistentVolumeClaimSpec{ 6995 AccessModes: []core.PersistentVolumeAccessMode{ 6996 core.ReadWriteOnce, 6997 }, 6998 Resources: core.VolumeResourceRequirements{ 6999 Requests: core.ResourceList{ 7000 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 7001 }, 7002 }, 7003 }, 7004 }}}}, 7005 } 7006 vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) 7007 if len(v1err) > 0 { 7008 t.Errorf("Invalid test volume - expected success %v", v1err) 7009 return 7010 } 7011 container := core.Container{ 7012 SecurityContext: nil, 7013 } 7014 propagation := core.MountPropagationBidirectional 7015 7016 successCase := []core.VolumeMount{ 7017 {Name: "abc", MountPath: "/foo"}, 7018 {Name: "123", MountPath: "/bar"}, 7019 {Name: "abc-123", MountPath: "/baz"}, 7020 {Name: "abc-123", MountPath: "/baa", SubPath: ""}, 7021 {Name: "abc-123", MountPath: "/bab", SubPath: "baz"}, 7022 {Name: "abc-123", MountPath: "d:", SubPath: ""}, 7023 {Name: "abc-123", MountPath: "F:", SubPath: ""}, 7024 {Name: "abc-123", MountPath: "G:\\mount", SubPath: ""}, 7025 {Name: "abc-123", MountPath: "/bac", SubPath: ".baz"}, 7026 {Name: "abc-123", MountPath: "/bad", SubPath: "..baz"}, 7027 {Name: "ephemeral", MountPath: "/foobar"}, 7028 {Name: "123", MountPath: "/rro-nil", ReadOnly: true, RecursiveReadOnly: nil}, 7029 {Name: "123", MountPath: "/rro-disabled", ReadOnly: true, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyDisabled)}, 7030 {Name: "123", MountPath: "/rro-disabled-2", ReadOnly: false, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyDisabled)}, 7031 {Name: "123", MountPath: "/rro-ifpossible", ReadOnly: true, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyIfPossible)}, 7032 {Name: "123", MountPath: "/rro-enabled", ReadOnly: true, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyEnabled)}, 7033 {Name: "123", MountPath: "/rro-enabled-2", ReadOnly: true, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyEnabled), MountPropagation: ptr.To(core.MountPropagationNone)}, 7034 } 7035 goodVolumeDevices := []core.VolumeDevice{ 7036 {Name: "xyz", DevicePath: "/foofoo"}, 7037 {Name: "uvw", DevicePath: "/foofoo/share/test"}, 7038 } 7039 if errs := ValidateVolumeMounts(successCase, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field")); len(errs) != 0 { 7040 t.Errorf("expected success: %v", errs) 7041 } 7042 7043 errorCases := map[string][]core.VolumeMount{ 7044 "empty name": {{Name: "", MountPath: "/foo"}}, 7045 "name not found": {{Name: "", MountPath: "/foo"}}, 7046 "empty mountpath": {{Name: "abc", MountPath: ""}}, 7047 "mountpath collision": {{Name: "foo", MountPath: "/path/a"}, {Name: "bar", MountPath: "/path/a"}}, 7048 "absolute subpath": {{Name: "abc", MountPath: "/bar", SubPath: "/baz"}}, 7049 "subpath in ..": {{Name: "abc", MountPath: "/bar", SubPath: "../baz"}}, 7050 "subpath contains ..": {{Name: "abc", MountPath: "/bar", SubPath: "baz/../bat"}}, 7051 "subpath ends in ..": {{Name: "abc", MountPath: "/bar", SubPath: "./.."}}, 7052 "disabled MountPropagation feature gate": {{Name: "abc", MountPath: "/bar", MountPropagation: &propagation}}, 7053 "name exists in volumeDevice": {{Name: "xyz", MountPath: "/bar"}}, 7054 "mountpath exists in volumeDevice": {{Name: "uvw", MountPath: "/mnt/exists"}}, 7055 "both exist in volumeDevice": {{Name: "xyz", MountPath: "/mnt/exists"}}, 7056 "rro but not ro": {{Name: "123", MountPath: "/rro-bad1", ReadOnly: false, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyEnabled)}}, 7057 "rro with incompatible propagation": {{Name: "123", MountPath: "/rro-bad2", ReadOnly: true, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyEnabled), MountPropagation: ptr.To(core.MountPropagationHostToContainer)}}, 7058 "rro-if-possible but not ro": {{Name: "123", MountPath: "/rro-bad1", ReadOnly: false, RecursiveReadOnly: ptr.To(core.RecursiveReadOnlyIfPossible)}}, 7059 } 7060 badVolumeDevice := []core.VolumeDevice{ 7061 {Name: "xyz", DevicePath: "/mnt/exists"}, 7062 } 7063 7064 for k, v := range errorCases { 7065 if errs := ValidateVolumeMounts(v, GetVolumeDeviceMap(badVolumeDevice), vols, &container, field.NewPath("field")); len(errs) == 0 { 7066 t.Errorf("expected failure for %s", k) 7067 } 7068 } 7069 } 7070 7071 func TestValidateSubpathMutuallyExclusive(t *testing.T) { 7072 volumes := []core.Volume{ 7073 {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, 7074 {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, 7075 {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, 7076 } 7077 vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) 7078 if len(v1err) > 0 { 7079 t.Errorf("Invalid test volume - expected success %v", v1err) 7080 return 7081 } 7082 7083 container := core.Container{ 7084 SecurityContext: nil, 7085 } 7086 7087 goodVolumeDevices := []core.VolumeDevice{ 7088 {Name: "xyz", DevicePath: "/foofoo"}, 7089 {Name: "uvw", DevicePath: "/foofoo/share/test"}, 7090 } 7091 7092 cases := map[string]struct { 7093 mounts []core.VolumeMount 7094 expectError bool 7095 }{ 7096 "subpath and subpathexpr not specified": { 7097 []core.VolumeMount{{ 7098 Name: "abc-123", 7099 MountPath: "/bab", 7100 }}, 7101 false, 7102 }, 7103 "subpath expr specified": { 7104 []core.VolumeMount{{ 7105 Name: "abc-123", 7106 MountPath: "/bab", 7107 SubPathExpr: "$(POD_NAME)", 7108 }}, 7109 false, 7110 }, 7111 "subpath specified": { 7112 []core.VolumeMount{{ 7113 Name: "abc-123", 7114 MountPath: "/bab", 7115 SubPath: "baz", 7116 }}, 7117 false, 7118 }, 7119 "subpath and subpathexpr specified": { 7120 []core.VolumeMount{{ 7121 Name: "abc-123", 7122 MountPath: "/bab", 7123 SubPath: "baz", 7124 SubPathExpr: "$(POD_NAME)", 7125 }}, 7126 true, 7127 }, 7128 } 7129 7130 for name, test := range cases { 7131 errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field")) 7132 7133 if len(errs) != 0 && !test.expectError { 7134 t.Errorf("test %v failed: %+v", name, errs) 7135 } 7136 7137 if len(errs) == 0 && test.expectError { 7138 t.Errorf("test %v failed, expected error", name) 7139 } 7140 } 7141 } 7142 7143 func TestValidateDisabledSubpathExpr(t *testing.T) { 7144 7145 volumes := []core.Volume{ 7146 {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, 7147 {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, 7148 {Name: "123", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, 7149 } 7150 vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) 7151 if len(v1err) > 0 { 7152 t.Errorf("Invalid test volume - expected success %v", v1err) 7153 return 7154 } 7155 7156 container := core.Container{ 7157 SecurityContext: nil, 7158 } 7159 7160 goodVolumeDevices := []core.VolumeDevice{ 7161 {Name: "xyz", DevicePath: "/foofoo"}, 7162 {Name: "uvw", DevicePath: "/foofoo/share/test"}, 7163 } 7164 7165 cases := map[string]struct { 7166 mounts []core.VolumeMount 7167 expectError bool 7168 }{ 7169 "subpath expr not specified": { 7170 []core.VolumeMount{{ 7171 Name: "abc-123", 7172 MountPath: "/bab", 7173 }}, 7174 false, 7175 }, 7176 "subpath expr specified": { 7177 []core.VolumeMount{{ 7178 Name: "abc-123", 7179 MountPath: "/bab", 7180 SubPathExpr: "$(POD_NAME)", 7181 }}, 7182 false, 7183 }, 7184 } 7185 7186 for name, test := range cases { 7187 errs := ValidateVolumeMounts(test.mounts, GetVolumeDeviceMap(goodVolumeDevices), vols, &container, field.NewPath("field")) 7188 7189 if len(errs) != 0 && !test.expectError { 7190 t.Errorf("test %v failed: %+v", name, errs) 7191 } 7192 7193 if len(errs) == 0 && test.expectError { 7194 t.Errorf("test %v failed, expected error", name) 7195 } 7196 } 7197 } 7198 7199 func TestValidateMountPropagation(t *testing.T) { 7200 bTrue := true 7201 bFalse := false 7202 privilegedContainer := &core.Container{ 7203 SecurityContext: &core.SecurityContext{ 7204 Privileged: &bTrue, 7205 }, 7206 } 7207 nonPrivilegedContainer := &core.Container{ 7208 SecurityContext: &core.SecurityContext{ 7209 Privileged: &bFalse, 7210 }, 7211 } 7212 defaultContainer := &core.Container{} 7213 7214 propagationBidirectional := core.MountPropagationBidirectional 7215 propagationHostToContainer := core.MountPropagationHostToContainer 7216 propagationNone := core.MountPropagationNone 7217 propagationInvalid := core.MountPropagationMode("invalid") 7218 7219 tests := []struct { 7220 mount core.VolumeMount 7221 container *core.Container 7222 expectError bool 7223 }{{ 7224 // implicitly non-privileged container + no propagation 7225 core.VolumeMount{Name: "foo", MountPath: "/foo"}, 7226 defaultContainer, 7227 false, 7228 }, { 7229 // implicitly non-privileged container + HostToContainer 7230 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, 7231 defaultContainer, 7232 false, 7233 }, { 7234 // non-privileged container + None 7235 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationNone}, 7236 defaultContainer, 7237 false, 7238 }, { 7239 // error: implicitly non-privileged container + Bidirectional 7240 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, 7241 defaultContainer, 7242 true, 7243 }, { 7244 // explicitly non-privileged container + no propagation 7245 core.VolumeMount{Name: "foo", MountPath: "/foo"}, 7246 nonPrivilegedContainer, 7247 false, 7248 }, { 7249 // explicitly non-privileged container + HostToContainer 7250 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, 7251 nonPrivilegedContainer, 7252 false, 7253 }, { 7254 // explicitly non-privileged container + HostToContainer 7255 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, 7256 nonPrivilegedContainer, 7257 true, 7258 }, { 7259 // privileged container + no propagation 7260 core.VolumeMount{Name: "foo", MountPath: "/foo"}, 7261 privilegedContainer, 7262 false, 7263 }, { 7264 // privileged container + HostToContainer 7265 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationHostToContainer}, 7266 privilegedContainer, 7267 false, 7268 }, { 7269 // privileged container + Bidirectional 7270 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, 7271 privilegedContainer, 7272 false, 7273 }, { 7274 // error: privileged container + invalid mount propagation 7275 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationInvalid}, 7276 privilegedContainer, 7277 true, 7278 }, { 7279 // no container + Bidirectional 7280 core.VolumeMount{Name: "foo", MountPath: "/foo", MountPropagation: &propagationBidirectional}, 7281 nil, 7282 false, 7283 }, 7284 } 7285 7286 volumes := []core.Volume{ 7287 {Name: "foo", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, 7288 } 7289 vols2, v2err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) 7290 if len(v2err) > 0 { 7291 t.Errorf("Invalid test volume - expected success %v", v2err) 7292 return 7293 } 7294 for i, test := range tests { 7295 errs := ValidateVolumeMounts([]core.VolumeMount{test.mount}, nil, vols2, test.container, field.NewPath("field")) 7296 if test.expectError && len(errs) == 0 { 7297 t.Errorf("test %d expected error, got none", i) 7298 } 7299 if !test.expectError && len(errs) != 0 { 7300 t.Errorf("test %d expected success, got error: %v", i, errs) 7301 } 7302 } 7303 } 7304 7305 func TestAlphaValidateVolumeDevices(t *testing.T) { 7306 volumes := []core.Volume{ 7307 {Name: "abc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim1"}}}, 7308 {Name: "abc-123", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "testclaim2"}}}, 7309 {Name: "def", VolumeSource: core.VolumeSource{HostPath: &core.HostPathVolumeSource{Path: "/foo/baz", Type: newHostPathType(string(core.HostPathUnset))}}}, 7310 {Name: "ephemeral", VolumeSource: core.VolumeSource{Ephemeral: &core.EphemeralVolumeSource{VolumeClaimTemplate: &core.PersistentVolumeClaimTemplate{ 7311 Spec: core.PersistentVolumeClaimSpec{ 7312 AccessModes: []core.PersistentVolumeAccessMode{ 7313 core.ReadWriteOnce, 7314 }, 7315 Resources: core.VolumeResourceRequirements{ 7316 Requests: core.ResourceList{ 7317 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 7318 }, 7319 }, 7320 }, 7321 }}}}, 7322 } 7323 7324 vols, v1err := ValidateVolumes(volumes, nil, field.NewPath("field"), PodValidationOptions{}) 7325 if len(v1err) > 0 { 7326 t.Errorf("Invalid test volumes - expected success %v", v1err) 7327 return 7328 } 7329 7330 successCase := []core.VolumeDevice{ 7331 {Name: "abc", DevicePath: "/foo"}, 7332 {Name: "abc-123", DevicePath: "/usr/share/test"}, 7333 {Name: "ephemeral", DevicePath: "/disk"}, 7334 } 7335 goodVolumeMounts := []core.VolumeMount{ 7336 {Name: "xyz", MountPath: "/foofoo"}, 7337 {Name: "ghi", MountPath: "/foo/usr/share/test"}, 7338 } 7339 7340 errorCases := map[string][]core.VolumeDevice{ 7341 "empty name": {{Name: "", DevicePath: "/foo"}}, 7342 "duplicate name": {{Name: "abc", DevicePath: "/foo"}, {Name: "abc", DevicePath: "/foo/bar"}}, 7343 "name not found": {{Name: "not-found", DevicePath: "/usr/share/test"}}, 7344 "name found but invalid source": {{Name: "def", DevicePath: "/usr/share/test"}}, 7345 "empty devicepath": {{Name: "abc", DevicePath: ""}}, 7346 "relative devicepath": {{Name: "abc-123", DevicePath: "baz"}}, 7347 "duplicate devicepath": {{Name: "abc", DevicePath: "/foo"}, {Name: "abc-123", DevicePath: "/foo"}}, 7348 "no backsteps": {{Name: "def", DevicePath: "/baz/../"}}, 7349 "name exists in volumemounts": {{Name: "abc", DevicePath: "/baz/../"}}, 7350 "path exists in volumemounts": {{Name: "xyz", DevicePath: "/this/path/exists"}}, 7351 "both exist in volumemounts": {{Name: "abc", DevicePath: "/this/path/exists"}}, 7352 } 7353 badVolumeMounts := []core.VolumeMount{ 7354 {Name: "abc", MountPath: "/foo"}, 7355 {Name: "abc-123", MountPath: "/this/path/exists"}, 7356 } 7357 7358 // Success Cases: 7359 // Validate normal success cases - only PVC volumeSource or generic ephemeral volume 7360 if errs := ValidateVolumeDevices(successCase, GetVolumeMountMap(goodVolumeMounts), vols, field.NewPath("field")); len(errs) != 0 { 7361 t.Errorf("expected success: %v", errs) 7362 } 7363 7364 // Error Cases: 7365 // Validate normal error cases - only PVC volumeSource 7366 for k, v := range errorCases { 7367 if errs := ValidateVolumeDevices(v, GetVolumeMountMap(badVolumeMounts), vols, field.NewPath("field")); len(errs) == 0 { 7368 t.Errorf("expected failure for %s", k) 7369 } 7370 } 7371 } 7372 7373 func TestValidateProbe(t *testing.T) { 7374 handler := core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}} 7375 // These fields must be positive. 7376 positiveFields := [...]string{"InitialDelaySeconds", "TimeoutSeconds", "PeriodSeconds", "SuccessThreshold", "FailureThreshold"} 7377 successCases := []*core.Probe{nil} 7378 for _, field := range positiveFields { 7379 probe := &core.Probe{ProbeHandler: handler} 7380 reflect.ValueOf(probe).Elem().FieldByName(field).SetInt(10) 7381 successCases = append(successCases, probe) 7382 } 7383 7384 for _, p := range successCases { 7385 if errs := validateProbe(p, defaultGracePeriod, field.NewPath("field")); len(errs) != 0 { 7386 t.Errorf("expected success: %v", errs) 7387 } 7388 } 7389 7390 errorCases := []*core.Probe{{TimeoutSeconds: 10, InitialDelaySeconds: 10}} 7391 for _, field := range positiveFields { 7392 probe := &core.Probe{ProbeHandler: handler} 7393 reflect.ValueOf(probe).Elem().FieldByName(field).SetInt(-10) 7394 errorCases = append(errorCases, probe) 7395 } 7396 for _, p := range errorCases { 7397 if errs := validateProbe(p, defaultGracePeriod, field.NewPath("field")); len(errs) == 0 { 7398 t.Errorf("expected failure for %v", p) 7399 } 7400 } 7401 } 7402 7403 func Test_validateProbe(t *testing.T) { 7404 fldPath := field.NewPath("test") 7405 type args struct { 7406 probe *core.Probe 7407 fldPath *field.Path 7408 } 7409 tests := []struct { 7410 name string 7411 args args 7412 want field.ErrorList 7413 }{{ 7414 args: args{ 7415 probe: &core.Probe{}, 7416 fldPath: fldPath, 7417 }, 7418 want: field.ErrorList{field.Required(fldPath, "must specify a handler type")}, 7419 }, { 7420 args: args{ 7421 probe: &core.Probe{ 7422 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7423 }, 7424 fldPath: fldPath, 7425 }, 7426 want: field.ErrorList{}, 7427 }, { 7428 args: args{ 7429 probe: &core.Probe{ 7430 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7431 InitialDelaySeconds: -1, 7432 }, 7433 fldPath: fldPath, 7434 }, 7435 want: field.ErrorList{field.Invalid(fldPath.Child("initialDelaySeconds"), -1, "must be greater than or equal to 0")}, 7436 }, { 7437 args: args{ 7438 probe: &core.Probe{ 7439 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7440 TimeoutSeconds: -1, 7441 }, 7442 fldPath: fldPath, 7443 }, 7444 want: field.ErrorList{field.Invalid(fldPath.Child("timeoutSeconds"), -1, "must be greater than or equal to 0")}, 7445 }, { 7446 args: args{ 7447 probe: &core.Probe{ 7448 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7449 PeriodSeconds: -1, 7450 }, 7451 fldPath: fldPath, 7452 }, 7453 want: field.ErrorList{field.Invalid(fldPath.Child("periodSeconds"), -1, "must be greater than or equal to 0")}, 7454 }, { 7455 args: args{ 7456 probe: &core.Probe{ 7457 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7458 SuccessThreshold: -1, 7459 }, 7460 fldPath: fldPath, 7461 }, 7462 want: field.ErrorList{field.Invalid(fldPath.Child("successThreshold"), -1, "must be greater than or equal to 0")}, 7463 }, { 7464 args: args{ 7465 probe: &core.Probe{ 7466 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7467 FailureThreshold: -1, 7468 }, 7469 fldPath: fldPath, 7470 }, 7471 want: field.ErrorList{field.Invalid(fldPath.Child("failureThreshold"), -1, "must be greater than or equal to 0")}, 7472 }, { 7473 args: args{ 7474 probe: &core.Probe{ 7475 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7476 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 7477 }, 7478 fldPath: fldPath, 7479 }, 7480 want: field.ErrorList{field.Invalid(fldPath.Child("terminationGracePeriodSeconds"), -1, "must be greater than 0")}, 7481 }, { 7482 args: args{ 7483 probe: &core.Probe{ 7484 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7485 TerminationGracePeriodSeconds: utilpointer.Int64(0), 7486 }, 7487 fldPath: fldPath, 7488 }, 7489 want: field.ErrorList{field.Invalid(fldPath.Child("terminationGracePeriodSeconds"), 0, "must be greater than 0")}, 7490 }, { 7491 args: args{ 7492 probe: &core.Probe{ 7493 ProbeHandler: core.ProbeHandler{Exec: &core.ExecAction{Command: []string{"echo"}}}, 7494 TerminationGracePeriodSeconds: utilpointer.Int64(1), 7495 }, 7496 fldPath: fldPath, 7497 }, 7498 want: field.ErrorList{}, 7499 }, 7500 } 7501 for _, tt := range tests { 7502 t.Run(tt.name, func(t *testing.T) { 7503 got := validateProbe(tt.args.probe, defaultGracePeriod, tt.args.fldPath) 7504 if len(got) != len(tt.want) { 7505 t.Errorf("validateProbe() = %v, want %v", got, tt.want) 7506 return 7507 } 7508 for i := range got { 7509 if got[i].Type != tt.want[i].Type || 7510 got[i].Field != tt.want[i].Field { 7511 t.Errorf("validateProbe()[%d] = %v, want %v", i, got[i], tt.want[i]) 7512 } 7513 } 7514 }) 7515 } 7516 } 7517 7518 func TestValidateHandler(t *testing.T) { 7519 successCases := []core.ProbeHandler{ 7520 {Exec: &core.ExecAction{Command: []string{"echo"}}}, 7521 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromInt32(1), Host: "", Scheme: "HTTP"}}, 7522 {HTTPGet: &core.HTTPGetAction{Path: "/foo", Port: intstr.FromInt32(65535), Host: "host", Scheme: "HTTP"}}, 7523 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP"}}, 7524 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []core.HTTPHeader{{Name: "Host", Value: "foo.example.com"}}}}, 7525 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []core.HTTPHeader{{Name: "X-Forwarded-For", Value: "1.2.3.4"}, {Name: "X-Forwarded-For", Value: "5.6.7.8"}}}}, 7526 } 7527 for _, h := range successCases { 7528 if errs := validateHandler(handlerFromProbe(&h), defaultGracePeriod, field.NewPath("field")); len(errs) != 0 { 7529 t.Errorf("expected success: %v", errs) 7530 } 7531 } 7532 7533 errorCases := []core.ProbeHandler{ 7534 {}, 7535 {Exec: &core.ExecAction{Command: []string{}}}, 7536 {HTTPGet: &core.HTTPGetAction{Path: "", Port: intstr.FromInt32(0), Host: ""}}, 7537 {HTTPGet: &core.HTTPGetAction{Path: "/foo", Port: intstr.FromInt32(65536), Host: "host"}}, 7538 {HTTPGet: &core.HTTPGetAction{Path: "", Port: intstr.FromString(""), Host: ""}}, 7539 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []core.HTTPHeader{{Name: "Host:", Value: "foo.example.com"}}}}, 7540 {HTTPGet: &core.HTTPGetAction{Path: "/", Port: intstr.FromString("port"), Host: "", Scheme: "HTTP", HTTPHeaders: []core.HTTPHeader{{Name: "X_Forwarded_For", Value: "foo.example.com"}}}}, 7541 } 7542 for _, h := range errorCases { 7543 if errs := validateHandler(handlerFromProbe(&h), defaultGracePeriod, field.NewPath("field")); len(errs) == 0 { 7544 t.Errorf("expected failure for %#v", h) 7545 } 7546 } 7547 } 7548 7549 func TestValidatePullPolicy(t *testing.T) { 7550 type T struct { 7551 Container core.Container 7552 ExpectedPolicy core.PullPolicy 7553 } 7554 testCases := map[string]T{ 7555 "NotPresent1": { 7556 core.Container{Name: "abc", Image: "image:latest", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7557 core.PullIfNotPresent, 7558 }, 7559 "NotPresent2": { 7560 core.Container{Name: "abc1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7561 core.PullIfNotPresent, 7562 }, 7563 "Always1": { 7564 core.Container{Name: "123", Image: "image:latest", ImagePullPolicy: "Always"}, 7565 core.PullAlways, 7566 }, 7567 "Always2": { 7568 core.Container{Name: "1234", Image: "image", ImagePullPolicy: "Always"}, 7569 core.PullAlways, 7570 }, 7571 "Never1": { 7572 core.Container{Name: "abc-123", Image: "image:latest", ImagePullPolicy: "Never"}, 7573 core.PullNever, 7574 }, 7575 "Never2": { 7576 core.Container{Name: "abc-1234", Image: "image", ImagePullPolicy: "Never"}, 7577 core.PullNever, 7578 }, 7579 } 7580 for k, v := range testCases { 7581 ctr := &v.Container 7582 errs := validatePullPolicy(ctr.ImagePullPolicy, field.NewPath("field")) 7583 if len(errs) != 0 { 7584 t.Errorf("case[%s] expected success, got %#v", k, errs) 7585 } 7586 if ctr.ImagePullPolicy != v.ExpectedPolicy { 7587 t.Errorf("case[%s] expected policy %v, got %v", k, v.ExpectedPolicy, ctr.ImagePullPolicy) 7588 } 7589 } 7590 } 7591 7592 func TestValidateResizePolicy(t *testing.T) { 7593 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true) 7594 tSupportedResizeResources := sets.NewString(string(core.ResourceCPU), string(core.ResourceMemory)) 7595 tSupportedResizePolicies := sets.NewString(string(core.NotRequired), string(core.RestartContainer)) 7596 type T struct { 7597 PolicyList []core.ContainerResizePolicy 7598 ExpectError bool 7599 Errors field.ErrorList 7600 PodRestartPolicy core.RestartPolicy 7601 } 7602 7603 testCases := map[string]T{ 7604 "ValidCPUandMemoryPolicies": { 7605 PolicyList: []core.ContainerResizePolicy{ 7606 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 7607 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 7608 }, 7609 ExpectError: false, 7610 Errors: nil, 7611 PodRestartPolicy: "Always", 7612 }, 7613 "ValidCPUPolicy": { 7614 PolicyList: []core.ContainerResizePolicy{ 7615 {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, 7616 }, 7617 ExpectError: false, 7618 Errors: nil, 7619 PodRestartPolicy: "Always", 7620 }, 7621 "ValidMemoryPolicy": { 7622 PolicyList: []core.ContainerResizePolicy{ 7623 {ResourceName: "memory", RestartPolicy: "NotRequired"}, 7624 }, 7625 ExpectError: false, 7626 Errors: nil, 7627 PodRestartPolicy: "Always", 7628 }, 7629 "NoPolicy": { 7630 PolicyList: []core.ContainerResizePolicy{}, 7631 ExpectError: false, 7632 Errors: nil, 7633 PodRestartPolicy: "Always", 7634 }, 7635 "ValidCPUandInvalidMemoryPolicy": { 7636 PolicyList: []core.ContainerResizePolicy{ 7637 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 7638 {ResourceName: "memory", RestartPolicy: "Restarrrt"}, 7639 }, 7640 ExpectError: true, 7641 Errors: field.ErrorList{field.NotSupported(field.NewPath("field"), core.ResourceResizeRestartPolicy("Restarrrt"), tSupportedResizePolicies.List())}, 7642 PodRestartPolicy: "Always", 7643 }, 7644 "ValidMemoryandInvalidCPUPolicy": { 7645 PolicyList: []core.ContainerResizePolicy{ 7646 {ResourceName: "cpu", RestartPolicy: "RestartNotRequirrred"}, 7647 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 7648 }, 7649 ExpectError: true, 7650 Errors: field.ErrorList{field.NotSupported(field.NewPath("field"), core.ResourceResizeRestartPolicy("RestartNotRequirrred"), tSupportedResizePolicies.List())}, 7651 PodRestartPolicy: "Always", 7652 }, 7653 "InvalidResourceNameValidPolicy": { 7654 PolicyList: []core.ContainerResizePolicy{ 7655 {ResourceName: "cpuuu", RestartPolicy: "NotRequired"}, 7656 }, 7657 ExpectError: true, 7658 Errors: field.ErrorList{field.NotSupported(field.NewPath("field"), core.ResourceName("cpuuu"), tSupportedResizeResources.List())}, 7659 PodRestartPolicy: "Always", 7660 }, 7661 "ValidResourceNameMissingPolicy": { 7662 PolicyList: []core.ContainerResizePolicy{ 7663 {ResourceName: "memory", RestartPolicy: ""}, 7664 }, 7665 ExpectError: true, 7666 Errors: field.ErrorList{field.Required(field.NewPath("field"), "")}, 7667 PodRestartPolicy: "Always", 7668 }, 7669 "RepeatedPolicies": { 7670 PolicyList: []core.ContainerResizePolicy{ 7671 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 7672 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 7673 {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, 7674 }, 7675 ExpectError: true, 7676 Errors: field.ErrorList{field.Duplicate(field.NewPath("field").Index(2), core.ResourceCPU)}, 7677 PodRestartPolicy: "Always", 7678 }, 7679 "InvalidCPUPolicyWithPodRestartPolicy": { 7680 PolicyList: []core.ContainerResizePolicy{ 7681 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 7682 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 7683 }, 7684 ExpectError: true, 7685 Errors: field.ErrorList{field.Invalid(field.NewPath("field"), core.ResourceResizeRestartPolicy("RestartContainer"), "must be 'NotRequired' when `restartPolicy` is 'Never'")}, 7686 PodRestartPolicy: "Never", 7687 }, 7688 "InvalidMemoryPolicyWithPodRestartPolicy": { 7689 PolicyList: []core.ContainerResizePolicy{ 7690 {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, 7691 {ResourceName: "memory", RestartPolicy: "NotRequired"}, 7692 }, 7693 ExpectError: true, 7694 Errors: field.ErrorList{field.Invalid(field.NewPath("field"), core.ResourceResizeRestartPolicy("RestartContainer"), "must be 'NotRequired' when `restartPolicy` is 'Never'")}, 7695 PodRestartPolicy: "Never", 7696 }, 7697 "InvalidMemoryCPUPolicyWithPodRestartPolicy": { 7698 PolicyList: []core.ContainerResizePolicy{ 7699 {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, 7700 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 7701 }, 7702 ExpectError: true, 7703 Errors: field.ErrorList{field.Invalid(field.NewPath("field"), core.ResourceResizeRestartPolicy("RestartContainer"), "must be 'NotRequired' when `restartPolicy` is 'Never'"), field.Invalid(field.NewPath("field"), core.ResourceResizeRestartPolicy("RestartContainer"), "must be 'NotRequired' when `restartPolicy` is 'Never'")}, 7704 PodRestartPolicy: "Never", 7705 }, 7706 "ValidMemoryCPUPolicyWithPodRestartPolicy": { 7707 PolicyList: []core.ContainerResizePolicy{ 7708 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 7709 {ResourceName: "memory", RestartPolicy: "NotRequired"}, 7710 }, 7711 ExpectError: false, 7712 Errors: nil, 7713 PodRestartPolicy: "Never", 7714 }, 7715 } 7716 for k, v := range testCases { 7717 errs := validateResizePolicy(v.PolicyList, field.NewPath("field"), &v.PodRestartPolicy) 7718 if !v.ExpectError && len(errs) > 0 { 7719 t.Errorf("Testcase %s - expected success, got error: %+v", k, errs) 7720 } 7721 if v.ExpectError { 7722 if len(errs) == 0 { 7723 t.Errorf("Testcase %s - expected error, got success", k) 7724 } 7725 delta := cmp.Diff(errs, v.Errors) 7726 if delta != "" { 7727 t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", k, v.Errors, errs, delta) 7728 } 7729 } 7730 } 7731 } 7732 7733 func getResourceLimits(cpu, memory string) core.ResourceList { 7734 res := core.ResourceList{} 7735 res[core.ResourceCPU] = resource.MustParse(cpu) 7736 res[core.ResourceMemory] = resource.MustParse(memory) 7737 return res 7738 } 7739 7740 func getResources(cpu, memory, storage string) core.ResourceList { 7741 res := core.ResourceList{} 7742 if cpu != "" { 7743 res[core.ResourceCPU] = resource.MustParse(cpu) 7744 } 7745 if memory != "" { 7746 res[core.ResourceMemory] = resource.MustParse(memory) 7747 } 7748 if storage != "" { 7749 res[core.ResourceEphemeralStorage] = resource.MustParse(storage) 7750 } 7751 return res 7752 } 7753 7754 func TestValidateEphemeralContainers(t *testing.T) { 7755 containers := []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}} 7756 initContainers := []core.Container{{Name: "ictr", Image: "iimage", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}} 7757 vols := map[string]core.VolumeSource{ 7758 "blk": {PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "pvc"}}, 7759 "vol": {EmptyDir: &core.EmptyDirVolumeSource{}}, 7760 } 7761 7762 // Success Cases 7763 for title, ephemeralContainers := range map[string][]core.EphemeralContainer{ 7764 "Empty Ephemeral Containers": {}, 7765 "Single Container": { 7766 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7767 }, 7768 "Multiple Containers": { 7769 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7770 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug2", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7771 }, 7772 "Single Container with Target": {{ 7773 EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7774 TargetContainerName: "ctr", 7775 }}, 7776 "All allowed fields": {{ 7777 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7778 7779 Name: "debug", 7780 Image: "image", 7781 Command: []string{"bash"}, 7782 Args: []string{"bash"}, 7783 WorkingDir: "/", 7784 EnvFrom: []core.EnvFromSource{{ 7785 ConfigMapRef: &core.ConfigMapEnvSource{ 7786 LocalObjectReference: core.LocalObjectReference{Name: "dummy"}, 7787 Optional: &[]bool{true}[0], 7788 }, 7789 }}, 7790 Env: []core.EnvVar{ 7791 {Name: "TEST", Value: "TRUE"}, 7792 }, 7793 VolumeMounts: []core.VolumeMount{ 7794 {Name: "vol", MountPath: "/vol"}, 7795 }, 7796 VolumeDevices: []core.VolumeDevice{ 7797 {Name: "blk", DevicePath: "/dev/block"}, 7798 }, 7799 TerminationMessagePath: "/dev/termination-log", 7800 TerminationMessagePolicy: "File", 7801 ImagePullPolicy: "IfNotPresent", 7802 SecurityContext: &core.SecurityContext{ 7803 Capabilities: &core.Capabilities{ 7804 Add: []core.Capability{"SYS_ADMIN"}, 7805 }, 7806 }, 7807 Stdin: true, 7808 StdinOnce: true, 7809 TTY: true, 7810 }, 7811 }}, 7812 } { 7813 var PodRestartPolicy core.RestartPolicy 7814 PodRestartPolicy = "Never" 7815 if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { 7816 t.Errorf("expected success for '%s' but got errors: %v", title, errs) 7817 } 7818 7819 PodRestartPolicy = "Always" 7820 if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { 7821 t.Errorf("expected success for '%s' but got errors: %v", title, errs) 7822 } 7823 7824 PodRestartPolicy = "OnFailure" 7825 if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { 7826 t.Errorf("expected success for '%s' but got errors: %v", title, errs) 7827 } 7828 } 7829 7830 // Failure Cases 7831 tcs := []struct { 7832 title, line string 7833 ephemeralContainers []core.EphemeralContainer 7834 expectedErrors field.ErrorList 7835 }{{ 7836 "Name Collision with Container.Containers", 7837 line(), 7838 []core.EphemeralContainer{ 7839 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7840 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7841 }, 7842 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}}, 7843 }, { 7844 "Name Collision with Container.InitContainers", 7845 line(), 7846 []core.EphemeralContainer{ 7847 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ictr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7848 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7849 }, 7850 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}}, 7851 }, { 7852 "Name Collision with EphemeralContainers", 7853 line(), 7854 []core.EphemeralContainer{ 7855 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7856 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7857 }, 7858 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[1].name"}}, 7859 }, { 7860 "empty Container", 7861 line(), 7862 []core.EphemeralContainer{ 7863 {EphemeralContainerCommon: core.EphemeralContainerCommon{}}, 7864 }, 7865 field.ErrorList{ 7866 {Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"}, 7867 {Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].image"}, 7868 {Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].terminationMessagePolicy"}, 7869 {Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].imagePullPolicy"}, 7870 }, 7871 }, { 7872 "empty Container Name", 7873 line(), 7874 []core.EphemeralContainer{ 7875 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7876 }, 7877 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"}}, 7878 }, { 7879 "whitespace padded image name", 7880 line(), 7881 []core.EphemeralContainer{ 7882 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: " image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 7883 }, 7884 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "ephemeralContainers[0].image"}}, 7885 }, { 7886 "invalid image pull policy", 7887 line(), 7888 []core.EphemeralContainer{ 7889 {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "PullThreeTimes", TerminationMessagePolicy: "File"}}, 7890 }, 7891 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "ephemeralContainers[0].imagePullPolicy"}}, 7892 }, { 7893 "TargetContainerName doesn't exist", 7894 line(), 7895 []core.EphemeralContainer{{ 7896 EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7897 TargetContainerName: "bogus", 7898 }}, 7899 field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[0].targetContainerName"}}, 7900 }, { 7901 "Targets an ephemeral container", 7902 line(), 7903 []core.EphemeralContainer{{ 7904 EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7905 }, { 7906 EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debugception", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 7907 TargetContainerName: "debug", 7908 }}, 7909 field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[1].targetContainerName"}}, 7910 }, { 7911 "Container uses disallowed field: Lifecycle", 7912 line(), 7913 []core.EphemeralContainer{{ 7914 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7915 Name: "debug", 7916 Image: "image", 7917 ImagePullPolicy: "IfNotPresent", 7918 TerminationMessagePolicy: "File", 7919 Lifecycle: &core.Lifecycle{ 7920 PreStop: &core.LifecycleHandler{ 7921 Exec: &core.ExecAction{Command: []string{"ls", "-l"}}, 7922 }, 7923 }, 7924 }, 7925 }}, 7926 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"}}, 7927 }, { 7928 "Container uses disallowed field: LivenessProbe", 7929 line(), 7930 []core.EphemeralContainer{{ 7931 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7932 Name: "debug", 7933 Image: "image", 7934 ImagePullPolicy: "IfNotPresent", 7935 TerminationMessagePolicy: "File", 7936 LivenessProbe: &core.Probe{ 7937 ProbeHandler: core.ProbeHandler{ 7938 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 7939 }, 7940 SuccessThreshold: 1, 7941 }, 7942 }, 7943 }}, 7944 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].livenessProbe"}}, 7945 }, { 7946 "Container uses disallowed field: Ports", 7947 line(), 7948 []core.EphemeralContainer{{ 7949 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7950 Name: "debug", 7951 Image: "image", 7952 ImagePullPolicy: "IfNotPresent", 7953 TerminationMessagePolicy: "File", 7954 Ports: []core.ContainerPort{ 7955 {Protocol: "TCP", ContainerPort: 80}, 7956 }, 7957 }, 7958 }}, 7959 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].ports"}}, 7960 }, { 7961 "Container uses disallowed field: ReadinessProbe", 7962 line(), 7963 []core.EphemeralContainer{{ 7964 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7965 Name: "debug", 7966 Image: "image", 7967 ImagePullPolicy: "IfNotPresent", 7968 TerminationMessagePolicy: "File", 7969 ReadinessProbe: &core.Probe{ 7970 ProbeHandler: core.ProbeHandler{ 7971 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 7972 }, 7973 }, 7974 }, 7975 }}, 7976 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].readinessProbe"}}, 7977 }, { 7978 "Container uses disallowed field: StartupProbe", 7979 line(), 7980 []core.EphemeralContainer{{ 7981 EphemeralContainerCommon: core.EphemeralContainerCommon{ 7982 Name: "debug", 7983 Image: "image", 7984 ImagePullPolicy: "IfNotPresent", 7985 TerminationMessagePolicy: "File", 7986 StartupProbe: &core.Probe{ 7987 ProbeHandler: core.ProbeHandler{ 7988 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 7989 }, 7990 SuccessThreshold: 1, 7991 }, 7992 }, 7993 }}, 7994 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].startupProbe"}}, 7995 }, { 7996 "Container uses disallowed field: Resources", 7997 line(), 7998 []core.EphemeralContainer{{ 7999 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8000 Name: "debug", 8001 Image: "image", 8002 ImagePullPolicy: "IfNotPresent", 8003 TerminationMessagePolicy: "File", 8004 Resources: core.ResourceRequirements{ 8005 Limits: core.ResourceList{ 8006 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8007 }, 8008 }, 8009 }, 8010 }}, 8011 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resources"}}, 8012 }, { 8013 "Container uses disallowed field: VolumeMount.SubPath", 8014 line(), 8015 []core.EphemeralContainer{{ 8016 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8017 Name: "debug", 8018 Image: "image", 8019 ImagePullPolicy: "IfNotPresent", 8020 TerminationMessagePolicy: "File", 8021 VolumeMounts: []core.VolumeMount{ 8022 {Name: "vol", MountPath: "/vol"}, 8023 {Name: "vol", MountPath: "/volsub", SubPath: "foo"}, 8024 }, 8025 }, 8026 }}, 8027 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPath"}}, 8028 }, { 8029 "Container uses disallowed field: VolumeMount.SubPathExpr", 8030 line(), 8031 []core.EphemeralContainer{{ 8032 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8033 Name: "debug", 8034 Image: "image", 8035 ImagePullPolicy: "IfNotPresent", 8036 TerminationMessagePolicy: "File", 8037 VolumeMounts: []core.VolumeMount{ 8038 {Name: "vol", MountPath: "/vol"}, 8039 {Name: "vol", MountPath: "/volsub", SubPathExpr: "$(POD_NAME)"}, 8040 }, 8041 }, 8042 }}, 8043 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPathExpr"}}, 8044 }, { 8045 "Disallowed field with other errors should only return a single Forbidden", 8046 line(), 8047 []core.EphemeralContainer{{ 8048 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8049 Name: "debug", 8050 Image: "image", 8051 ImagePullPolicy: "IfNotPresent", 8052 TerminationMessagePolicy: "File", 8053 Lifecycle: &core.Lifecycle{ 8054 PreStop: &core.LifecycleHandler{ 8055 Exec: &core.ExecAction{Command: []string{}}, 8056 }, 8057 }, 8058 }, 8059 }}, 8060 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"}}, 8061 }, { 8062 "Container uses disallowed field: ResizePolicy", 8063 line(), 8064 []core.EphemeralContainer{{ 8065 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8066 Name: "resources-resize-policy", 8067 Image: "image", 8068 ImagePullPolicy: "IfNotPresent", 8069 TerminationMessagePolicy: "File", 8070 ResizePolicy: []core.ContainerResizePolicy{ 8071 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 8072 }, 8073 }, 8074 }}, 8075 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resizePolicy"}}, 8076 }, { 8077 "Forbidden RestartPolicy: Always", 8078 line(), 8079 []core.EphemeralContainer{{ 8080 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8081 Name: "foo", 8082 Image: "image", 8083 ImagePullPolicy: "IfNotPresent", 8084 TerminationMessagePolicy: "File", 8085 RestartPolicy: &containerRestartPolicyAlways, 8086 }, 8087 }}, 8088 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, 8089 }, { 8090 "Forbidden RestartPolicy: OnFailure", 8091 line(), 8092 []core.EphemeralContainer{{ 8093 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8094 Name: "foo", 8095 Image: "image", 8096 ImagePullPolicy: "IfNotPresent", 8097 TerminationMessagePolicy: "File", 8098 RestartPolicy: &containerRestartPolicyOnFailure, 8099 }, 8100 }}, 8101 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, 8102 }, { 8103 "Forbidden RestartPolicy: Never", 8104 line(), 8105 []core.EphemeralContainer{{ 8106 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8107 Name: "foo", 8108 Image: "image", 8109 ImagePullPolicy: "IfNotPresent", 8110 TerminationMessagePolicy: "File", 8111 RestartPolicy: &containerRestartPolicyNever, 8112 }, 8113 }}, 8114 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, 8115 }, { 8116 "Forbidden RestartPolicy: invalid", 8117 line(), 8118 []core.EphemeralContainer{{ 8119 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8120 Name: "foo", 8121 Image: "image", 8122 ImagePullPolicy: "IfNotPresent", 8123 TerminationMessagePolicy: "File", 8124 RestartPolicy: &containerRestartPolicyInvalid, 8125 }, 8126 }}, 8127 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, 8128 }, { 8129 "Forbidden RestartPolicy: empty", 8130 line(), 8131 []core.EphemeralContainer{{ 8132 EphemeralContainerCommon: core.EphemeralContainerCommon{ 8133 Name: "foo", 8134 Image: "image", 8135 ImagePullPolicy: "IfNotPresent", 8136 TerminationMessagePolicy: "File", 8137 RestartPolicy: &containerRestartPolicyEmpty, 8138 }, 8139 }}, 8140 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, 8141 }, 8142 } 8143 8144 var PodRestartPolicy core.RestartPolicy 8145 8146 for _, tc := range tcs { 8147 t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) { 8148 8149 PodRestartPolicy = "Never" 8150 errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) 8151 if len(errs) == 0 { 8152 t.Fatal("expected error but received none") 8153 } 8154 8155 PodRestartPolicy = "Always" 8156 errs = validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) 8157 if len(errs) == 0 { 8158 t.Fatal("expected error but received none") 8159 } 8160 8161 PodRestartPolicy = "OnFailure" 8162 errs = validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, nil, field.NewPath("ephemeralContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) 8163 if len(errs) == 0 { 8164 t.Fatal("expected error but received none") 8165 } 8166 8167 if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" { 8168 t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff) 8169 t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs)) 8170 } 8171 }) 8172 } 8173 } 8174 8175 func TestValidateWindowsPodSecurityContext(t *testing.T) { 8176 validWindowsSC := &core.PodSecurityContext{WindowsOptions: &core.WindowsSecurityContextOptions{RunAsUserName: utilpointer.String("dummy")}} 8177 invalidWindowsSC := &core.PodSecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummyRole"}} 8178 cases := map[string]struct { 8179 podSec *core.PodSpec 8180 expectErr bool 8181 errorType field.ErrorType 8182 errorDetail string 8183 }{ 8184 "valid SC, windows, no error": { 8185 podSec: &core.PodSpec{SecurityContext: validWindowsSC}, 8186 expectErr: false, 8187 }, 8188 "invalid SC, windows, error": { 8189 podSec: &core.PodSpec{SecurityContext: invalidWindowsSC}, 8190 errorType: "FieldValueForbidden", 8191 errorDetail: "cannot be set for a windows pod", 8192 expectErr: true, 8193 }, 8194 } 8195 for k, v := range cases { 8196 t.Run(k, func(t *testing.T) { 8197 errs := validateWindows(v.podSec, field.NewPath("field")) 8198 if v.expectErr && len(errs) > 0 { 8199 if errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 8200 t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs) 8201 } 8202 } else if v.expectErr && len(errs) == 0 { 8203 t.Errorf("Unexpected success") 8204 } 8205 if !v.expectErr && len(errs) != 0 { 8206 t.Errorf("Unexpected error(s): %v", errs) 8207 } 8208 }) 8209 } 8210 } 8211 8212 func TestValidateLinuxPodSecurityContext(t *testing.T) { 8213 runAsUser := int64(1) 8214 validLinuxSC := &core.PodSecurityContext{ 8215 SELinuxOptions: &core.SELinuxOptions{ 8216 User: "user", 8217 Role: "role", 8218 Type: "type", 8219 Level: "level", 8220 }, 8221 RunAsUser: &runAsUser, 8222 } 8223 invalidLinuxSC := &core.PodSecurityContext{ 8224 WindowsOptions: &core.WindowsSecurityContextOptions{RunAsUserName: utilpointer.String("myUser")}, 8225 } 8226 8227 cases := map[string]struct { 8228 podSpec *core.PodSpec 8229 expectErr bool 8230 errorType field.ErrorType 8231 errorDetail string 8232 }{ 8233 "valid SC, linux, no error": { 8234 podSpec: &core.PodSpec{SecurityContext: validLinuxSC}, 8235 expectErr: false, 8236 }, 8237 "invalid SC, linux, error": { 8238 podSpec: &core.PodSpec{SecurityContext: invalidLinuxSC}, 8239 errorType: "FieldValueForbidden", 8240 errorDetail: "windows options cannot be set for a linux pod", 8241 expectErr: true, 8242 }, 8243 } 8244 for k, v := range cases { 8245 t.Run(k, func(t *testing.T) { 8246 errs := validateLinux(v.podSpec, field.NewPath("field")) 8247 if v.expectErr && len(errs) > 0 { 8248 if errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 8249 t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs) 8250 } 8251 } else if v.expectErr && len(errs) == 0 { 8252 t.Errorf("Unexpected success") 8253 } 8254 if !v.expectErr && len(errs) != 0 { 8255 t.Errorf("Unexpected error(s): %v", errs) 8256 } 8257 }) 8258 } 8259 } 8260 8261 func TestValidateContainers(t *testing.T) { 8262 volumeDevices := make(map[string]core.VolumeSource) 8263 capabilities.SetForTests(capabilities.Capabilities{ 8264 AllowPrivileged: true, 8265 }) 8266 8267 successCase := []core.Container{ 8268 {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8269 // backwards compatibility to ensure containers in pod template spec do not check for this 8270 {Name: "def", Image: " ", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8271 {Name: "ghi", Image: " some ", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8272 {Name: "123", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8273 {Name: "abc-123", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, { 8274 Name: "life-123", 8275 Image: "image", 8276 Lifecycle: &core.Lifecycle{ 8277 PreStop: &core.LifecycleHandler{ 8278 Exec: &core.ExecAction{Command: []string{"ls", "-l"}}, 8279 }, 8280 }, 8281 ImagePullPolicy: "IfNotPresent", 8282 TerminationMessagePolicy: "File", 8283 }, { 8284 Name: "resources-test", 8285 Image: "image", 8286 Resources: core.ResourceRequirements{ 8287 Limits: core.ResourceList{ 8288 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8289 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8290 core.ResourceName("my.org/resource"): resource.MustParse("10"), 8291 }, 8292 }, 8293 ImagePullPolicy: "IfNotPresent", 8294 TerminationMessagePolicy: "File", 8295 }, { 8296 Name: "resources-test-with-request-and-limit", 8297 Image: "image", 8298 Resources: core.ResourceRequirements{ 8299 Requests: core.ResourceList{ 8300 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8301 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8302 }, 8303 Limits: core.ResourceList{ 8304 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8305 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8306 }, 8307 }, 8308 ImagePullPolicy: "IfNotPresent", 8309 TerminationMessagePolicy: "File", 8310 }, { 8311 Name: "resources-request-limit-simple", 8312 Image: "image", 8313 Resources: core.ResourceRequirements{ 8314 Requests: core.ResourceList{ 8315 core.ResourceName(core.ResourceCPU): resource.MustParse("8"), 8316 }, 8317 Limits: core.ResourceList{ 8318 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8319 }, 8320 }, 8321 ImagePullPolicy: "IfNotPresent", 8322 TerminationMessagePolicy: "File", 8323 }, { 8324 Name: "resources-request-limit-edge", 8325 Image: "image", 8326 Resources: core.ResourceRequirements{ 8327 Requests: core.ResourceList{ 8328 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8329 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8330 core.ResourceName("my.org/resource"): resource.MustParse("10"), 8331 }, 8332 Limits: core.ResourceList{ 8333 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8334 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8335 core.ResourceName("my.org/resource"): resource.MustParse("10"), 8336 }, 8337 }, 8338 ImagePullPolicy: "IfNotPresent", 8339 TerminationMessagePolicy: "File", 8340 }, { 8341 Name: "resources-request-limit-partials", 8342 Image: "image", 8343 Resources: core.ResourceRequirements{ 8344 Requests: core.ResourceList{ 8345 core.ResourceName(core.ResourceCPU): resource.MustParse("9.5"), 8346 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8347 }, 8348 Limits: core.ResourceList{ 8349 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 8350 core.ResourceName("my.org/resource"): resource.MustParse("10"), 8351 }, 8352 }, 8353 ImagePullPolicy: "IfNotPresent", 8354 TerminationMessagePolicy: "File", 8355 }, { 8356 Name: "resources-request", 8357 Image: "image", 8358 Resources: core.ResourceRequirements{ 8359 Requests: core.ResourceList{ 8360 core.ResourceName(core.ResourceCPU): resource.MustParse("9.5"), 8361 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 8362 }, 8363 }, 8364 ImagePullPolicy: "IfNotPresent", 8365 TerminationMessagePolicy: "File", 8366 }, { 8367 Name: "resources-resize-policy", 8368 Image: "image", 8369 ResizePolicy: []core.ContainerResizePolicy{ 8370 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 8371 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 8372 }, 8373 ImagePullPolicy: "IfNotPresent", 8374 TerminationMessagePolicy: "File", 8375 }, { 8376 Name: "same-host-port-different-protocol", 8377 Image: "image", 8378 Ports: []core.ContainerPort{ 8379 {ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, 8380 {ContainerPort: 80, HostPort: 80, Protocol: "UDP"}, 8381 }, 8382 ImagePullPolicy: "IfNotPresent", 8383 TerminationMessagePolicy: "File", 8384 }, { 8385 Name: "fallback-to-logs-termination-message", 8386 Image: "image", 8387 ImagePullPolicy: "IfNotPresent", 8388 TerminationMessagePolicy: "FallbackToLogsOnError", 8389 }, { 8390 Name: "file-termination-message", 8391 Image: "image", 8392 ImagePullPolicy: "IfNotPresent", 8393 TerminationMessagePolicy: "File", 8394 }, { 8395 Name: "env-from-source", 8396 Image: "image", 8397 ImagePullPolicy: "IfNotPresent", 8398 TerminationMessagePolicy: "File", 8399 EnvFrom: []core.EnvFromSource{{ 8400 ConfigMapRef: &core.ConfigMapEnvSource{ 8401 LocalObjectReference: core.LocalObjectReference{ 8402 Name: "test", 8403 }, 8404 }, 8405 }}, 8406 }, 8407 {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", SecurityContext: fakeValidSecurityContext(true)}, { 8408 Name: "live-123", 8409 Image: "image", 8410 LivenessProbe: &core.Probe{ 8411 ProbeHandler: core.ProbeHandler{ 8412 TCPSocket: &core.TCPSocketAction{ 8413 Port: intstr.FromInt32(80), 8414 }, 8415 }, 8416 SuccessThreshold: 1, 8417 }, 8418 ImagePullPolicy: "IfNotPresent", 8419 TerminationMessagePolicy: "File", 8420 }, { 8421 Name: "startup-123", 8422 Image: "image", 8423 StartupProbe: &core.Probe{ 8424 ProbeHandler: core.ProbeHandler{ 8425 TCPSocket: &core.TCPSocketAction{ 8426 Port: intstr.FromInt32(80), 8427 }, 8428 }, 8429 SuccessThreshold: 1, 8430 }, 8431 ImagePullPolicy: "IfNotPresent", 8432 TerminationMessagePolicy: "File", 8433 }, { 8434 Name: "resize-policy-cpu", 8435 Image: "image", 8436 ImagePullPolicy: "IfNotPresent", 8437 TerminationMessagePolicy: "File", 8438 ResizePolicy: []core.ContainerResizePolicy{ 8439 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 8440 }, 8441 }, { 8442 Name: "resize-policy-mem", 8443 Image: "image", 8444 ImagePullPolicy: "IfNotPresent", 8445 TerminationMessagePolicy: "File", 8446 ResizePolicy: []core.ContainerResizePolicy{ 8447 {ResourceName: "memory", RestartPolicy: "RestartContainer"}, 8448 }, 8449 }, { 8450 Name: "resize-policy-cpu-and-mem", 8451 Image: "image", 8452 ImagePullPolicy: "IfNotPresent", 8453 TerminationMessagePolicy: "File", 8454 ResizePolicy: []core.ContainerResizePolicy{ 8455 {ResourceName: "memory", RestartPolicy: "NotRequired"}, 8456 {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, 8457 }, 8458 }, 8459 } 8460 8461 var PodRestartPolicy core.RestartPolicy = "Always" 8462 if errs := validateContainers(successCase, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { 8463 t.Errorf("expected success: %v", errs) 8464 } 8465 8466 capabilities.SetForTests(capabilities.Capabilities{ 8467 AllowPrivileged: false, 8468 }) 8469 errorCases := []struct { 8470 title, line string 8471 containers []core.Container 8472 expectedErrors field.ErrorList 8473 }{{ 8474 "zero-length name", 8475 line(), 8476 []core.Container{{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 8477 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].name"}}, 8478 }, { 8479 "zero-length-image", 8480 line(), 8481 []core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 8482 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}}, 8483 }, { 8484 "name > 63 characters", 8485 line(), 8486 []core.Container{{Name: strings.Repeat("a", 64), Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 8487 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}}, 8488 }, { 8489 "name not a DNS label", 8490 line(), 8491 []core.Container{{Name: "a.b.c", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 8492 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}}, 8493 }, { 8494 "name not unique", 8495 line(), 8496 []core.Container{ 8497 {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8498 {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8499 }, 8500 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "containers[1].name"}}, 8501 }, { 8502 "zero-length image", 8503 line(), 8504 []core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 8505 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}}, 8506 }, { 8507 "host port not unique", 8508 line(), 8509 []core.Container{ 8510 {Name: "abc", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}}, 8511 ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8512 {Name: "def", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}}, 8513 ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8514 }, 8515 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "containers[1].ports[0].hostPort"}}, 8516 }, { 8517 "invalid env var name", 8518 line(), 8519 []core.Container{ 8520 {Name: "abc", Image: "image", Env: []core.EnvVar{{Name: "ev!1"}}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8521 }, 8522 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].env[0].name"}}, 8523 }, { 8524 "unknown volume name", 8525 line(), 8526 []core.Container{ 8527 {Name: "abc", Image: "image", VolumeMounts: []core.VolumeMount{{Name: "anything", MountPath: "/foo"}}, 8528 ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 8529 }, 8530 field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "containers[0].volumeMounts[0].name"}}, 8531 }, { 8532 "invalid lifecycle, no exec command.", 8533 line(), 8534 []core.Container{{ 8535 Name: "life-123", 8536 Image: "image", 8537 Lifecycle: &core.Lifecycle{ 8538 PreStop: &core.LifecycleHandler{ 8539 Exec: &core.ExecAction{}, 8540 }, 8541 }, 8542 ImagePullPolicy: "IfNotPresent", 8543 TerminationMessagePolicy: "File", 8544 }}, 8545 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.exec.command"}}, 8546 }, { 8547 "invalid lifecycle, no http path.", 8548 line(), 8549 []core.Container{{ 8550 Name: "life-123", 8551 Image: "image", 8552 Lifecycle: &core.Lifecycle{ 8553 PreStop: &core.LifecycleHandler{ 8554 HTTPGet: &core.HTTPGetAction{ 8555 Port: intstr.FromInt32(80), 8556 Scheme: "HTTP", 8557 }, 8558 }, 8559 }, 8560 ImagePullPolicy: "IfNotPresent", 8561 TerminationMessagePolicy: "File", 8562 }}, 8563 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.httpGet.path"}}, 8564 }, { 8565 "invalid lifecycle, no http port.", 8566 line(), 8567 []core.Container{{ 8568 Name: "life-123", 8569 Image: "image", 8570 Lifecycle: &core.Lifecycle{ 8571 PreStop: &core.LifecycleHandler{ 8572 HTTPGet: &core.HTTPGetAction{ 8573 Path: "/", 8574 Scheme: "HTTP", 8575 }, 8576 }, 8577 }, 8578 ImagePullPolicy: "IfNotPresent", 8579 TerminationMessagePolicy: "File", 8580 }}, 8581 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.httpGet.port"}}, 8582 }, { 8583 "invalid lifecycle, no http scheme.", 8584 line(), 8585 []core.Container{{ 8586 Name: "life-123", 8587 Image: "image", 8588 Lifecycle: &core.Lifecycle{ 8589 PreStop: &core.LifecycleHandler{ 8590 HTTPGet: &core.HTTPGetAction{ 8591 Path: "/", 8592 Port: intstr.FromInt32(80), 8593 }, 8594 }, 8595 }, 8596 ImagePullPolicy: "IfNotPresent", 8597 TerminationMessagePolicy: "File", 8598 }}, 8599 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].lifecycle.preStop.httpGet.scheme"}}, 8600 }, { 8601 "invalid lifecycle, no tcp socket port.", 8602 line(), 8603 []core.Container{{ 8604 Name: "life-123", 8605 Image: "image", 8606 Lifecycle: &core.Lifecycle{ 8607 PreStop: &core.LifecycleHandler{ 8608 TCPSocket: &core.TCPSocketAction{}, 8609 }, 8610 }, 8611 ImagePullPolicy: "IfNotPresent", 8612 TerminationMessagePolicy: "File", 8613 }}, 8614 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}}, 8615 }, { 8616 "invalid lifecycle, zero tcp socket port.", 8617 line(), 8618 []core.Container{{ 8619 Name: "life-123", 8620 Image: "image", 8621 Lifecycle: &core.Lifecycle{ 8622 PreStop: &core.LifecycleHandler{ 8623 TCPSocket: &core.TCPSocketAction{ 8624 Port: intstr.FromInt32(0), 8625 }, 8626 }, 8627 }, 8628 ImagePullPolicy: "IfNotPresent", 8629 TerminationMessagePolicy: "File", 8630 }}, 8631 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}}, 8632 }, { 8633 "invalid lifecycle, no action.", 8634 line(), 8635 []core.Container{{ 8636 Name: "life-123", 8637 Image: "image", 8638 Lifecycle: &core.Lifecycle{ 8639 PreStop: &core.LifecycleHandler{}, 8640 }, 8641 ImagePullPolicy: "IfNotPresent", 8642 TerminationMessagePolicy: "File", 8643 }}, 8644 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop"}}, 8645 }, { 8646 "invalid readiness probe, terminationGracePeriodSeconds set.", 8647 line(), 8648 []core.Container{{ 8649 Name: "life-123", 8650 Image: "image", 8651 ReadinessProbe: &core.Probe{ 8652 ProbeHandler: core.ProbeHandler{ 8653 TCPSocket: &core.TCPSocketAction{ 8654 Port: intstr.FromInt32(80), 8655 }, 8656 }, 8657 TerminationGracePeriodSeconds: utilpointer.Int64(10), 8658 }, 8659 ImagePullPolicy: "IfNotPresent", 8660 TerminationMessagePolicy: "File", 8661 }}, 8662 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"}}, 8663 }, { 8664 "invalid liveness probe, no tcp socket port.", 8665 line(), 8666 []core.Container{{ 8667 Name: "live-123", 8668 Image: "image", 8669 LivenessProbe: &core.Probe{ 8670 ProbeHandler: core.ProbeHandler{ 8671 TCPSocket: &core.TCPSocketAction{}, 8672 }, 8673 SuccessThreshold: 1, 8674 }, 8675 ImagePullPolicy: "IfNotPresent", 8676 TerminationMessagePolicy: "File", 8677 }}, 8678 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.tcpSocket.port"}}, 8679 }, { 8680 "invalid liveness probe, no action.", 8681 line(), 8682 []core.Container{{ 8683 Name: "live-123", 8684 Image: "image", 8685 LivenessProbe: &core.Probe{ 8686 ProbeHandler: core.ProbeHandler{}, 8687 SuccessThreshold: 1, 8688 }, 8689 ImagePullPolicy: "IfNotPresent", 8690 TerminationMessagePolicy: "File", 8691 }}, 8692 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].livenessProbe"}}, 8693 }, { 8694 "invalid liveness probe, successThreshold != 1", 8695 line(), 8696 []core.Container{{ 8697 Name: "live-123", 8698 Image: "image", 8699 LivenessProbe: &core.Probe{ 8700 ProbeHandler: core.ProbeHandler{ 8701 TCPSocket: &core.TCPSocketAction{ 8702 Port: intstr.FromInt32(80), 8703 }, 8704 }, 8705 SuccessThreshold: 2, 8706 }, 8707 ImagePullPolicy: "IfNotPresent", 8708 TerminationMessagePolicy: "File", 8709 }}, 8710 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"}}, 8711 }, { 8712 "invalid startup probe, successThreshold != 1", 8713 line(), 8714 []core.Container{{ 8715 Name: "startup-123", 8716 Image: "image", 8717 StartupProbe: &core.Probe{ 8718 ProbeHandler: core.ProbeHandler{ 8719 TCPSocket: &core.TCPSocketAction{ 8720 Port: intstr.FromInt32(80), 8721 }, 8722 }, 8723 SuccessThreshold: 2, 8724 }, 8725 ImagePullPolicy: "IfNotPresent", 8726 TerminationMessagePolicy: "File", 8727 }}, 8728 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"}}, 8729 }, { 8730 "invalid liveness probe, negative numbers", 8731 line(), 8732 []core.Container{{ 8733 Name: "live-123", 8734 Image: "image", 8735 LivenessProbe: &core.Probe{ 8736 ProbeHandler: core.ProbeHandler{ 8737 TCPSocket: &core.TCPSocketAction{ 8738 Port: intstr.FromInt32(80), 8739 }, 8740 }, 8741 InitialDelaySeconds: -1, 8742 TimeoutSeconds: -1, 8743 PeriodSeconds: -1, 8744 SuccessThreshold: -1, 8745 FailureThreshold: -1, 8746 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 8747 }, 8748 ImagePullPolicy: "IfNotPresent", 8749 TerminationMessagePolicy: "File", 8750 }}, 8751 field.ErrorList{ 8752 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.initialDelaySeconds"}, 8753 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.timeoutSeconds"}, 8754 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.periodSeconds"}, 8755 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"}, 8756 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.failureThreshold"}, 8757 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.terminationGracePeriodSeconds"}, 8758 {Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"}, 8759 }, 8760 }, { 8761 "invalid readiness probe, negative numbers", 8762 line(), 8763 []core.Container{{ 8764 Name: "ready-123", 8765 Image: "image", 8766 ReadinessProbe: &core.Probe{ 8767 ProbeHandler: core.ProbeHandler{ 8768 TCPSocket: &core.TCPSocketAction{ 8769 Port: intstr.FromInt32(80), 8770 }, 8771 }, 8772 InitialDelaySeconds: -1, 8773 TimeoutSeconds: -1, 8774 PeriodSeconds: -1, 8775 SuccessThreshold: -1, 8776 FailureThreshold: -1, 8777 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 8778 }, 8779 ImagePullPolicy: "IfNotPresent", 8780 TerminationMessagePolicy: "File", 8781 }}, 8782 field.ErrorList{ 8783 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.initialDelaySeconds"}, 8784 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.timeoutSeconds"}, 8785 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.periodSeconds"}, 8786 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.successThreshold"}, 8787 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.failureThreshold"}, 8788 // terminationGracePeriodSeconds returns multiple validation errors here: 8789 // containers[0].readinessProbe.terminationGracePeriodSeconds: Invalid value: -1: must be greater than 0 8790 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"}, 8791 // containers[0].readinessProbe.terminationGracePeriodSeconds: Invalid value: -1: must not be set for readinessProbes 8792 {Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"}, 8793 }, 8794 }, { 8795 "invalid startup probe, negative numbers", 8796 line(), 8797 []core.Container{{ 8798 Name: "startup-123", 8799 Image: "image", 8800 StartupProbe: &core.Probe{ 8801 ProbeHandler: core.ProbeHandler{ 8802 TCPSocket: &core.TCPSocketAction{ 8803 Port: intstr.FromInt32(80), 8804 }, 8805 }, 8806 InitialDelaySeconds: -1, 8807 TimeoutSeconds: -1, 8808 PeriodSeconds: -1, 8809 SuccessThreshold: -1, 8810 FailureThreshold: -1, 8811 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 8812 }, 8813 ImagePullPolicy: "IfNotPresent", 8814 TerminationMessagePolicy: "File", 8815 }}, 8816 field.ErrorList{ 8817 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.initialDelaySeconds"}, 8818 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.timeoutSeconds"}, 8819 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.periodSeconds"}, 8820 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"}, 8821 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.failureThreshold"}, 8822 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.terminationGracePeriodSeconds"}, 8823 {Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"}, 8824 }, 8825 }, { 8826 "invalid message termination policy", 8827 line(), 8828 []core.Container{{ 8829 Name: "life-123", 8830 Image: "image", 8831 ImagePullPolicy: "IfNotPresent", 8832 TerminationMessagePolicy: "Unknown", 8833 }}, 8834 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].terminationMessagePolicy"}}, 8835 }, { 8836 "empty message termination policy", 8837 line(), 8838 []core.Container{{ 8839 Name: "life-123", 8840 Image: "image", 8841 ImagePullPolicy: "IfNotPresent", 8842 TerminationMessagePolicy: "", 8843 }}, 8844 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].terminationMessagePolicy"}}, 8845 }, { 8846 "privilege disabled", 8847 line(), 8848 []core.Container{{ 8849 Name: "abc", 8850 Image: "image", 8851 SecurityContext: fakeValidSecurityContext(true), 8852 ImagePullPolicy: "IfNotPresent", 8853 TerminationMessagePolicy: "File", 8854 }}, 8855 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].securityContext.privileged"}}, 8856 }, { 8857 "invalid compute resource", 8858 line(), 8859 []core.Container{{ 8860 Name: "abc-123", 8861 Image: "image", 8862 Resources: core.ResourceRequirements{ 8863 Limits: core.ResourceList{ 8864 "disk": resource.MustParse("10G"), 8865 }, 8866 }, 8867 ImagePullPolicy: "IfNotPresent", 8868 TerminationMessagePolicy: "File", 8869 }}, 8870 field.ErrorList{ 8871 {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"}, 8872 {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"}, 8873 }, 8874 }, { 8875 "Resource CPU invalid", 8876 line(), 8877 []core.Container{{ 8878 Name: "abc-123", 8879 Image: "image", 8880 Resources: core.ResourceRequirements{ 8881 Limits: getResourceLimits("-10", "0"), 8882 }, 8883 ImagePullPolicy: "IfNotPresent", 8884 TerminationMessagePolicy: "File", 8885 }}, 8886 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[cpu]"}}, 8887 }, { 8888 "Resource Requests CPU invalid", 8889 line(), 8890 []core.Container{{ 8891 Name: "abc-123", 8892 Image: "image", 8893 Resources: core.ResourceRequirements{ 8894 Requests: getResourceLimits("-10", "0"), 8895 }, 8896 ImagePullPolicy: "IfNotPresent", 8897 TerminationMessagePolicy: "File", 8898 }}, 8899 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests[cpu]"}}, 8900 }, { 8901 "Resource Memory invalid", 8902 line(), 8903 []core.Container{{ 8904 Name: "abc-123", 8905 Image: "image", 8906 Resources: core.ResourceRequirements{ 8907 Limits: getResourceLimits("0", "-10"), 8908 }, 8909 ImagePullPolicy: "IfNotPresent", 8910 TerminationMessagePolicy: "File", 8911 }}, 8912 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[memory]"}}, 8913 }, { 8914 "Request limit simple invalid", 8915 line(), 8916 []core.Container{{ 8917 Name: "abc-123", 8918 Image: "image", 8919 Resources: core.ResourceRequirements{ 8920 Limits: getResourceLimits("5", "3"), 8921 Requests: getResourceLimits("6", "3"), 8922 }, 8923 ImagePullPolicy: "IfNotPresent", 8924 TerminationMessagePolicy: "File", 8925 }}, 8926 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, 8927 }, { 8928 "Invalid storage limit request", 8929 line(), 8930 []core.Container{{ 8931 Name: "abc-123", 8932 Image: "image", 8933 Resources: core.ResourceRequirements{ 8934 Limits: core.ResourceList{ 8935 core.ResourceName("attachable-volumes-aws-ebs"): *resource.NewQuantity(10, resource.DecimalSI), 8936 }, 8937 }, 8938 ImagePullPolicy: "IfNotPresent", 8939 TerminationMessagePolicy: "File", 8940 }}, 8941 field.ErrorList{ 8942 {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"}, 8943 {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"}, 8944 }, 8945 }, { 8946 "CPU request limit multiple invalid", 8947 line(), 8948 []core.Container{{ 8949 Name: "abc-123", 8950 Image: "image", 8951 Resources: core.ResourceRequirements{ 8952 Limits: getResourceLimits("5", "3"), 8953 Requests: getResourceLimits("6", "3"), 8954 }, 8955 ImagePullPolicy: "IfNotPresent", 8956 TerminationMessagePolicy: "File", 8957 }}, 8958 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, 8959 }, { 8960 "Memory request limit multiple invalid", 8961 line(), 8962 []core.Container{{ 8963 Name: "abc-123", 8964 Image: "image", 8965 Resources: core.ResourceRequirements{ 8966 Limits: getResourceLimits("5", "3"), 8967 Requests: getResourceLimits("5", "4"), 8968 }, 8969 ImagePullPolicy: "IfNotPresent", 8970 TerminationMessagePolicy: "File", 8971 }}, 8972 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, 8973 }, { 8974 "Invalid env from", 8975 line(), 8976 []core.Container{{ 8977 Name: "env-from-source", 8978 Image: "image", 8979 ImagePullPolicy: "IfNotPresent", 8980 TerminationMessagePolicy: "File", 8981 EnvFrom: []core.EnvFromSource{{ 8982 ConfigMapRef: &core.ConfigMapEnvSource{ 8983 LocalObjectReference: core.LocalObjectReference{ 8984 Name: "$%^&*#", 8985 }, 8986 }, 8987 }}, 8988 }}, 8989 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].envFrom[0].configMapRef.name"}}, 8990 }, { 8991 "Unsupported resize policy for memory", 8992 line(), 8993 []core.Container{{ 8994 Name: "resize-policy-mem-invalid", 8995 Image: "image", 8996 ImagePullPolicy: "IfNotPresent", 8997 TerminationMessagePolicy: "File", 8998 ResizePolicy: []core.ContainerResizePolicy{ 8999 {ResourceName: "memory", RestartPolicy: "RestartContainerrrr"}, 9000 }, 9001 }}, 9002 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].resizePolicy"}}, 9003 }, { 9004 "Unsupported resize policy for CPU", 9005 line(), 9006 []core.Container{{ 9007 Name: "resize-policy-cpu-invalid", 9008 Image: "image", 9009 ImagePullPolicy: "IfNotPresent", 9010 TerminationMessagePolicy: "File", 9011 ResizePolicy: []core.ContainerResizePolicy{ 9012 {ResourceName: "cpu", RestartPolicy: "RestartNotRequired"}, 9013 }, 9014 }}, 9015 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].resizePolicy"}}, 9016 }, { 9017 "Forbidden RestartPolicy: Always", 9018 line(), 9019 []core.Container{{ 9020 Name: "foo", 9021 Image: "image", 9022 ImagePullPolicy: "IfNotPresent", 9023 TerminationMessagePolicy: "File", 9024 RestartPolicy: &containerRestartPolicyAlways, 9025 }}, 9026 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, 9027 }, { 9028 "Forbidden RestartPolicy: OnFailure", 9029 line(), 9030 []core.Container{{ 9031 Name: "foo", 9032 Image: "image", 9033 ImagePullPolicy: "IfNotPresent", 9034 TerminationMessagePolicy: "File", 9035 RestartPolicy: &containerRestartPolicyOnFailure, 9036 }}, 9037 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, 9038 }, { 9039 "Forbidden RestartPolicy: Never", 9040 line(), 9041 []core.Container{{ 9042 Name: "foo", 9043 Image: "image", 9044 ImagePullPolicy: "IfNotPresent", 9045 TerminationMessagePolicy: "File", 9046 RestartPolicy: &containerRestartPolicyNever, 9047 }}, 9048 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, 9049 }, { 9050 "Forbidden RestartPolicy: invalid", 9051 line(), 9052 []core.Container{{ 9053 Name: "foo", 9054 Image: "image", 9055 ImagePullPolicy: "IfNotPresent", 9056 TerminationMessagePolicy: "File", 9057 RestartPolicy: &containerRestartPolicyInvalid, 9058 }}, 9059 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, 9060 }, { 9061 "Forbidden RestartPolicy: empty", 9062 line(), 9063 []core.Container{{ 9064 Name: "foo", 9065 Image: "image", 9066 ImagePullPolicy: "IfNotPresent", 9067 TerminationMessagePolicy: "File", 9068 RestartPolicy: &containerRestartPolicyEmpty, 9069 }}, 9070 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, 9071 }, 9072 } 9073 9074 for _, tc := range errorCases { 9075 t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) { 9076 errs := validateContainers(tc.containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("containers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) 9077 if len(errs) == 0 { 9078 t.Fatal("expected error but received none") 9079 } 9080 9081 if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" { 9082 t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff) 9083 t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs)) 9084 } 9085 }) 9086 } 9087 } 9088 9089 func TestValidateInitContainers(t *testing.T) { 9090 volumeDevices := make(map[string]core.VolumeSource) 9091 capabilities.SetForTests(capabilities.Capabilities{ 9092 AllowPrivileged: true, 9093 }) 9094 9095 containers := []core.Container{{ 9096 Name: "app", 9097 Image: "nginx", 9098 ImagePullPolicy: "IfNotPresent", 9099 TerminationMessagePolicy: "File", 9100 }, 9101 } 9102 9103 successCase := []core.Container{{ 9104 Name: "container-1-same-host-port-different-protocol", 9105 Image: "image", 9106 Ports: []core.ContainerPort{ 9107 {ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, 9108 {ContainerPort: 80, HostPort: 80, Protocol: "UDP"}, 9109 }, 9110 ImagePullPolicy: "IfNotPresent", 9111 TerminationMessagePolicy: "File", 9112 }, { 9113 Name: "container-2-same-host-port-different-protocol", 9114 Image: "image", 9115 Ports: []core.ContainerPort{ 9116 {ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, 9117 {ContainerPort: 80, HostPort: 80, Protocol: "UDP"}, 9118 }, 9119 ImagePullPolicy: "IfNotPresent", 9120 TerminationMessagePolicy: "File", 9121 }, { 9122 Name: "container-3-restart-always-with-lifecycle-hook-and-probes", 9123 Image: "image", 9124 ImagePullPolicy: "IfNotPresent", 9125 TerminationMessagePolicy: "File", 9126 RestartPolicy: &containerRestartPolicyAlways, 9127 Lifecycle: &core.Lifecycle{ 9128 PostStart: &core.LifecycleHandler{ 9129 Exec: &core.ExecAction{ 9130 Command: []string{"echo", "post start"}, 9131 }, 9132 }, 9133 PreStop: &core.LifecycleHandler{ 9134 Exec: &core.ExecAction{ 9135 Command: []string{"echo", "pre stop"}, 9136 }, 9137 }, 9138 }, 9139 LivenessProbe: &core.Probe{ 9140 ProbeHandler: core.ProbeHandler{ 9141 TCPSocket: &core.TCPSocketAction{ 9142 Port: intstr.FromInt32(80), 9143 }, 9144 }, 9145 SuccessThreshold: 1, 9146 }, 9147 ReadinessProbe: &core.Probe{ 9148 ProbeHandler: core.ProbeHandler{ 9149 TCPSocket: &core.TCPSocketAction{ 9150 Port: intstr.FromInt32(80), 9151 }, 9152 }, 9153 }, 9154 StartupProbe: &core.Probe{ 9155 ProbeHandler: core.ProbeHandler{ 9156 TCPSocket: &core.TCPSocketAction{ 9157 Port: intstr.FromInt32(80), 9158 }, 9159 }, 9160 SuccessThreshold: 1, 9161 }, 9162 }, 9163 } 9164 var PodRestartPolicy core.RestartPolicy = "Never" 9165 if errs := validateInitContainers(successCase, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { 9166 t.Errorf("expected success: %v", errs) 9167 } 9168 9169 capabilities.SetForTests(capabilities.Capabilities{ 9170 AllowPrivileged: false, 9171 }) 9172 errorCases := []struct { 9173 title, line string 9174 initContainers []core.Container 9175 expectedErrors field.ErrorList 9176 }{{ 9177 "empty name", 9178 line(), 9179 []core.Container{{ 9180 Name: "", 9181 Image: "image", 9182 ImagePullPolicy: "IfNotPresent", 9183 TerminationMessagePolicy: "File", 9184 }}, 9185 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "initContainers[0].name", BadValue: ""}}, 9186 }, { 9187 "name collision with regular container", 9188 line(), 9189 []core.Container{{ 9190 Name: "app", 9191 Image: "image", 9192 ImagePullPolicy: "IfNotPresent", 9193 TerminationMessagePolicy: "File", 9194 }}, 9195 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].name", BadValue: "app"}}, 9196 }, { 9197 "invalid termination message policy", 9198 line(), 9199 []core.Container{{ 9200 Name: "init", 9201 Image: "image", 9202 ImagePullPolicy: "IfNotPresent", 9203 TerminationMessagePolicy: "Unknown", 9204 }}, 9205 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].terminationMessagePolicy", BadValue: core.TerminationMessagePolicy("Unknown")}}, 9206 }, { 9207 "duplicate names", 9208 line(), 9209 []core.Container{{ 9210 Name: "init", 9211 Image: "image", 9212 ImagePullPolicy: "IfNotPresent", 9213 TerminationMessagePolicy: "File", 9214 }, { 9215 Name: "init", 9216 Image: "image", 9217 ImagePullPolicy: "IfNotPresent", 9218 TerminationMessagePolicy: "File", 9219 }}, 9220 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[1].name", BadValue: "init"}}, 9221 }, { 9222 "duplicate ports", 9223 line(), 9224 []core.Container{{ 9225 Name: "abc", 9226 Image: "image", 9227 Ports: []core.ContainerPort{{ 9228 ContainerPort: 8080, HostPort: 8080, Protocol: "TCP", 9229 }, { 9230 ContainerPort: 8080, HostPort: 8080, Protocol: "TCP", 9231 }}, 9232 ImagePullPolicy: "IfNotPresent", 9233 TerminationMessagePolicy: "File", 9234 }}, 9235 field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].ports[1].hostPort", BadValue: "TCP//8080"}}, 9236 }, { 9237 "uses disallowed field: Lifecycle", 9238 line(), 9239 []core.Container{{ 9240 Name: "debug", 9241 Image: "image", 9242 ImagePullPolicy: "IfNotPresent", 9243 TerminationMessagePolicy: "File", 9244 Lifecycle: &core.Lifecycle{ 9245 PreStop: &core.LifecycleHandler{ 9246 Exec: &core.ExecAction{Command: []string{"ls", "-l"}}, 9247 }, 9248 }, 9249 }}, 9250 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].lifecycle", BadValue: ""}}, 9251 }, { 9252 "uses disallowed field: LivenessProbe", 9253 line(), 9254 []core.Container{{ 9255 Name: "debug", 9256 Image: "image", 9257 ImagePullPolicy: "IfNotPresent", 9258 TerminationMessagePolicy: "File", 9259 LivenessProbe: &core.Probe{ 9260 ProbeHandler: core.ProbeHandler{ 9261 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 9262 }, 9263 SuccessThreshold: 1, 9264 }, 9265 }}, 9266 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].livenessProbe", BadValue: ""}}, 9267 }, { 9268 "uses disallowed field: ReadinessProbe", 9269 line(), 9270 []core.Container{{ 9271 Name: "debug", 9272 Image: "image", 9273 ImagePullPolicy: "IfNotPresent", 9274 TerminationMessagePolicy: "File", 9275 ReadinessProbe: &core.Probe{ 9276 ProbeHandler: core.ProbeHandler{ 9277 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 9278 }, 9279 }, 9280 }}, 9281 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].readinessProbe", BadValue: ""}}, 9282 }, { 9283 "Container uses disallowed field: StartupProbe", 9284 line(), 9285 []core.Container{{ 9286 Name: "debug", 9287 Image: "image", 9288 ImagePullPolicy: "IfNotPresent", 9289 TerminationMessagePolicy: "File", 9290 StartupProbe: &core.Probe{ 9291 ProbeHandler: core.ProbeHandler{ 9292 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 9293 }, 9294 SuccessThreshold: 1, 9295 }, 9296 }}, 9297 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}}, 9298 }, { 9299 "Disallowed field with other errors should only return a single Forbidden", 9300 line(), 9301 []core.Container{{ 9302 Name: "debug", 9303 Image: "image", 9304 ImagePullPolicy: "IfNotPresent", 9305 TerminationMessagePolicy: "File", 9306 StartupProbe: &core.Probe{ 9307 ProbeHandler: core.ProbeHandler{ 9308 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 9309 }, 9310 InitialDelaySeconds: -1, 9311 TimeoutSeconds: -1, 9312 PeriodSeconds: -1, 9313 SuccessThreshold: -1, 9314 FailureThreshold: -1, 9315 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 9316 }, 9317 }}, 9318 field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}}, 9319 }, { 9320 "Not supported RestartPolicy: OnFailure", 9321 line(), 9322 []core.Container{{ 9323 Name: "init", 9324 Image: "image", 9325 ImagePullPolicy: "IfNotPresent", 9326 TerminationMessagePolicy: "File", 9327 RestartPolicy: &containerRestartPolicyOnFailure, 9328 }}, 9329 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyOnFailure}}, 9330 }, { 9331 "Not supported RestartPolicy: Never", 9332 line(), 9333 []core.Container{{ 9334 Name: "init", 9335 Image: "image", 9336 ImagePullPolicy: "IfNotPresent", 9337 TerminationMessagePolicy: "File", 9338 RestartPolicy: &containerRestartPolicyNever, 9339 }}, 9340 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyNever}}, 9341 }, { 9342 "Not supported RestartPolicy: invalid", 9343 line(), 9344 []core.Container{{ 9345 Name: "init", 9346 Image: "image", 9347 ImagePullPolicy: "IfNotPresent", 9348 TerminationMessagePolicy: "File", 9349 RestartPolicy: &containerRestartPolicyInvalid, 9350 }}, 9351 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyInvalid}}, 9352 }, { 9353 "Not supported RestartPolicy: empty", 9354 line(), 9355 []core.Container{{ 9356 Name: "init", 9357 Image: "image", 9358 ImagePullPolicy: "IfNotPresent", 9359 TerminationMessagePolicy: "File", 9360 RestartPolicy: &containerRestartPolicyEmpty, 9361 }}, 9362 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyEmpty}}, 9363 }, { 9364 "invalid startup probe in restartable container, successThreshold != 1", 9365 line(), 9366 []core.Container{{ 9367 Name: "restartable-init", 9368 Image: "image", 9369 ImagePullPolicy: "IfNotPresent", 9370 TerminationMessagePolicy: "File", 9371 RestartPolicy: &containerRestartPolicyAlways, 9372 StartupProbe: &core.Probe{ 9373 ProbeHandler: core.ProbeHandler{ 9374 TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt32(80)}, 9375 }, 9376 SuccessThreshold: 2, 9377 }, 9378 }}, 9379 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].startupProbe.successThreshold", BadValue: int32(2)}}, 9380 }, { 9381 "invalid readiness probe, terminationGracePeriodSeconds set.", 9382 line(), 9383 []core.Container{{ 9384 Name: "life-123", 9385 Image: "image", 9386 ImagePullPolicy: "IfNotPresent", 9387 TerminationMessagePolicy: "File", 9388 RestartPolicy: &containerRestartPolicyAlways, 9389 ReadinessProbe: &core.Probe{ 9390 ProbeHandler: core.ProbeHandler{ 9391 TCPSocket: &core.TCPSocketAction{ 9392 Port: intstr.FromInt32(80), 9393 }, 9394 }, 9395 TerminationGracePeriodSeconds: utilpointer.Int64(10), 9396 }, 9397 }}, 9398 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].readinessProbe.terminationGracePeriodSeconds", BadValue: utilpointer.Int64(10)}}, 9399 }, { 9400 "invalid liveness probe, successThreshold != 1", 9401 line(), 9402 []core.Container{{ 9403 Name: "live-123", 9404 Image: "image", 9405 ImagePullPolicy: "IfNotPresent", 9406 TerminationMessagePolicy: "File", 9407 RestartPolicy: &containerRestartPolicyAlways, 9408 LivenessProbe: &core.Probe{ 9409 ProbeHandler: core.ProbeHandler{ 9410 TCPSocket: &core.TCPSocketAction{ 9411 Port: intstr.FromInt32(80), 9412 }, 9413 }, 9414 SuccessThreshold: 2, 9415 }, 9416 }}, 9417 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].livenessProbe.successThreshold", BadValue: int32(2)}}, 9418 }, { 9419 "invalid lifecycle, no exec command.", 9420 line(), 9421 []core.Container{{ 9422 Name: "life-123", 9423 Image: "image", 9424 ImagePullPolicy: "IfNotPresent", 9425 TerminationMessagePolicy: "File", 9426 RestartPolicy: &containerRestartPolicyAlways, 9427 Lifecycle: &core.Lifecycle{ 9428 PreStop: &core.LifecycleHandler{ 9429 Exec: &core.ExecAction{}, 9430 }, 9431 }, 9432 }}, 9433 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "initContainers[0].lifecycle.preStop.exec.command", BadValue: ""}}, 9434 }, { 9435 "invalid lifecycle, no http path.", 9436 line(), 9437 []core.Container{{ 9438 Name: "life-123", 9439 Image: "image", 9440 ImagePullPolicy: "IfNotPresent", 9441 TerminationMessagePolicy: "File", 9442 RestartPolicy: &containerRestartPolicyAlways, 9443 Lifecycle: &core.Lifecycle{ 9444 PreStop: &core.LifecycleHandler{ 9445 HTTPGet: &core.HTTPGetAction{ 9446 Port: intstr.FromInt32(80), 9447 Scheme: "HTTP", 9448 }, 9449 }, 9450 }, 9451 }}, 9452 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "initContainers[0].lifecycle.preStop.httpGet.path", BadValue: ""}}, 9453 }, { 9454 "invalid lifecycle, no http port.", 9455 line(), 9456 []core.Container{{ 9457 Name: "life-123", 9458 Image: "image", 9459 ImagePullPolicy: "IfNotPresent", 9460 TerminationMessagePolicy: "File", 9461 RestartPolicy: &containerRestartPolicyAlways, 9462 Lifecycle: &core.Lifecycle{ 9463 PreStop: &core.LifecycleHandler{ 9464 HTTPGet: &core.HTTPGetAction{ 9465 Path: "/", 9466 Scheme: "HTTP", 9467 }, 9468 }, 9469 }, 9470 }}, 9471 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].lifecycle.preStop.httpGet.port", BadValue: 0}}, 9472 }, { 9473 "invalid lifecycle, no http scheme.", 9474 line(), 9475 []core.Container{{ 9476 Name: "life-123", 9477 Image: "image", 9478 ImagePullPolicy: "IfNotPresent", 9479 TerminationMessagePolicy: "File", 9480 RestartPolicy: &containerRestartPolicyAlways, 9481 Lifecycle: &core.Lifecycle{ 9482 PreStop: &core.LifecycleHandler{ 9483 HTTPGet: &core.HTTPGetAction{ 9484 Path: "/", 9485 Port: intstr.FromInt32(80), 9486 }, 9487 }, 9488 }, 9489 }}, 9490 field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].lifecycle.preStop.httpGet.scheme", BadValue: core.URIScheme("")}}, 9491 }, { 9492 "invalid lifecycle, no tcp socket port.", 9493 line(), 9494 []core.Container{{ 9495 Name: "life-123", 9496 Image: "image", 9497 ImagePullPolicy: "IfNotPresent", 9498 TerminationMessagePolicy: "File", 9499 RestartPolicy: &containerRestartPolicyAlways, 9500 Lifecycle: &core.Lifecycle{ 9501 PreStop: &core.LifecycleHandler{ 9502 TCPSocket: &core.TCPSocketAction{}, 9503 }, 9504 }, 9505 }}, 9506 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].lifecycle.preStop.tcpSocket.port", BadValue: 0}}, 9507 }, { 9508 "invalid lifecycle, zero tcp socket port.", 9509 line(), 9510 []core.Container{{ 9511 Name: "life-123", 9512 Image: "image", 9513 ImagePullPolicy: "IfNotPresent", 9514 TerminationMessagePolicy: "File", 9515 RestartPolicy: &containerRestartPolicyAlways, 9516 Lifecycle: &core.Lifecycle{ 9517 PreStop: &core.LifecycleHandler{ 9518 TCPSocket: &core.TCPSocketAction{ 9519 Port: intstr.FromInt32(0), 9520 }, 9521 }, 9522 }, 9523 }}, 9524 field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].lifecycle.preStop.tcpSocket.port", BadValue: 0}}, 9525 }, { 9526 "invalid lifecycle, no action.", 9527 line(), 9528 []core.Container{{ 9529 Name: "life-123", 9530 Image: "image", 9531 ImagePullPolicy: "IfNotPresent", 9532 TerminationMessagePolicy: "File", 9533 RestartPolicy: &containerRestartPolicyAlways, 9534 Lifecycle: &core.Lifecycle{ 9535 PreStop: &core.LifecycleHandler{}, 9536 }, 9537 }}, 9538 field.ErrorList{{Type: field.ErrorTypeRequired, Field: "initContainers[0].lifecycle.preStop", BadValue: ""}}, 9539 }, 9540 } 9541 9542 for _, tc := range errorCases { 9543 t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) { 9544 errs := validateInitContainers(tc.initContainers, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("initContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) 9545 if len(errs) == 0 { 9546 t.Fatal("expected error but received none") 9547 } 9548 9549 if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "Detail")); diff != "" { 9550 t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff) 9551 t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs)) 9552 } 9553 }) 9554 } 9555 } 9556 9557 func TestValidateRestartPolicy(t *testing.T) { 9558 successCases := []core.RestartPolicy{ 9559 core.RestartPolicyAlways, 9560 core.RestartPolicyOnFailure, 9561 core.RestartPolicyNever, 9562 } 9563 for _, policy := range successCases { 9564 if errs := validateRestartPolicy(&policy, field.NewPath("field")); len(errs) != 0 { 9565 t.Errorf("expected success: %v", errs) 9566 } 9567 } 9568 9569 errorCases := []core.RestartPolicy{"", "newpolicy"} 9570 9571 for k, policy := range errorCases { 9572 if errs := validateRestartPolicy(&policy, field.NewPath("field")); len(errs) == 0 { 9573 t.Errorf("expected failure for %d", k) 9574 } 9575 } 9576 } 9577 9578 func TestValidateDNSPolicy(t *testing.T) { 9579 successCases := []core.DNSPolicy{core.DNSClusterFirst, core.DNSDefault, core.DNSClusterFirstWithHostNet, core.DNSNone} 9580 for _, policy := range successCases { 9581 if errs := validateDNSPolicy(&policy, field.NewPath("field")); len(errs) != 0 { 9582 t.Errorf("expected success: %v", errs) 9583 } 9584 } 9585 9586 errorCases := []core.DNSPolicy{core.DNSPolicy("invalid"), core.DNSPolicy("")} 9587 for _, policy := range errorCases { 9588 if errs := validateDNSPolicy(&policy, field.NewPath("field")); len(errs) == 0 { 9589 t.Errorf("expected failure for %v", policy) 9590 } 9591 } 9592 } 9593 9594 func TestValidatePodDNSConfig(t *testing.T) { 9595 generateTestSearchPathFunc := func(numChars int) string { 9596 res := "" 9597 for i := 0; i < numChars; i++ { 9598 res = res + "a" 9599 } 9600 return res 9601 } 9602 testOptionValue := "2" 9603 testDNSNone := core.DNSNone 9604 testDNSClusterFirst := core.DNSClusterFirst 9605 9606 testCases := []struct { 9607 desc string 9608 dnsConfig *core.PodDNSConfig 9609 dnsPolicy *core.DNSPolicy 9610 opts PodValidationOptions 9611 expectedError bool 9612 }{{ 9613 desc: "valid: empty DNSConfig", 9614 dnsConfig: &core.PodDNSConfig{}, 9615 expectedError: false, 9616 }, { 9617 desc: "valid: 1 option", 9618 dnsConfig: &core.PodDNSConfig{ 9619 Options: []core.PodDNSConfigOption{ 9620 {Name: "ndots", Value: &testOptionValue}, 9621 }, 9622 }, 9623 expectedError: false, 9624 }, { 9625 desc: "valid: 1 nameserver", 9626 dnsConfig: &core.PodDNSConfig{ 9627 Nameservers: []string{"127.0.0.1"}, 9628 }, 9629 expectedError: false, 9630 }, { 9631 desc: "valid: DNSNone with 1 nameserver", 9632 dnsConfig: &core.PodDNSConfig{ 9633 Nameservers: []string{"127.0.0.1"}, 9634 }, 9635 dnsPolicy: &testDNSNone, 9636 expectedError: false, 9637 }, { 9638 desc: "valid: 1 search path", 9639 dnsConfig: &core.PodDNSConfig{ 9640 Searches: []string{"custom"}, 9641 }, 9642 expectedError: false, 9643 }, { 9644 desc: "valid: 1 search path with trailing period", 9645 dnsConfig: &core.PodDNSConfig{ 9646 Searches: []string{"custom."}, 9647 }, 9648 expectedError: false, 9649 }, { 9650 desc: "valid: 3 nameservers and 6 search paths(legacy)", 9651 dnsConfig: &core.PodDNSConfig{ 9652 Nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, 9653 Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local."}, 9654 }, 9655 expectedError: false, 9656 }, { 9657 desc: "valid: 3 nameservers and 32 search paths", 9658 dnsConfig: &core.PodDNSConfig{ 9659 Nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, 9660 Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local.", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32"}, 9661 }, 9662 expectedError: false, 9663 }, { 9664 desc: "valid: 256 characters in search path list(legacy)", 9665 dnsConfig: &core.PodDNSConfig{ 9666 // We can have 256 - (6 - 1) = 251 characters in total for 6 search paths. 9667 Searches: []string{ 9668 generateTestSearchPathFunc(1), 9669 generateTestSearchPathFunc(50), 9670 generateTestSearchPathFunc(50), 9671 generateTestSearchPathFunc(50), 9672 generateTestSearchPathFunc(50), 9673 generateTestSearchPathFunc(50), 9674 }, 9675 }, 9676 expectedError: false, 9677 }, { 9678 desc: "valid: 2048 characters in search path list", 9679 dnsConfig: &core.PodDNSConfig{ 9680 // We can have 2048 - (32 - 1) = 2017 characters in total for 32 search paths. 9681 Searches: []string{ 9682 generateTestSearchPathFunc(64), 9683 generateTestSearchPathFunc(63), 9684 generateTestSearchPathFunc(63), 9685 generateTestSearchPathFunc(63), 9686 generateTestSearchPathFunc(63), 9687 generateTestSearchPathFunc(63), 9688 generateTestSearchPathFunc(63), 9689 generateTestSearchPathFunc(63), 9690 generateTestSearchPathFunc(63), 9691 generateTestSearchPathFunc(63), 9692 generateTestSearchPathFunc(63), 9693 generateTestSearchPathFunc(63), 9694 generateTestSearchPathFunc(63), 9695 generateTestSearchPathFunc(63), 9696 generateTestSearchPathFunc(63), 9697 generateTestSearchPathFunc(63), 9698 generateTestSearchPathFunc(63), 9699 generateTestSearchPathFunc(63), 9700 generateTestSearchPathFunc(63), 9701 generateTestSearchPathFunc(63), 9702 generateTestSearchPathFunc(63), 9703 generateTestSearchPathFunc(63), 9704 generateTestSearchPathFunc(63), 9705 generateTestSearchPathFunc(63), 9706 generateTestSearchPathFunc(63), 9707 generateTestSearchPathFunc(63), 9708 generateTestSearchPathFunc(63), 9709 generateTestSearchPathFunc(63), 9710 generateTestSearchPathFunc(63), 9711 generateTestSearchPathFunc(63), 9712 generateTestSearchPathFunc(63), 9713 generateTestSearchPathFunc(63), 9714 }, 9715 }, 9716 expectedError: false, 9717 }, { 9718 desc: "valid: ipv6 nameserver", 9719 dnsConfig: &core.PodDNSConfig{ 9720 Nameservers: []string{"FE80::0202:B3FF:FE1E:8329"}, 9721 }, 9722 expectedError: false, 9723 }, { 9724 desc: "invalid: 4 nameservers", 9725 dnsConfig: &core.PodDNSConfig{ 9726 Nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8", "1.2.3.4"}, 9727 }, 9728 expectedError: true, 9729 }, { 9730 desc: "valid: 7 search paths", 9731 dnsConfig: &core.PodDNSConfig{ 9732 Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local", "exceeded"}, 9733 }, 9734 }, { 9735 desc: "invalid: 33 search paths", 9736 dnsConfig: &core.PodDNSConfig{ 9737 Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local.", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33"}, 9738 }, 9739 expectedError: true, 9740 }, { 9741 desc: "valid: 257 characters in search path list", 9742 dnsConfig: &core.PodDNSConfig{ 9743 // We can have 256 - (6 - 1) = 251 characters in total for 6 search paths. 9744 Searches: []string{ 9745 generateTestSearchPathFunc(2), 9746 generateTestSearchPathFunc(50), 9747 generateTestSearchPathFunc(50), 9748 generateTestSearchPathFunc(50), 9749 generateTestSearchPathFunc(50), 9750 generateTestSearchPathFunc(50), 9751 }, 9752 }, 9753 }, { 9754 desc: "invalid: 2049 characters in search path list", 9755 dnsConfig: &core.PodDNSConfig{ 9756 // We can have 2048 - (32 - 1) = 2017 characters in total for 32 search paths. 9757 Searches: []string{ 9758 generateTestSearchPathFunc(65), 9759 generateTestSearchPathFunc(63), 9760 generateTestSearchPathFunc(63), 9761 generateTestSearchPathFunc(63), 9762 generateTestSearchPathFunc(63), 9763 generateTestSearchPathFunc(63), 9764 generateTestSearchPathFunc(63), 9765 generateTestSearchPathFunc(63), 9766 generateTestSearchPathFunc(63), 9767 generateTestSearchPathFunc(63), 9768 generateTestSearchPathFunc(63), 9769 generateTestSearchPathFunc(63), 9770 generateTestSearchPathFunc(63), 9771 generateTestSearchPathFunc(63), 9772 generateTestSearchPathFunc(63), 9773 generateTestSearchPathFunc(63), 9774 generateTestSearchPathFunc(63), 9775 generateTestSearchPathFunc(63), 9776 generateTestSearchPathFunc(63), 9777 generateTestSearchPathFunc(63), 9778 generateTestSearchPathFunc(63), 9779 generateTestSearchPathFunc(63), 9780 generateTestSearchPathFunc(63), 9781 generateTestSearchPathFunc(63), 9782 generateTestSearchPathFunc(63), 9783 generateTestSearchPathFunc(63), 9784 generateTestSearchPathFunc(63), 9785 generateTestSearchPathFunc(63), 9786 generateTestSearchPathFunc(63), 9787 generateTestSearchPathFunc(63), 9788 generateTestSearchPathFunc(63), 9789 generateTestSearchPathFunc(63), 9790 }, 9791 }, 9792 expectedError: true, 9793 }, { 9794 desc: "invalid search path", 9795 dnsConfig: &core.PodDNSConfig{ 9796 Searches: []string{"custom?"}, 9797 }, 9798 expectedError: true, 9799 }, { 9800 desc: "invalid nameserver", 9801 dnsConfig: &core.PodDNSConfig{ 9802 Nameservers: []string{"invalid"}, 9803 }, 9804 expectedError: true, 9805 }, { 9806 desc: "invalid empty option name", 9807 dnsConfig: &core.PodDNSConfig{ 9808 Options: []core.PodDNSConfigOption{ 9809 {Value: &testOptionValue}, 9810 }, 9811 }, 9812 expectedError: true, 9813 }, { 9814 desc: "invalid: DNSNone with 0 nameserver", 9815 dnsConfig: &core.PodDNSConfig{ 9816 Searches: []string{"custom"}, 9817 }, 9818 dnsPolicy: &testDNSNone, 9819 expectedError: true, 9820 }, 9821 } 9822 9823 for _, tc := range testCases { 9824 if tc.dnsPolicy == nil { 9825 tc.dnsPolicy = &testDNSClusterFirst 9826 } 9827 9828 errs := validatePodDNSConfig(tc.dnsConfig, tc.dnsPolicy, field.NewPath("dnsConfig"), tc.opts) 9829 if len(errs) != 0 && !tc.expectedError { 9830 t.Errorf("%v: validatePodDNSConfig(%v) = %v, want nil", tc.desc, tc.dnsConfig, errs) 9831 } else if len(errs) == 0 && tc.expectedError { 9832 t.Errorf("%v: validatePodDNSConfig(%v) = nil, want error", tc.desc, tc.dnsConfig) 9833 } 9834 } 9835 } 9836 9837 func TestValidatePodReadinessGates(t *testing.T) { 9838 successCases := []struct { 9839 desc string 9840 readinessGates []core.PodReadinessGate 9841 }{{ 9842 "no gate", 9843 []core.PodReadinessGate{}, 9844 }, { 9845 "one readiness gate", 9846 []core.PodReadinessGate{{ 9847 ConditionType: core.PodConditionType("example.com/condition"), 9848 }}, 9849 }, { 9850 "two readiness gates", 9851 []core.PodReadinessGate{{ 9852 ConditionType: core.PodConditionType("example.com/condition1"), 9853 }, { 9854 ConditionType: core.PodConditionType("example.com/condition2"), 9855 }}, 9856 }, 9857 } 9858 for _, tc := range successCases { 9859 if errs := validateReadinessGates(tc.readinessGates, field.NewPath("field")); len(errs) != 0 { 9860 t.Errorf("expect tc %q to success: %v", tc.desc, errs) 9861 } 9862 } 9863 9864 errorCases := []struct { 9865 desc string 9866 readinessGates []core.PodReadinessGate 9867 }{{ 9868 "invalid condition type", 9869 []core.PodReadinessGate{{ 9870 ConditionType: core.PodConditionType("invalid/condition/type"), 9871 }}, 9872 }, 9873 } 9874 for _, tc := range errorCases { 9875 if errs := validateReadinessGates(tc.readinessGates, field.NewPath("field")); len(errs) == 0 { 9876 t.Errorf("expected tc %q to fail", tc.desc) 9877 } 9878 } 9879 } 9880 9881 func TestValidatePodConditions(t *testing.T) { 9882 successCases := []struct { 9883 desc string 9884 podConditions []core.PodCondition 9885 }{{ 9886 "no condition", 9887 []core.PodCondition{}, 9888 }, { 9889 "one system condition", 9890 []core.PodCondition{{ 9891 Type: core.PodReady, 9892 Status: core.ConditionTrue, 9893 }}, 9894 }, { 9895 "one system condition and one custom condition", 9896 []core.PodCondition{{ 9897 Type: core.PodReady, 9898 Status: core.ConditionTrue, 9899 }, { 9900 Type: core.PodConditionType("example.com/condition"), 9901 Status: core.ConditionFalse, 9902 }}, 9903 }, { 9904 "two custom condition", 9905 []core.PodCondition{{ 9906 Type: core.PodConditionType("foobar"), 9907 Status: core.ConditionTrue, 9908 }, { 9909 Type: core.PodConditionType("example.com/condition"), 9910 Status: core.ConditionFalse, 9911 }}, 9912 }, 9913 } 9914 9915 for _, tc := range successCases { 9916 if errs := validatePodConditions(tc.podConditions, field.NewPath("field")); len(errs) != 0 { 9917 t.Errorf("expected tc %q to success, but got: %v", tc.desc, errs) 9918 } 9919 } 9920 9921 errorCases := []struct { 9922 desc string 9923 podConditions []core.PodCondition 9924 }{{ 9925 "one system condition and a invalid custom condition", 9926 []core.PodCondition{{ 9927 Type: core.PodReady, 9928 Status: core.ConditionStatus("True"), 9929 }, { 9930 Type: core.PodConditionType("invalid/custom/condition"), 9931 Status: core.ConditionStatus("True"), 9932 }}, 9933 }, 9934 } 9935 for _, tc := range errorCases { 9936 if errs := validatePodConditions(tc.podConditions, field.NewPath("field")); len(errs) == 0 { 9937 t.Errorf("expected tc %q to fail", tc.desc) 9938 } 9939 } 9940 } 9941 9942 func TestValidatePodSpec(t *testing.T) { 9943 activeDeadlineSeconds := int64(30) 9944 activeDeadlineSecondsMax := int64(math.MaxInt32) 9945 9946 minUserID := int64(0) 9947 maxUserID := int64(2147483647) 9948 minGroupID := int64(0) 9949 maxGroupID := int64(2147483647) 9950 goodfsGroupChangePolicy := core.FSGroupChangeAlways 9951 badfsGroupChangePolicy1 := core.PodFSGroupChangePolicy("invalid") 9952 badfsGroupChangePolicy2 := core.PodFSGroupChangePolicy("") 9953 9954 successCases := map[string]core.PodSpec{ 9955 "populate basic fields, leave defaults for most": { 9956 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 9957 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 9958 RestartPolicy: core.RestartPolicyAlways, 9959 DNSPolicy: core.DNSClusterFirst, 9960 }, 9961 "populate all fields": { 9962 Volumes: []core.Volume{ 9963 {Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 9964 }, 9965 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 9966 InitContainers: []core.Container{{Name: "ictr", Image: "iimage", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 9967 RestartPolicy: core.RestartPolicyAlways, 9968 NodeSelector: map[string]string{ 9969 "key": "value", 9970 }, 9971 NodeName: "foobar", 9972 DNSPolicy: core.DNSClusterFirst, 9973 ActiveDeadlineSeconds: &activeDeadlineSeconds, 9974 ServiceAccountName: "acct", 9975 }, 9976 "populate all fields with larger active deadline": { 9977 Volumes: []core.Volume{ 9978 {Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 9979 }, 9980 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 9981 InitContainers: []core.Container{{Name: "ictr", Image: "iimage", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 9982 RestartPolicy: core.RestartPolicyAlways, 9983 NodeSelector: map[string]string{ 9984 "key": "value", 9985 }, 9986 NodeName: "foobar", 9987 DNSPolicy: core.DNSClusterFirst, 9988 ActiveDeadlineSeconds: &activeDeadlineSecondsMax, 9989 ServiceAccountName: "acct", 9990 }, 9991 "populate HostNetwork": { 9992 Containers: []core.Container{ 9993 {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 9994 Ports: []core.ContainerPort{ 9995 {HostPort: 8080, ContainerPort: 8080, Protocol: "TCP"}}, 9996 }, 9997 }, 9998 SecurityContext: &core.PodSecurityContext{ 9999 HostNetwork: true, 10000 }, 10001 RestartPolicy: core.RestartPolicyAlways, 10002 DNSPolicy: core.DNSClusterFirst, 10003 }, 10004 "populate RunAsUser SupplementalGroups FSGroup with minID 0": { 10005 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10006 SecurityContext: &core.PodSecurityContext{ 10007 SupplementalGroups: []int64{minGroupID}, 10008 RunAsUser: &minUserID, 10009 FSGroup: &minGroupID, 10010 }, 10011 RestartPolicy: core.RestartPolicyAlways, 10012 DNSPolicy: core.DNSClusterFirst, 10013 }, 10014 "populate RunAsUser SupplementalGroups FSGroup with maxID 2147483647": { 10015 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10016 SecurityContext: &core.PodSecurityContext{ 10017 SupplementalGroups: []int64{maxGroupID}, 10018 RunAsUser: &maxUserID, 10019 FSGroup: &maxGroupID, 10020 }, 10021 RestartPolicy: core.RestartPolicyAlways, 10022 DNSPolicy: core.DNSClusterFirst, 10023 }, 10024 "populate HostIPC": { 10025 SecurityContext: &core.PodSecurityContext{ 10026 HostIPC: true, 10027 }, 10028 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10029 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10030 RestartPolicy: core.RestartPolicyAlways, 10031 DNSPolicy: core.DNSClusterFirst, 10032 }, 10033 "populate HostPID": { 10034 SecurityContext: &core.PodSecurityContext{ 10035 HostPID: true, 10036 }, 10037 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10038 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10039 RestartPolicy: core.RestartPolicyAlways, 10040 DNSPolicy: core.DNSClusterFirst, 10041 }, 10042 "populate Affinity": { 10043 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10044 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10045 RestartPolicy: core.RestartPolicyAlways, 10046 DNSPolicy: core.DNSClusterFirst, 10047 }, 10048 "populate HostAliases": { 10049 HostAliases: []core.HostAlias{{IP: "12.34.56.78", Hostnames: []string{"host1", "host2"}}}, 10050 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10051 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10052 RestartPolicy: core.RestartPolicyAlways, 10053 DNSPolicy: core.DNSClusterFirst, 10054 }, 10055 "populate HostAliases with `foo.bar` hostnames": { 10056 HostAliases: []core.HostAlias{{IP: "12.34.56.78", Hostnames: []string{"host1.foo", "host2.bar"}}}, 10057 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10058 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10059 RestartPolicy: core.RestartPolicyAlways, 10060 DNSPolicy: core.DNSClusterFirst, 10061 }, 10062 "populate HostAliases with HostNetwork": { 10063 HostAliases: []core.HostAlias{{IP: "12.34.56.78", Hostnames: []string{"host1.foo", "host2.bar"}}}, 10064 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10065 SecurityContext: &core.PodSecurityContext{ 10066 HostNetwork: true, 10067 }, 10068 RestartPolicy: core.RestartPolicyAlways, 10069 DNSPolicy: core.DNSClusterFirst, 10070 }, 10071 "populate PriorityClassName": { 10072 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10073 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10074 RestartPolicy: core.RestartPolicyAlways, 10075 DNSPolicy: core.DNSClusterFirst, 10076 PriorityClassName: "valid-name", 10077 }, 10078 "populate ShareProcessNamespace": { 10079 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10080 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10081 RestartPolicy: core.RestartPolicyAlways, 10082 DNSPolicy: core.DNSClusterFirst, 10083 SecurityContext: &core.PodSecurityContext{ 10084 ShareProcessNamespace: &[]bool{true}[0], 10085 }, 10086 }, 10087 "populate RuntimeClassName": { 10088 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10089 RestartPolicy: core.RestartPolicyAlways, 10090 DNSPolicy: core.DNSClusterFirst, 10091 RuntimeClassName: utilpointer.String("valid-sandbox"), 10092 }, 10093 "populate Overhead": { 10094 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10095 RestartPolicy: core.RestartPolicyAlways, 10096 DNSPolicy: core.DNSClusterFirst, 10097 RuntimeClassName: utilpointer.String("valid-sandbox"), 10098 Overhead: core.ResourceList{}, 10099 }, 10100 "populate DNSPolicy": { 10101 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10102 SecurityContext: &core.PodSecurityContext{ 10103 FSGroupChangePolicy: &goodfsGroupChangePolicy, 10104 }, 10105 RestartPolicy: core.RestartPolicyAlways, 10106 DNSPolicy: core.DNSClusterFirst, 10107 }, 10108 } 10109 for k, v := range successCases { 10110 t.Run(k, func(t *testing.T) { 10111 opts := PodValidationOptions{ 10112 ResourceIsPod: true, 10113 } 10114 if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), opts); len(errs) != 0 { 10115 t.Errorf("expected success: %v", errs) 10116 } 10117 }) 10118 } 10119 10120 activeDeadlineSeconds = int64(0) 10121 activeDeadlineSecondsTooLarge := int64(math.MaxInt32 + 1) 10122 10123 minUserID = int64(-1) 10124 maxUserID = int64(2147483648) 10125 minGroupID = int64(-1) 10126 maxGroupID = int64(2147483648) 10127 10128 failureCases := map[string]core.PodSpec{ 10129 "bad volume": { 10130 Volumes: []core.Volume{{}}, 10131 RestartPolicy: core.RestartPolicyAlways, 10132 DNSPolicy: core.DNSClusterFirst, 10133 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10134 }, 10135 "no containers": { 10136 RestartPolicy: core.RestartPolicyAlways, 10137 DNSPolicy: core.DNSClusterFirst, 10138 }, 10139 "bad container": { 10140 Containers: []core.Container{{}}, 10141 RestartPolicy: core.RestartPolicyAlways, 10142 DNSPolicy: core.DNSClusterFirst, 10143 }, 10144 "bad init container": { 10145 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10146 InitContainers: []core.Container{{}}, 10147 RestartPolicy: core.RestartPolicyAlways, 10148 DNSPolicy: core.DNSClusterFirst, 10149 }, 10150 "bad DNS policy": { 10151 DNSPolicy: core.DNSPolicy("invalid"), 10152 RestartPolicy: core.RestartPolicyAlways, 10153 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10154 }, 10155 "bad service account name": { 10156 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10157 RestartPolicy: core.RestartPolicyAlways, 10158 DNSPolicy: core.DNSClusterFirst, 10159 ServiceAccountName: "invalidName", 10160 }, 10161 "bad restart policy": { 10162 RestartPolicy: "UnknowPolicy", 10163 DNSPolicy: core.DNSClusterFirst, 10164 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10165 }, 10166 "with hostNetwork hostPort unspecified": { 10167 Containers: []core.Container{ 10168 {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []core.ContainerPort{ 10169 {HostPort: 0, ContainerPort: 2600, Protocol: "TCP"}}, 10170 }, 10171 }, 10172 SecurityContext: &core.PodSecurityContext{ 10173 HostNetwork: true, 10174 }, 10175 RestartPolicy: core.RestartPolicyAlways, 10176 DNSPolicy: core.DNSClusterFirst, 10177 }, 10178 "with hostNetwork hostPort not equal to containerPort": { 10179 Containers: []core.Container{ 10180 {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []core.ContainerPort{ 10181 {HostPort: 8080, ContainerPort: 2600, Protocol: "TCP"}}, 10182 }, 10183 }, 10184 SecurityContext: &core.PodSecurityContext{ 10185 HostNetwork: true, 10186 }, 10187 RestartPolicy: core.RestartPolicyAlways, 10188 DNSPolicy: core.DNSClusterFirst, 10189 }, 10190 "with hostAliases with invalid IP": { 10191 SecurityContext: &core.PodSecurityContext{ 10192 HostNetwork: false, 10193 }, 10194 HostAliases: []core.HostAlias{{IP: "999.999.999.999", Hostnames: []string{"host1", "host2"}}}, 10195 }, 10196 "with hostAliases with invalid hostname": { 10197 SecurityContext: &core.PodSecurityContext{ 10198 HostNetwork: false, 10199 }, 10200 HostAliases: []core.HostAlias{{IP: "12.34.56.78", Hostnames: []string{"@#$^#@#$"}}}, 10201 }, 10202 "bad supplementalGroups large than math.MaxInt32": { 10203 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10204 SecurityContext: &core.PodSecurityContext{ 10205 SupplementalGroups: []int64{maxGroupID, 1234}, 10206 }, 10207 RestartPolicy: core.RestartPolicyAlways, 10208 DNSPolicy: core.DNSClusterFirst, 10209 }, 10210 "bad supplementalGroups less than 0": { 10211 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10212 SecurityContext: &core.PodSecurityContext{ 10213 SupplementalGroups: []int64{minGroupID, 1234}, 10214 }, 10215 RestartPolicy: core.RestartPolicyAlways, 10216 DNSPolicy: core.DNSClusterFirst, 10217 }, 10218 "bad runAsUser large than math.MaxInt32": { 10219 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10220 SecurityContext: &core.PodSecurityContext{ 10221 RunAsUser: &maxUserID, 10222 }, 10223 RestartPolicy: core.RestartPolicyAlways, 10224 DNSPolicy: core.DNSClusterFirst, 10225 }, 10226 "bad runAsUser less than 0": { 10227 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10228 SecurityContext: &core.PodSecurityContext{ 10229 RunAsUser: &minUserID, 10230 }, 10231 RestartPolicy: core.RestartPolicyAlways, 10232 DNSPolicy: core.DNSClusterFirst, 10233 }, 10234 "bad fsGroup large than math.MaxInt32": { 10235 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10236 SecurityContext: &core.PodSecurityContext{ 10237 FSGroup: &maxGroupID, 10238 }, 10239 RestartPolicy: core.RestartPolicyAlways, 10240 DNSPolicy: core.DNSClusterFirst, 10241 }, 10242 "bad fsGroup less than 0": { 10243 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10244 SecurityContext: &core.PodSecurityContext{ 10245 FSGroup: &minGroupID, 10246 }, 10247 RestartPolicy: core.RestartPolicyAlways, 10248 DNSPolicy: core.DNSClusterFirst, 10249 }, 10250 "bad-active-deadline-seconds": { 10251 Volumes: []core.Volume{ 10252 {Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 10253 }, 10254 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10255 RestartPolicy: core.RestartPolicyAlways, 10256 NodeSelector: map[string]string{ 10257 "key": "value", 10258 }, 10259 NodeName: "foobar", 10260 DNSPolicy: core.DNSClusterFirst, 10261 ActiveDeadlineSeconds: &activeDeadlineSeconds, 10262 }, 10263 "active-deadline-seconds-too-large": { 10264 Volumes: []core.Volume{ 10265 {Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 10266 }, 10267 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10268 RestartPolicy: core.RestartPolicyAlways, 10269 NodeSelector: map[string]string{ 10270 "key": "value", 10271 }, 10272 NodeName: "foobar", 10273 DNSPolicy: core.DNSClusterFirst, 10274 ActiveDeadlineSeconds: &activeDeadlineSecondsTooLarge, 10275 }, 10276 "bad nodeName": { 10277 NodeName: "node name", 10278 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10279 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10280 RestartPolicy: core.RestartPolicyAlways, 10281 DNSPolicy: core.DNSClusterFirst, 10282 }, 10283 "bad PriorityClassName": { 10284 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10285 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10286 RestartPolicy: core.RestartPolicyAlways, 10287 DNSPolicy: core.DNSClusterFirst, 10288 PriorityClassName: "InvalidName", 10289 }, 10290 "ShareProcessNamespace and HostPID both set": { 10291 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10292 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10293 RestartPolicy: core.RestartPolicyAlways, 10294 DNSPolicy: core.DNSClusterFirst, 10295 SecurityContext: &core.PodSecurityContext{ 10296 HostPID: true, 10297 ShareProcessNamespace: &[]bool{true}[0], 10298 }, 10299 }, 10300 "bad RuntimeClassName": { 10301 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10302 RestartPolicy: core.RestartPolicyAlways, 10303 DNSPolicy: core.DNSClusterFirst, 10304 RuntimeClassName: utilpointer.String("invalid/sandbox"), 10305 }, 10306 "bad empty fsGroupchangepolicy": { 10307 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10308 SecurityContext: &core.PodSecurityContext{ 10309 FSGroupChangePolicy: &badfsGroupChangePolicy2, 10310 }, 10311 RestartPolicy: core.RestartPolicyAlways, 10312 DNSPolicy: core.DNSClusterFirst, 10313 }, 10314 "bad invalid fsgroupchangepolicy": { 10315 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10316 SecurityContext: &core.PodSecurityContext{ 10317 FSGroupChangePolicy: &badfsGroupChangePolicy1, 10318 }, 10319 RestartPolicy: core.RestartPolicyAlways, 10320 DNSPolicy: core.DNSClusterFirst, 10321 }, 10322 "disallowed resources resize policy for init containers": { 10323 InitContainers: []core.Container{{ 10324 Name: "initctr", 10325 Image: "initimage", 10326 ResizePolicy: []core.ContainerResizePolicy{ 10327 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 10328 }, 10329 ImagePullPolicy: "IfNotPresent", 10330 TerminationMessagePolicy: "File", 10331 }}, 10332 Containers: []core.Container{{ 10333 Name: "ctr", 10334 Image: "image", 10335 ResizePolicy: []core.ContainerResizePolicy{ 10336 {ResourceName: "cpu", RestartPolicy: "NotRequired"}, 10337 }, 10338 ImagePullPolicy: "IfNotPresent", 10339 TerminationMessagePolicy: "File", 10340 }}, 10341 RestartPolicy: core.RestartPolicyAlways, 10342 DNSPolicy: core.DNSClusterFirst, 10343 }, 10344 } 10345 for k, v := range failureCases { 10346 opts := PodValidationOptions{ 10347 ResourceIsPod: true, 10348 } 10349 if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), opts); len(errs) == 0 { 10350 t.Errorf("expected failure for %q", k) 10351 } 10352 } 10353 } 10354 10355 func extendPodSpecwithTolerations(in core.PodSpec, tolerations []core.Toleration) core.PodSpec { 10356 var out core.PodSpec 10357 out.Containers = in.Containers 10358 out.RestartPolicy = in.RestartPolicy 10359 out.DNSPolicy = in.DNSPolicy 10360 out.Tolerations = tolerations 10361 return out 10362 } 10363 10364 func TestValidatePod(t *testing.T) { 10365 validPodSpec := func(affinity *core.Affinity) core.PodSpec { 10366 spec := core.PodSpec{ 10367 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10368 RestartPolicy: core.RestartPolicyAlways, 10369 DNSPolicy: core.DNSClusterFirst, 10370 } 10371 if affinity != nil { 10372 spec.Affinity = affinity 10373 } 10374 return spec 10375 } 10376 validPVCSpec := core.PersistentVolumeClaimSpec{ 10377 AccessModes: []core.PersistentVolumeAccessMode{ 10378 core.ReadWriteOnce, 10379 }, 10380 Resources: core.VolumeResourceRequirements{ 10381 Requests: core.ResourceList{ 10382 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 10383 }, 10384 }, 10385 } 10386 validPVCTemplate := core.PersistentVolumeClaimTemplate{ 10387 Spec: validPVCSpec, 10388 } 10389 longPodName := strings.Repeat("a", 200) 10390 longVolName := strings.Repeat("b", 60) 10391 10392 successCases := map[string]core.Pod{ 10393 "basic fields": { 10394 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 10395 Spec: core.PodSpec{ 10396 Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, 10397 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10398 RestartPolicy: core.RestartPolicyAlways, 10399 DNSPolicy: core.DNSClusterFirst, 10400 }, 10401 }, 10402 "just about everything": { 10403 ObjectMeta: metav1.ObjectMeta{Name: "abc.123.do-re-mi", Namespace: "ns"}, 10404 Spec: core.PodSpec{ 10405 Volumes: []core.Volume{ 10406 {Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}, 10407 }, 10408 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10409 RestartPolicy: core.RestartPolicyAlways, 10410 DNSPolicy: core.DNSClusterFirst, 10411 NodeSelector: map[string]string{ 10412 "key": "value", 10413 }, 10414 NodeName: "foobar", 10415 }, 10416 }, 10417 "serialized node affinity requirements": { 10418 ObjectMeta: metav1.ObjectMeta{ 10419 Name: "123", 10420 Namespace: "ns", 10421 }, 10422 Spec: validPodSpec( 10423 // TODO: Uncomment and move this block and move inside NodeAffinity once 10424 // RequiredDuringSchedulingRequiredDuringExecution is implemented 10425 // RequiredDuringSchedulingRequiredDuringExecution: &core.NodeSelector{ 10426 // NodeSelectorTerms: []core.NodeSelectorTerm{ 10427 // { 10428 // MatchExpressions: []core.NodeSelectorRequirement{ 10429 // { 10430 // Key: "key1", 10431 // Operator: core.NodeSelectorOpExists 10432 // }, 10433 // }, 10434 // }, 10435 // }, 10436 // }, 10437 &core.Affinity{ 10438 NodeAffinity: &core.NodeAffinity{ 10439 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 10440 NodeSelectorTerms: []core.NodeSelectorTerm{{ 10441 MatchExpressions: []core.NodeSelectorRequirement{{ 10442 Key: "key2", 10443 Operator: core.NodeSelectorOpIn, 10444 Values: []string{"value1", "value2"}, 10445 }}, 10446 MatchFields: []core.NodeSelectorRequirement{{ 10447 Key: "metadata.name", 10448 Operator: core.NodeSelectorOpIn, 10449 Values: []string{"host1"}, 10450 }}, 10451 }}, 10452 }, 10453 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 10454 Weight: 10, 10455 Preference: core.NodeSelectorTerm{ 10456 MatchExpressions: []core.NodeSelectorRequirement{{ 10457 Key: "foo", 10458 Operator: core.NodeSelectorOpIn, 10459 Values: []string{"bar"}, 10460 }}, 10461 }, 10462 }}, 10463 }, 10464 }, 10465 ), 10466 }, 10467 "serialized node affinity requirements, II": { 10468 ObjectMeta: metav1.ObjectMeta{ 10469 Name: "123", 10470 Namespace: "ns", 10471 }, 10472 Spec: validPodSpec( 10473 // TODO: Uncomment and move this block and move inside NodeAffinity once 10474 // RequiredDuringSchedulingRequiredDuringExecution is implemented 10475 // RequiredDuringSchedulingRequiredDuringExecution: &core.NodeSelector{ 10476 // NodeSelectorTerms: []core.NodeSelectorTerm{ 10477 // { 10478 // MatchExpressions: []core.NodeSelectorRequirement{ 10479 // { 10480 // Key: "key1", 10481 // Operator: core.NodeSelectorOpExists 10482 // }, 10483 // }, 10484 // }, 10485 // }, 10486 // }, 10487 &core.Affinity{ 10488 NodeAffinity: &core.NodeAffinity{ 10489 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 10490 NodeSelectorTerms: []core.NodeSelectorTerm{{ 10491 MatchExpressions: []core.NodeSelectorRequirement{}, 10492 }}, 10493 }, 10494 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 10495 Weight: 10, 10496 Preference: core.NodeSelectorTerm{ 10497 MatchExpressions: []core.NodeSelectorRequirement{}, 10498 }, 10499 }}, 10500 }, 10501 }, 10502 ), 10503 }, 10504 "serialized pod affinity in affinity requirements in annotations": { 10505 ObjectMeta: metav1.ObjectMeta{ 10506 Name: "123", 10507 Namespace: "ns", 10508 // TODO: Uncomment and move this block into Annotations map once 10509 // RequiredDuringSchedulingRequiredDuringExecution is implemented 10510 // "requiredDuringSchedulingRequiredDuringExecution": [{ 10511 // "labelSelector": { 10512 // "matchExpressions": [{ 10513 // "key": "key2", 10514 // "operator": "In", 10515 // "values": ["value1", "value2"] 10516 // }] 10517 // }, 10518 // "namespaces":["ns"], 10519 // "topologyKey": "zone" 10520 // }] 10521 }, 10522 Spec: validPodSpec(&core.Affinity{ 10523 PodAffinity: &core.PodAffinity{ 10524 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 10525 LabelSelector: &metav1.LabelSelector{ 10526 MatchExpressions: []metav1.LabelSelectorRequirement{{ 10527 Key: "key2", 10528 Operator: metav1.LabelSelectorOpIn, 10529 Values: []string{"value1", "value2"}, 10530 }}, 10531 }, 10532 TopologyKey: "zone", 10533 Namespaces: []string{"ns"}, 10534 NamespaceSelector: &metav1.LabelSelector{ 10535 MatchExpressions: []metav1.LabelSelectorRequirement{{ 10536 Key: "key", 10537 Operator: metav1.LabelSelectorOpIn, 10538 Values: []string{"value1", "value2"}, 10539 }}, 10540 }, 10541 }}, 10542 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 10543 Weight: 10, 10544 PodAffinityTerm: core.PodAffinityTerm{ 10545 LabelSelector: &metav1.LabelSelector{ 10546 MatchExpressions: []metav1.LabelSelectorRequirement{{ 10547 Key: "key2", 10548 Operator: metav1.LabelSelectorOpNotIn, 10549 Values: []string{"value1", "value2"}, 10550 }}, 10551 }, 10552 Namespaces: []string{"ns"}, 10553 TopologyKey: "region", 10554 }, 10555 }}, 10556 }, 10557 }), 10558 }, 10559 "serialized pod anti affinity with different Label Operators in affinity requirements in annotations": { 10560 ObjectMeta: metav1.ObjectMeta{ 10561 Name: "123", 10562 Namespace: "ns", 10563 // TODO: Uncomment and move this block into Annotations map once 10564 // RequiredDuringSchedulingRequiredDuringExecution is implemented 10565 // "requiredDuringSchedulingRequiredDuringExecution": [{ 10566 // "labelSelector": { 10567 // "matchExpressions": [{ 10568 // "key": "key2", 10569 // "operator": "In", 10570 // "values": ["value1", "value2"] 10571 // }] 10572 // }, 10573 // "namespaces":["ns"], 10574 // "topologyKey": "zone" 10575 // }] 10576 }, 10577 Spec: validPodSpec(&core.Affinity{ 10578 PodAntiAffinity: &core.PodAntiAffinity{ 10579 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 10580 LabelSelector: &metav1.LabelSelector{ 10581 MatchExpressions: []metav1.LabelSelectorRequirement{{ 10582 Key: "key2", 10583 Operator: metav1.LabelSelectorOpExists, 10584 }}, 10585 }, 10586 TopologyKey: "zone", 10587 Namespaces: []string{"ns"}, 10588 }}, 10589 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 10590 Weight: 10, 10591 PodAffinityTerm: core.PodAffinityTerm{ 10592 LabelSelector: &metav1.LabelSelector{ 10593 MatchExpressions: []metav1.LabelSelectorRequirement{{ 10594 Key: "key2", 10595 Operator: metav1.LabelSelectorOpDoesNotExist, 10596 }}, 10597 }, 10598 Namespaces: []string{"ns"}, 10599 TopologyKey: "region", 10600 }, 10601 }}, 10602 }, 10603 }), 10604 }, 10605 "populate forgiveness tolerations with exists operator in annotations.": { 10606 ObjectMeta: metav1.ObjectMeta{ 10607 Name: "123", 10608 Namespace: "ns", 10609 }, 10610 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "Exists", Value: "", Effect: "NoExecute", TolerationSeconds: &[]int64{60}[0]}}), 10611 }, 10612 "populate forgiveness tolerations with equal operator in annotations.": { 10613 ObjectMeta: metav1.ObjectMeta{ 10614 Name: "123", 10615 Namespace: "ns", 10616 }, 10617 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoExecute", TolerationSeconds: &[]int64{60}[0]}}), 10618 }, 10619 "populate tolerations equal operator in annotations.": { 10620 ObjectMeta: metav1.ObjectMeta{ 10621 Name: "123", 10622 Namespace: "ns", 10623 }, 10624 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}), 10625 }, 10626 "populate tolerations exists operator in annotations.": { 10627 ObjectMeta: metav1.ObjectMeta{ 10628 Name: "123", 10629 Namespace: "ns", 10630 }, 10631 Spec: validPodSpec(nil), 10632 }, 10633 "empty key with Exists operator is OK for toleration, empty toleration key means match all taint keys.": { 10634 ObjectMeta: metav1.ObjectMeta{ 10635 Name: "123", 10636 Namespace: "ns", 10637 }, 10638 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Operator: "Exists", Effect: "NoSchedule"}}), 10639 }, 10640 "empty operator is OK for toleration, defaults to Equal.": { 10641 ObjectMeta: metav1.ObjectMeta{ 10642 Name: "123", 10643 Namespace: "ns", 10644 }, 10645 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Value: "bar", Effect: "NoSchedule"}}), 10646 }, 10647 "empty effect is OK for toleration, empty toleration effect means match all taint effects.": { 10648 ObjectMeta: metav1.ObjectMeta{ 10649 Name: "123", 10650 Namespace: "ns", 10651 }, 10652 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "Equal", Value: "bar"}}), 10653 }, 10654 "negative tolerationSeconds is OK for toleration.": { 10655 ObjectMeta: metav1.ObjectMeta{ 10656 Name: "pod-forgiveness-invalid", 10657 Namespace: "ns", 10658 }, 10659 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "node.kubernetes.io/not-ready", Operator: "Exists", Effect: "NoExecute", TolerationSeconds: &[]int64{-2}[0]}}), 10660 }, 10661 "runtime default seccomp profile": { 10662 ObjectMeta: metav1.ObjectMeta{ 10663 Name: "123", 10664 Namespace: "ns", 10665 Annotations: map[string]string{ 10666 core.SeccompPodAnnotationKey: core.SeccompProfileRuntimeDefault, 10667 }, 10668 }, 10669 Spec: validPodSpec(nil), 10670 }, 10671 "docker default seccomp profile": { 10672 ObjectMeta: metav1.ObjectMeta{ 10673 Name: "123", 10674 Namespace: "ns", 10675 Annotations: map[string]string{ 10676 core.SeccompPodAnnotationKey: core.DeprecatedSeccompProfileDockerDefault, 10677 }, 10678 }, 10679 Spec: validPodSpec(nil), 10680 }, 10681 "unconfined seccomp profile": { 10682 ObjectMeta: metav1.ObjectMeta{ 10683 Name: "123", 10684 Namespace: "ns", 10685 Annotations: map[string]string{ 10686 core.SeccompPodAnnotationKey: "unconfined", 10687 }, 10688 }, 10689 Spec: validPodSpec(nil), 10690 }, 10691 "localhost seccomp profile": { 10692 ObjectMeta: metav1.ObjectMeta{ 10693 Name: "123", 10694 Namespace: "ns", 10695 Annotations: map[string]string{ 10696 core.SeccompPodAnnotationKey: "localhost/foo", 10697 }, 10698 }, 10699 Spec: validPodSpec(nil), 10700 }, 10701 "localhost seccomp profile for a container": { 10702 ObjectMeta: metav1.ObjectMeta{ 10703 Name: "123", 10704 Namespace: "ns", 10705 Annotations: map[string]string{ 10706 core.SeccompContainerAnnotationKeyPrefix + "foo": "localhost/foo", 10707 }, 10708 }, 10709 Spec: validPodSpec(nil), 10710 }, 10711 "runtime default seccomp profile for a pod": { 10712 ObjectMeta: metav1.ObjectMeta{ 10713 Name: "123", 10714 Namespace: "ns", 10715 }, 10716 Spec: core.PodSpec{ 10717 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10718 RestartPolicy: core.RestartPolicyAlways, 10719 DNSPolicy: core.DNSDefault, 10720 SecurityContext: &core.PodSecurityContext{ 10721 SeccompProfile: &core.SeccompProfile{ 10722 Type: core.SeccompProfileTypeRuntimeDefault, 10723 }, 10724 }, 10725 }, 10726 }, 10727 "runtime default seccomp profile for a container": { 10728 ObjectMeta: metav1.ObjectMeta{ 10729 Name: "123", 10730 Namespace: "ns", 10731 }, 10732 Spec: core.PodSpec{ 10733 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10734 SecurityContext: &core.SecurityContext{ 10735 SeccompProfile: &core.SeccompProfile{ 10736 Type: core.SeccompProfileTypeRuntimeDefault, 10737 }, 10738 }, 10739 }}, 10740 RestartPolicy: core.RestartPolicyAlways, 10741 DNSPolicy: core.DNSDefault, 10742 }, 10743 }, 10744 "unconfined seccomp profile for a pod": { 10745 ObjectMeta: metav1.ObjectMeta{ 10746 Name: "123", 10747 Namespace: "ns", 10748 }, 10749 Spec: core.PodSpec{ 10750 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10751 RestartPolicy: core.RestartPolicyAlways, 10752 DNSPolicy: core.DNSDefault, 10753 SecurityContext: &core.PodSecurityContext{ 10754 SeccompProfile: &core.SeccompProfile{ 10755 Type: core.SeccompProfileTypeUnconfined, 10756 }, 10757 }, 10758 }, 10759 }, 10760 "unconfined seccomp profile for a container": { 10761 ObjectMeta: metav1.ObjectMeta{ 10762 Name: "123", 10763 Namespace: "ns", 10764 }, 10765 Spec: core.PodSpec{ 10766 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10767 SecurityContext: &core.SecurityContext{ 10768 SeccompProfile: &core.SeccompProfile{ 10769 Type: core.SeccompProfileTypeUnconfined, 10770 }, 10771 }, 10772 }}, 10773 RestartPolicy: core.RestartPolicyAlways, 10774 DNSPolicy: core.DNSDefault, 10775 }, 10776 }, 10777 "localhost seccomp profile for a pod": { 10778 ObjectMeta: metav1.ObjectMeta{ 10779 Name: "123", 10780 Namespace: "ns", 10781 }, 10782 Spec: core.PodSpec{ 10783 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10784 RestartPolicy: core.RestartPolicyAlways, 10785 DNSPolicy: core.DNSDefault, 10786 SecurityContext: &core.PodSecurityContext{ 10787 SeccompProfile: &core.SeccompProfile{ 10788 Type: core.SeccompProfileTypeLocalhost, 10789 LocalhostProfile: utilpointer.String("filename.json"), 10790 }, 10791 }, 10792 }, 10793 }, 10794 "localhost seccomp profile for a container, II": { 10795 ObjectMeta: metav1.ObjectMeta{ 10796 Name: "123", 10797 Namespace: "ns", 10798 }, 10799 Spec: core.PodSpec{ 10800 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10801 SecurityContext: &core.SecurityContext{ 10802 SeccompProfile: &core.SeccompProfile{ 10803 Type: core.SeccompProfileTypeLocalhost, 10804 LocalhostProfile: utilpointer.String("filename.json"), 10805 }, 10806 }, 10807 }}, 10808 RestartPolicy: core.RestartPolicyAlways, 10809 DNSPolicy: core.DNSDefault, 10810 }, 10811 }, 10812 "default AppArmor annotation for a container": { 10813 ObjectMeta: metav1.ObjectMeta{ 10814 Name: "123", 10815 Namespace: "ns", 10816 Annotations: map[string]string{ 10817 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "ctr": v1.DeprecatedAppArmorBetaProfileRuntimeDefault, 10818 }, 10819 }, 10820 Spec: validPodSpec(nil), 10821 }, 10822 "default AppArmor annotation for an init container": { 10823 ObjectMeta: metav1.ObjectMeta{ 10824 Name: "123", 10825 Namespace: "ns", 10826 Annotations: map[string]string{ 10827 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "init-ctr": v1.DeprecatedAppArmorBetaProfileRuntimeDefault, 10828 }, 10829 }, 10830 Spec: core.PodSpec{ 10831 InitContainers: []core.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10832 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10833 RestartPolicy: core.RestartPolicyAlways, 10834 DNSPolicy: core.DNSClusterFirst, 10835 }, 10836 }, 10837 "localhost AppArmor annotation for a container": { 10838 ObjectMeta: metav1.ObjectMeta{ 10839 Name: "123", 10840 Namespace: "ns", 10841 Annotations: map[string]string{ 10842 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "ctr": v1.DeprecatedAppArmorBetaProfileNamePrefix + "foo", 10843 }, 10844 }, 10845 Spec: validPodSpec(nil), 10846 }, 10847 "runtime default AppArmor profile for a pod": { 10848 ObjectMeta: metav1.ObjectMeta{ 10849 Name: "123", 10850 Namespace: "ns", 10851 }, 10852 Spec: core.PodSpec{ 10853 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10854 RestartPolicy: core.RestartPolicyAlways, 10855 DNSPolicy: core.DNSDefault, 10856 SecurityContext: &core.PodSecurityContext{ 10857 AppArmorProfile: &core.AppArmorProfile{ 10858 Type: core.AppArmorProfileTypeRuntimeDefault, 10859 }, 10860 }, 10861 }, 10862 }, 10863 "runtime default AppArmor profile for a container": { 10864 ObjectMeta: metav1.ObjectMeta{ 10865 Name: "123", 10866 Namespace: "ns", 10867 }, 10868 Spec: core.PodSpec{ 10869 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10870 SecurityContext: &core.SecurityContext{ 10871 AppArmorProfile: &core.AppArmorProfile{ 10872 Type: core.AppArmorProfileTypeRuntimeDefault, 10873 }, 10874 }, 10875 }}, 10876 RestartPolicy: core.RestartPolicyAlways, 10877 DNSPolicy: core.DNSDefault, 10878 }, 10879 }, 10880 "unconfined AppArmor profile for a pod": { 10881 ObjectMeta: metav1.ObjectMeta{ 10882 Name: "123", 10883 Namespace: "ns", 10884 }, 10885 Spec: core.PodSpec{ 10886 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10887 RestartPolicy: core.RestartPolicyAlways, 10888 DNSPolicy: core.DNSDefault, 10889 SecurityContext: &core.PodSecurityContext{ 10890 AppArmorProfile: &core.AppArmorProfile{ 10891 Type: core.AppArmorProfileTypeUnconfined, 10892 }, 10893 }, 10894 }, 10895 }, 10896 "unconfined AppArmor profile for a container": { 10897 ObjectMeta: metav1.ObjectMeta{ 10898 Name: "123", 10899 Namespace: "ns", 10900 }, 10901 Spec: core.PodSpec{ 10902 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10903 SecurityContext: &core.SecurityContext{ 10904 AppArmorProfile: &core.AppArmorProfile{ 10905 Type: core.AppArmorProfileTypeUnconfined, 10906 }, 10907 }, 10908 }}, 10909 RestartPolicy: core.RestartPolicyAlways, 10910 DNSPolicy: core.DNSDefault, 10911 }, 10912 }, 10913 "localhost AppArmor profile for a pod": { 10914 ObjectMeta: metav1.ObjectMeta{ 10915 Name: "123", 10916 Namespace: "ns", 10917 }, 10918 Spec: core.PodSpec{ 10919 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10920 RestartPolicy: core.RestartPolicyAlways, 10921 DNSPolicy: core.DNSDefault, 10922 SecurityContext: &core.PodSecurityContext{ 10923 AppArmorProfile: &core.AppArmorProfile{ 10924 Type: core.AppArmorProfileTypeLocalhost, 10925 LocalhostProfile: ptr.To("example-org/application-foo"), 10926 }, 10927 }, 10928 }, 10929 }, 10930 "localhost AppArmor profile for a container field": { 10931 ObjectMeta: metav1.ObjectMeta{ 10932 Name: "123", 10933 Namespace: "ns", 10934 }, 10935 Spec: core.PodSpec{ 10936 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10937 SecurityContext: &core.SecurityContext{ 10938 AppArmorProfile: &core.AppArmorProfile{ 10939 Type: core.AppArmorProfileTypeLocalhost, 10940 LocalhostProfile: ptr.To("example-org/application-foo"), 10941 }, 10942 }, 10943 }}, 10944 RestartPolicy: core.RestartPolicyAlways, 10945 DNSPolicy: core.DNSDefault, 10946 }, 10947 }, 10948 "matching AppArmor fields and annotations": { 10949 ObjectMeta: metav1.ObjectMeta{ 10950 Name: "123", 10951 Namespace: "ns", 10952 Annotations: map[string]string{ 10953 core.DeprecatedAppArmorAnnotationKeyPrefix + "ctr": core.DeprecatedAppArmorAnnotationValueLocalhostPrefix + "foo", 10954 }, 10955 }, 10956 Spec: core.PodSpec{ 10957 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 10958 SecurityContext: &core.SecurityContext{ 10959 AppArmorProfile: &core.AppArmorProfile{ 10960 Type: core.AppArmorProfileTypeLocalhost, 10961 LocalhostProfile: ptr.To("foo"), 10962 }, 10963 }, 10964 }}, 10965 RestartPolicy: core.RestartPolicyAlways, 10966 DNSPolicy: core.DNSDefault, 10967 }, 10968 }, 10969 "matching AppArmor pod field and annotations": { 10970 ObjectMeta: metav1.ObjectMeta{ 10971 Name: "123", 10972 Namespace: "ns", 10973 Annotations: map[string]string{ 10974 core.DeprecatedAppArmorAnnotationKeyPrefix + "ctr": core.DeprecatedAppArmorAnnotationValueLocalhostPrefix + "foo", 10975 }, 10976 }, 10977 Spec: core.PodSpec{ 10978 SecurityContext: &core.PodSecurityContext{ 10979 AppArmorProfile: &core.AppArmorProfile{ 10980 Type: core.AppArmorProfileTypeLocalhost, 10981 LocalhostProfile: ptr.To("foo"), 10982 }, 10983 }, 10984 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10985 RestartPolicy: core.RestartPolicyAlways, 10986 DNSPolicy: core.DNSDefault, 10987 }, 10988 }, 10989 "syntactically valid sysctls": { 10990 ObjectMeta: metav1.ObjectMeta{ 10991 Name: "123", 10992 Namespace: "ns", 10993 }, 10994 Spec: core.PodSpec{ 10995 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 10996 RestartPolicy: core.RestartPolicyAlways, 10997 DNSPolicy: core.DNSClusterFirst, 10998 SecurityContext: &core.PodSecurityContext{ 10999 Sysctls: []core.Sysctl{{ 11000 Name: "kernel.shmmni", 11001 Value: "32768", 11002 }, { 11003 Name: "kernel.shmmax", 11004 Value: "1000000000", 11005 }, { 11006 Name: "knet.ipv4.route.min_pmtu", 11007 Value: "1000", 11008 }}, 11009 }, 11010 }, 11011 }, 11012 "valid extended resources for init container": { 11013 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 11014 Spec: core.PodSpec{ 11015 InitContainers: []core.Container{{ 11016 Name: "valid-extended", 11017 Image: "image", 11018 ImagePullPolicy: "IfNotPresent", 11019 Resources: core.ResourceRequirements{ 11020 Requests: core.ResourceList{ 11021 core.ResourceName("example.com/a"): resource.MustParse("10"), 11022 }, 11023 Limits: core.ResourceList{ 11024 core.ResourceName("example.com/a"): resource.MustParse("10"), 11025 }, 11026 }, 11027 TerminationMessagePolicy: "File", 11028 }}, 11029 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11030 RestartPolicy: core.RestartPolicyAlways, 11031 DNSPolicy: core.DNSClusterFirst, 11032 }, 11033 }, 11034 "valid extended resources for regular container": { 11035 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 11036 Spec: core.PodSpec{ 11037 InitContainers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11038 Containers: []core.Container{{ 11039 Name: "valid-extended", 11040 Image: "image", 11041 ImagePullPolicy: "IfNotPresent", 11042 Resources: core.ResourceRequirements{ 11043 Requests: core.ResourceList{ 11044 core.ResourceName("example.com/a"): resource.MustParse("10"), 11045 }, 11046 Limits: core.ResourceList{ 11047 core.ResourceName("example.com/a"): resource.MustParse("10"), 11048 }, 11049 }, 11050 TerminationMessagePolicy: "File", 11051 }}, 11052 RestartPolicy: core.RestartPolicyAlways, 11053 DNSPolicy: core.DNSClusterFirst, 11054 }, 11055 }, 11056 "valid serviceaccount token projected volume with serviceaccount name specified": { 11057 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 11058 Spec: core.PodSpec{ 11059 ServiceAccountName: "some-service-account", 11060 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11061 RestartPolicy: core.RestartPolicyAlways, 11062 DNSPolicy: core.DNSClusterFirst, 11063 Volumes: []core.Volume{{ 11064 Name: "projected-volume", 11065 VolumeSource: core.VolumeSource{ 11066 Projected: &core.ProjectedVolumeSource{ 11067 Sources: []core.VolumeProjection{{ 11068 ServiceAccountToken: &core.ServiceAccountTokenProjection{ 11069 Audience: "foo-audience", 11070 ExpirationSeconds: 6000, 11071 Path: "foo-path", 11072 }, 11073 }}, 11074 }, 11075 }, 11076 }}, 11077 }, 11078 }, 11079 "valid ClusterTrustBundlePEM projected volume referring to a CTB by name": { 11080 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 11081 Spec: core.PodSpec{ 11082 ServiceAccountName: "some-service-account", 11083 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11084 RestartPolicy: core.RestartPolicyAlways, 11085 DNSPolicy: core.DNSClusterFirst, 11086 Volumes: []core.Volume{ 11087 { 11088 Name: "projected-volume", 11089 VolumeSource: core.VolumeSource{ 11090 Projected: &core.ProjectedVolumeSource{ 11091 Sources: []core.VolumeProjection{ 11092 { 11093 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 11094 Path: "foo-path", 11095 Name: utilpointer.String("foo"), 11096 }, 11097 }, 11098 }, 11099 }, 11100 }, 11101 }, 11102 }, 11103 }, 11104 }, 11105 "valid ClusterTrustBundlePEM projected volume referring to a CTB by signer name": { 11106 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 11107 Spec: core.PodSpec{ 11108 ServiceAccountName: "some-service-account", 11109 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11110 RestartPolicy: core.RestartPolicyAlways, 11111 DNSPolicy: core.DNSClusterFirst, 11112 Volumes: []core.Volume{ 11113 { 11114 Name: "projected-volume", 11115 VolumeSource: core.VolumeSource{ 11116 Projected: &core.ProjectedVolumeSource{ 11117 Sources: []core.VolumeProjection{ 11118 { 11119 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 11120 Path: "foo-path", 11121 SignerName: utilpointer.String("example.com/foo"), 11122 LabelSelector: &metav1.LabelSelector{ 11123 MatchLabels: map[string]string{ 11124 "version": "live", 11125 }, 11126 }, 11127 }, 11128 }, 11129 }, 11130 }, 11131 }, 11132 }, 11133 }, 11134 }, 11135 }, 11136 "ephemeral volume + PVC, no conflict between them": { 11137 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11138 Spec: core.PodSpec{ 11139 Volumes: []core.Volume{ 11140 {Name: "pvc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "my-pvc"}}}, 11141 {Name: "ephemeral", VolumeSource: core.VolumeSource{Ephemeral: &core.EphemeralVolumeSource{VolumeClaimTemplate: &validPVCTemplate}}}, 11142 }, 11143 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11144 RestartPolicy: core.RestartPolicyAlways, 11145 DNSPolicy: core.DNSClusterFirst, 11146 }, 11147 }, 11148 "negative pod-deletion-cost": { 11149 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.PodDeletionCost: "-100"}}, 11150 Spec: core.PodSpec{ 11151 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11152 RestartPolicy: core.RestartPolicyAlways, 11153 DNSPolicy: core.DNSClusterFirst, 11154 }, 11155 }, 11156 "positive pod-deletion-cost": { 11157 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.PodDeletionCost: "100"}}, 11158 Spec: core.PodSpec{ 11159 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11160 RestartPolicy: core.RestartPolicyAlways, 11161 DNSPolicy: core.DNSClusterFirst, 11162 }, 11163 }, 11164 "MatchLabelKeys/MismatchLabelKeys in required PodAffinity": { 11165 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11166 Spec: core.PodSpec{ 11167 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11168 RestartPolicy: core.RestartPolicyAlways, 11169 DNSPolicy: core.DNSClusterFirst, 11170 Affinity: &core.Affinity{ 11171 PodAffinity: &core.PodAffinity{ 11172 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11173 { 11174 LabelSelector: &metav1.LabelSelector{ 11175 MatchExpressions: []metav1.LabelSelectorRequirement{ 11176 { 11177 Key: "key", 11178 Operator: metav1.LabelSelectorOpNotIn, 11179 Values: []string{"value1", "value2"}, 11180 }, 11181 { 11182 Key: "key2", 11183 Operator: metav1.LabelSelectorOpIn, 11184 Values: []string{"value1"}, 11185 }, 11186 { 11187 Key: "key3", 11188 Operator: metav1.LabelSelectorOpNotIn, 11189 Values: []string{"value1"}, 11190 }, 11191 }, 11192 }, 11193 TopologyKey: "k8s.io/zone", 11194 MatchLabelKeys: []string{"key2"}, 11195 MismatchLabelKeys: []string{"key3"}, 11196 }, 11197 }, 11198 }, 11199 }, 11200 }, 11201 }, 11202 "MatchLabelKeys/MismatchLabelKeys in preferred PodAffinity": { 11203 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11204 Spec: core.PodSpec{ 11205 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11206 RestartPolicy: core.RestartPolicyAlways, 11207 DNSPolicy: core.DNSClusterFirst, 11208 Affinity: &core.Affinity{ 11209 PodAffinity: &core.PodAffinity{ 11210 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 11211 { 11212 Weight: 10, 11213 PodAffinityTerm: core.PodAffinityTerm{ 11214 LabelSelector: &metav1.LabelSelector{ 11215 MatchExpressions: []metav1.LabelSelectorRequirement{ 11216 { 11217 Key: "key", 11218 Operator: metav1.LabelSelectorOpNotIn, 11219 Values: []string{"value1", "value2"}, 11220 }, 11221 { 11222 Key: "key2", 11223 Operator: metav1.LabelSelectorOpIn, 11224 Values: []string{"value1"}, 11225 }, 11226 { 11227 Key: "key3", 11228 Operator: metav1.LabelSelectorOpNotIn, 11229 Values: []string{"value1"}, 11230 }, 11231 }, 11232 }, 11233 TopologyKey: "k8s.io/zone", 11234 MatchLabelKeys: []string{"key2"}, 11235 MismatchLabelKeys: []string{"key3"}, 11236 }, 11237 }, 11238 }, 11239 }, 11240 }, 11241 }, 11242 }, 11243 "MatchLabelKeys/MismatchLabelKeys in required PodAntiAffinity": { 11244 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11245 Spec: core.PodSpec{ 11246 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11247 RestartPolicy: core.RestartPolicyAlways, 11248 DNSPolicy: core.DNSClusterFirst, 11249 Affinity: &core.Affinity{ 11250 PodAntiAffinity: &core.PodAntiAffinity{ 11251 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11252 { 11253 LabelSelector: &metav1.LabelSelector{ 11254 MatchExpressions: []metav1.LabelSelectorRequirement{ 11255 { 11256 Key: "key", 11257 Operator: metav1.LabelSelectorOpNotIn, 11258 Values: []string{"value1", "value2"}, 11259 }, 11260 { 11261 Key: "key2", 11262 Operator: metav1.LabelSelectorOpIn, 11263 Values: []string{"value1"}, 11264 }, 11265 { 11266 Key: "key3", 11267 Operator: metav1.LabelSelectorOpNotIn, 11268 Values: []string{"value1"}, 11269 }, 11270 }, 11271 }, 11272 TopologyKey: "k8s.io/zone", 11273 MatchLabelKeys: []string{"key2"}, 11274 MismatchLabelKeys: []string{"key3"}, 11275 }, 11276 }, 11277 }, 11278 }, 11279 }, 11280 }, 11281 "MatchLabelKeys/MismatchLabelKeys in preferred PodAntiAffinity": { 11282 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11283 Spec: core.PodSpec{ 11284 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11285 RestartPolicy: core.RestartPolicyAlways, 11286 DNSPolicy: core.DNSClusterFirst, 11287 Affinity: &core.Affinity{ 11288 PodAntiAffinity: &core.PodAntiAffinity{ 11289 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 11290 { 11291 Weight: 10, 11292 PodAffinityTerm: core.PodAffinityTerm{ 11293 LabelSelector: &metav1.LabelSelector{ 11294 MatchExpressions: []metav1.LabelSelectorRequirement{ 11295 { 11296 Key: "key", 11297 Operator: metav1.LabelSelectorOpNotIn, 11298 Values: []string{"value1", "value2"}, 11299 }, 11300 { 11301 Key: "key2", 11302 Operator: metav1.LabelSelectorOpIn, 11303 Values: []string{"value1"}, 11304 }, 11305 { 11306 Key: "key3", 11307 Operator: metav1.LabelSelectorOpNotIn, 11308 Values: []string{"value1"}, 11309 }, 11310 }, 11311 }, 11312 TopologyKey: "k8s.io/zone", 11313 MatchLabelKeys: []string{"key2"}, 11314 MismatchLabelKeys: []string{"key3"}, 11315 }, 11316 }, 11317 }, 11318 }, 11319 }, 11320 }, 11321 }, 11322 "LabelSelector can have the same key as MismatchLabelKeys": { 11323 // Note: On the contrary, in case of matchLabelKeys, keys in matchLabelKeys are not allowed to be specified in labelSelector by users. 11324 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 11325 Spec: core.PodSpec{ 11326 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11327 RestartPolicy: core.RestartPolicyAlways, 11328 DNSPolicy: core.DNSClusterFirst, 11329 Affinity: &core.Affinity{ 11330 PodAffinity: &core.PodAffinity{ 11331 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11332 { 11333 LabelSelector: &metav1.LabelSelector{ 11334 MatchExpressions: []metav1.LabelSelectorRequirement{ 11335 { 11336 Key: "key", 11337 Operator: metav1.LabelSelectorOpNotIn, 11338 Values: []string{"value1", "value2"}, 11339 }, 11340 { 11341 // This is the same key as in MismatchLabelKeys 11342 // but it's allowed. 11343 Key: "key2", 11344 Operator: metav1.LabelSelectorOpIn, 11345 Values: []string{"value1"}, 11346 }, 11347 { 11348 Key: "key2", 11349 Operator: metav1.LabelSelectorOpNotIn, 11350 Values: []string{"value1"}, 11351 }, 11352 }, 11353 }, 11354 TopologyKey: "k8s.io/zone", 11355 MismatchLabelKeys: []string{"key2"}, 11356 }, 11357 }, 11358 }, 11359 }, 11360 }, 11361 }, 11362 } 11363 11364 for k, v := range successCases { 11365 t.Run(k, func(t *testing.T) { 11366 if errs := ValidatePodCreate(&v, PodValidationOptions{}); len(errs) != 0 { 11367 t.Errorf("expected success: %v", errs) 11368 } 11369 }) 11370 } 11371 11372 errorCases := map[string]struct { 11373 spec core.Pod 11374 expectedError string 11375 }{ 11376 "bad name": { 11377 expectedError: "metadata.name", 11378 spec: core.Pod{ 11379 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: "ns"}, 11380 Spec: core.PodSpec{ 11381 RestartPolicy: core.RestartPolicyAlways, 11382 DNSPolicy: core.DNSClusterFirst, 11383 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11384 }, 11385 }, 11386 }, 11387 "image whitespace": { 11388 expectedError: "spec.containers[0].image", 11389 spec: core.Pod{ 11390 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "ns"}, 11391 Spec: core.PodSpec{ 11392 RestartPolicy: core.RestartPolicyAlways, 11393 DNSPolicy: core.DNSClusterFirst, 11394 Containers: []core.Container{{Name: "ctr", Image: " ", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11395 }, 11396 }, 11397 }, 11398 "image leading and trailing whitespace": { 11399 expectedError: "spec.containers[0].image", 11400 spec: core.Pod{ 11401 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "ns"}, 11402 Spec: core.PodSpec{ 11403 RestartPolicy: core.RestartPolicyAlways, 11404 DNSPolicy: core.DNSClusterFirst, 11405 Containers: []core.Container{{Name: "ctr", Image: " something ", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11406 }, 11407 }, 11408 }, 11409 "bad namespace": { 11410 expectedError: "metadata.namespace", 11411 spec: core.Pod{ 11412 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: ""}, 11413 Spec: core.PodSpec{ 11414 RestartPolicy: core.RestartPolicyAlways, 11415 DNSPolicy: core.DNSClusterFirst, 11416 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11417 }, 11418 }, 11419 }, 11420 "bad spec": { 11421 expectedError: "spec.containers[0].name", 11422 spec: core.Pod{ 11423 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "ns"}, 11424 Spec: core.PodSpec{ 11425 Containers: []core.Container{{}}, 11426 }, 11427 }, 11428 }, 11429 "bad label": { 11430 expectedError: "NoUppercaseOrSpecialCharsLike=Equals", 11431 spec: core.Pod{ 11432 ObjectMeta: metav1.ObjectMeta{ 11433 Name: "abc", 11434 Namespace: "ns", 11435 Labels: map[string]string{ 11436 "NoUppercaseOrSpecialCharsLike=Equals": "bar", 11437 }, 11438 }, 11439 Spec: core.PodSpec{ 11440 RestartPolicy: core.RestartPolicyAlways, 11441 DNSPolicy: core.DNSClusterFirst, 11442 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 11443 }, 11444 }, 11445 }, 11446 "invalid node selector requirement in node affinity, operator can't be null": { 11447 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator", 11448 spec: core.Pod{ 11449 ObjectMeta: metav1.ObjectMeta{ 11450 Name: "123", 11451 Namespace: "ns", 11452 }, 11453 Spec: validPodSpec(&core.Affinity{ 11454 NodeAffinity: &core.NodeAffinity{ 11455 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11456 NodeSelectorTerms: []core.NodeSelectorTerm{{ 11457 MatchExpressions: []core.NodeSelectorRequirement{{ 11458 Key: "key1", 11459 }}, 11460 }}, 11461 }, 11462 }, 11463 }), 11464 }, 11465 }, 11466 "invalid node selector requirement in node affinity, key is invalid": { 11467 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key", 11468 spec: core.Pod{ 11469 ObjectMeta: metav1.ObjectMeta{ 11470 Name: "123", 11471 Namespace: "ns", 11472 }, 11473 Spec: validPodSpec(&core.Affinity{ 11474 NodeAffinity: &core.NodeAffinity{ 11475 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11476 NodeSelectorTerms: []core.NodeSelectorTerm{{ 11477 MatchExpressions: []core.NodeSelectorRequirement{{ 11478 Key: "invalid key ___@#", 11479 Operator: core.NodeSelectorOpExists, 11480 }}, 11481 }}, 11482 }, 11483 }, 11484 }), 11485 }, 11486 }, 11487 "invalid node field selector requirement in node affinity, more values for field selector": { 11488 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchFields[0].values", 11489 spec: core.Pod{ 11490 ObjectMeta: metav1.ObjectMeta{ 11491 Name: "123", 11492 Namespace: "ns", 11493 }, 11494 Spec: validPodSpec(&core.Affinity{ 11495 NodeAffinity: &core.NodeAffinity{ 11496 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11497 NodeSelectorTerms: []core.NodeSelectorTerm{{ 11498 MatchFields: []core.NodeSelectorRequirement{{ 11499 Key: "metadata.name", 11500 Operator: core.NodeSelectorOpIn, 11501 Values: []string{"host1", "host2"}, 11502 }}, 11503 }}, 11504 }, 11505 }, 11506 }), 11507 }, 11508 }, 11509 "invalid node field selector requirement in node affinity, invalid operator": { 11510 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchFields[0].operator", 11511 spec: core.Pod{ 11512 ObjectMeta: metav1.ObjectMeta{ 11513 Name: "123", 11514 Namespace: "ns", 11515 }, 11516 Spec: validPodSpec(&core.Affinity{ 11517 NodeAffinity: &core.NodeAffinity{ 11518 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11519 NodeSelectorTerms: []core.NodeSelectorTerm{{ 11520 MatchFields: []core.NodeSelectorRequirement{{ 11521 Key: "metadata.name", 11522 Operator: core.NodeSelectorOpExists, 11523 }}, 11524 }}, 11525 }, 11526 }, 11527 }), 11528 }, 11529 }, 11530 "invalid node field selector requirement in node affinity, invalid key": { 11531 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchFields[0].key", 11532 spec: core.Pod{ 11533 ObjectMeta: metav1.ObjectMeta{ 11534 Name: "123", 11535 Namespace: "ns", 11536 }, 11537 Spec: validPodSpec(&core.Affinity{ 11538 NodeAffinity: &core.NodeAffinity{ 11539 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11540 NodeSelectorTerms: []core.NodeSelectorTerm{{ 11541 MatchFields: []core.NodeSelectorRequirement{{ 11542 Key: "metadata.namespace", 11543 Operator: core.NodeSelectorOpIn, 11544 Values: []string{"ns1"}, 11545 }}, 11546 }}, 11547 }, 11548 }, 11549 }), 11550 }, 11551 }, 11552 "invalid preferredSchedulingTerm in node affinity, weight should be in range 1-100": { 11553 expectedError: "must be in the range 1-100", 11554 spec: core.Pod{ 11555 ObjectMeta: metav1.ObjectMeta{ 11556 Name: "123", 11557 Namespace: "ns", 11558 }, 11559 Spec: validPodSpec(&core.Affinity{ 11560 NodeAffinity: &core.NodeAffinity{ 11561 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 11562 Weight: 199, 11563 Preference: core.NodeSelectorTerm{ 11564 MatchExpressions: []core.NodeSelectorRequirement{{ 11565 Key: "foo", 11566 Operator: core.NodeSelectorOpIn, 11567 Values: []string{"bar"}, 11568 }}, 11569 }, 11570 }}, 11571 }, 11572 }), 11573 }, 11574 }, 11575 "invalid requiredDuringSchedulingIgnoredDuringExecution node selector, nodeSelectorTerms must have at least one term": { 11576 expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms", 11577 spec: core.Pod{ 11578 ObjectMeta: metav1.ObjectMeta{ 11579 Name: "123", 11580 Namespace: "ns", 11581 }, 11582 Spec: validPodSpec(&core.Affinity{ 11583 NodeAffinity: &core.NodeAffinity{ 11584 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 11585 NodeSelectorTerms: []core.NodeSelectorTerm{}, 11586 }, 11587 }, 11588 }), 11589 }, 11590 }, 11591 "invalid weight in preferredDuringSchedulingIgnoredDuringExecution in pod affinity annotations, weight should be in range 1-100": { 11592 expectedError: "must be in the range 1-100", 11593 spec: core.Pod{ 11594 ObjectMeta: metav1.ObjectMeta{ 11595 Name: "123", 11596 Namespace: "ns", 11597 }, 11598 Spec: validPodSpec(&core.Affinity{ 11599 PodAffinity: &core.PodAffinity{ 11600 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11601 Weight: 109, 11602 PodAffinityTerm: core.PodAffinityTerm{ 11603 LabelSelector: &metav1.LabelSelector{ 11604 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11605 Key: "key2", 11606 Operator: metav1.LabelSelectorOpNotIn, 11607 Values: []string{"value1", "value2"}, 11608 }}, 11609 }, 11610 Namespaces: []string{"ns"}, 11611 TopologyKey: "region", 11612 }, 11613 }}, 11614 }, 11615 }), 11616 }, 11617 }, 11618 "invalid labelSelector in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, values should be empty if the operator is Exists": { 11619 expectedError: "spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values", 11620 spec: core.Pod{ 11621 ObjectMeta: metav1.ObjectMeta{ 11622 Name: "123", 11623 Namespace: "ns", 11624 }, 11625 Spec: validPodSpec(&core.Affinity{ 11626 PodAntiAffinity: &core.PodAntiAffinity{ 11627 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11628 Weight: 10, 11629 PodAffinityTerm: core.PodAffinityTerm{ 11630 LabelSelector: &metav1.LabelSelector{ 11631 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11632 Key: "key2", 11633 Operator: metav1.LabelSelectorOpExists, 11634 Values: []string{"value1", "value2"}, 11635 }}, 11636 }, 11637 Namespaces: []string{"ns"}, 11638 TopologyKey: "region", 11639 }, 11640 }}, 11641 }, 11642 }), 11643 }, 11644 }, 11645 "invalid namespaceSelector in preferredDuringSchedulingIgnoredDuringExecution in podaffinity, In operator must include Values": { 11646 expectedError: "spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.namespaceSelector.matchExpressions[0].values", 11647 spec: core.Pod{ 11648 ObjectMeta: metav1.ObjectMeta{ 11649 Name: "123", 11650 Namespace: "ns", 11651 }, 11652 Spec: validPodSpec(&core.Affinity{ 11653 PodAntiAffinity: &core.PodAntiAffinity{ 11654 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11655 Weight: 10, 11656 PodAffinityTerm: core.PodAffinityTerm{ 11657 NamespaceSelector: &metav1.LabelSelector{ 11658 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11659 Key: "key2", 11660 Operator: metav1.LabelSelectorOpIn, 11661 }}, 11662 }, 11663 Namespaces: []string{"ns"}, 11664 TopologyKey: "region", 11665 }, 11666 }}, 11667 }, 11668 }), 11669 }, 11670 }, 11671 "invalid namespaceSelector in preferredDuringSchedulingIgnoredDuringExecution in podaffinity, Exists operator can not have values": { 11672 expectedError: "spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.namespaceSelector.matchExpressions[0].values", 11673 spec: core.Pod{ 11674 ObjectMeta: metav1.ObjectMeta{ 11675 Name: "123", 11676 Namespace: "ns", 11677 }, 11678 Spec: validPodSpec(&core.Affinity{ 11679 PodAntiAffinity: &core.PodAntiAffinity{ 11680 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11681 Weight: 10, 11682 PodAffinityTerm: core.PodAffinityTerm{ 11683 NamespaceSelector: &metav1.LabelSelector{ 11684 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11685 Key: "key2", 11686 Operator: metav1.LabelSelectorOpExists, 11687 Values: []string{"value1", "value2"}, 11688 }}, 11689 }, 11690 Namespaces: []string{"ns"}, 11691 TopologyKey: "region", 11692 }, 11693 }}, 11694 }, 11695 }), 11696 }, 11697 }, 11698 "invalid name space in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, namespace should be valid": { 11699 expectedError: "spec.affinity.podAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.namespace", 11700 spec: core.Pod{ 11701 ObjectMeta: metav1.ObjectMeta{ 11702 Name: "123", 11703 Namespace: "ns", 11704 }, 11705 Spec: validPodSpec(&core.Affinity{ 11706 PodAffinity: &core.PodAffinity{ 11707 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11708 Weight: 10, 11709 PodAffinityTerm: core.PodAffinityTerm{ 11710 LabelSelector: &metav1.LabelSelector{ 11711 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11712 Key: "key2", 11713 Operator: metav1.LabelSelectorOpExists, 11714 }}, 11715 }, 11716 Namespaces: []string{"INVALID_NAMESPACE"}, 11717 TopologyKey: "region", 11718 }, 11719 }}, 11720 }, 11721 }), 11722 }, 11723 }, 11724 "invalid hard pod affinity, empty topologyKey is not allowed for hard pod affinity": { 11725 expectedError: "can not be empty", 11726 spec: core.Pod{ 11727 ObjectMeta: metav1.ObjectMeta{ 11728 Name: "123", 11729 Namespace: "ns", 11730 }, 11731 Spec: validPodSpec(&core.Affinity{ 11732 PodAffinity: &core.PodAffinity{ 11733 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 11734 LabelSelector: &metav1.LabelSelector{ 11735 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11736 Key: "key2", 11737 Operator: metav1.LabelSelectorOpIn, 11738 Values: []string{"value1", "value2"}, 11739 }}, 11740 }, 11741 Namespaces: []string{"ns"}, 11742 }}, 11743 }, 11744 }), 11745 }, 11746 }, 11747 "invalid hard pod anti-affinity, empty topologyKey is not allowed for hard pod anti-affinity": { 11748 expectedError: "can not be empty", 11749 spec: core.Pod{ 11750 ObjectMeta: metav1.ObjectMeta{ 11751 Name: "123", 11752 Namespace: "ns", 11753 }, 11754 Spec: validPodSpec(&core.Affinity{ 11755 PodAntiAffinity: &core.PodAntiAffinity{ 11756 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 11757 LabelSelector: &metav1.LabelSelector{ 11758 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11759 Key: "key2", 11760 Operator: metav1.LabelSelectorOpIn, 11761 Values: []string{"value1", "value2"}, 11762 }}, 11763 }, 11764 Namespaces: []string{"ns"}, 11765 }}, 11766 }, 11767 }), 11768 }, 11769 }, 11770 "invalid soft pod affinity, empty topologyKey is not allowed for soft pod affinity": { 11771 expectedError: "can not be empty", 11772 spec: core.Pod{ 11773 ObjectMeta: metav1.ObjectMeta{ 11774 Name: "123", 11775 Namespace: "ns", 11776 }, 11777 Spec: validPodSpec(&core.Affinity{ 11778 PodAffinity: &core.PodAffinity{ 11779 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11780 Weight: 10, 11781 PodAffinityTerm: core.PodAffinityTerm{ 11782 LabelSelector: &metav1.LabelSelector{ 11783 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11784 Key: "key2", 11785 Operator: metav1.LabelSelectorOpNotIn, 11786 Values: []string{"value1", "value2"}, 11787 }}, 11788 }, 11789 Namespaces: []string{"ns"}, 11790 }, 11791 }}, 11792 }, 11793 }), 11794 }, 11795 }, 11796 "invalid soft pod anti-affinity, empty topologyKey is not allowed for soft pod anti-affinity": { 11797 expectedError: "can not be empty", 11798 spec: core.Pod{ 11799 ObjectMeta: metav1.ObjectMeta{ 11800 Name: "123", 11801 Namespace: "ns", 11802 }, 11803 Spec: validPodSpec(&core.Affinity{ 11804 PodAntiAffinity: &core.PodAntiAffinity{ 11805 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{{ 11806 Weight: 10, 11807 PodAffinityTerm: core.PodAffinityTerm{ 11808 LabelSelector: &metav1.LabelSelector{ 11809 MatchExpressions: []metav1.LabelSelectorRequirement{{ 11810 Key: "key2", 11811 Operator: metav1.LabelSelectorOpNotIn, 11812 Values: []string{"value1", "value2"}, 11813 }}, 11814 }, 11815 Namespaces: []string{"ns"}, 11816 }, 11817 }}, 11818 }, 11819 }), 11820 }, 11821 }, 11822 "invalid soft pod affinity, key in MatchLabelKeys isn't correctly defined": { 11823 expectedError: "prefix part must be non-empty", 11824 spec: core.Pod{ 11825 ObjectMeta: metav1.ObjectMeta{ 11826 Name: "123", 11827 Namespace: "ns", 11828 }, 11829 Spec: validPodSpec(&core.Affinity{ 11830 PodAffinity: &core.PodAffinity{ 11831 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 11832 { 11833 Weight: 10, 11834 PodAffinityTerm: core.PodAffinityTerm{ 11835 LabelSelector: &metav1.LabelSelector{ 11836 MatchExpressions: []metav1.LabelSelectorRequirement{ 11837 { 11838 Key: "key", 11839 Operator: metav1.LabelSelectorOpNotIn, 11840 Values: []string{"value1", "value2"}, 11841 }, 11842 }, 11843 }, 11844 TopologyKey: "k8s.io/zone", 11845 MatchLabelKeys: []string{"/simple"}, 11846 }, 11847 }, 11848 }, 11849 }, 11850 }), 11851 }, 11852 }, 11853 "invalid hard pod affinity, key in MatchLabelKeys isn't correctly defined": { 11854 expectedError: "prefix part must be non-empty", 11855 spec: core.Pod{ 11856 ObjectMeta: metav1.ObjectMeta{ 11857 Name: "123", 11858 Namespace: "ns", 11859 }, 11860 Spec: validPodSpec(&core.Affinity{ 11861 PodAffinity: &core.PodAffinity{ 11862 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11863 { 11864 LabelSelector: &metav1.LabelSelector{ 11865 MatchExpressions: []metav1.LabelSelectorRequirement{ 11866 { 11867 Key: "key", 11868 Operator: metav1.LabelSelectorOpNotIn, 11869 Values: []string{"value1", "value2"}, 11870 }, 11871 }, 11872 }, 11873 TopologyKey: "k8s.io/zone", 11874 MatchLabelKeys: []string{"/simple"}, 11875 }, 11876 }, 11877 }, 11878 }), 11879 }, 11880 }, 11881 "invalid soft pod anti-affinity, key in MatchLabelKeys isn't correctly defined": { 11882 expectedError: "prefix part must be non-empty", 11883 spec: core.Pod{ 11884 ObjectMeta: metav1.ObjectMeta{ 11885 Name: "123", 11886 Namespace: "ns", 11887 }, 11888 Spec: validPodSpec(&core.Affinity{ 11889 PodAntiAffinity: &core.PodAntiAffinity{ 11890 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 11891 { 11892 Weight: 10, 11893 PodAffinityTerm: core.PodAffinityTerm{ 11894 LabelSelector: &metav1.LabelSelector{ 11895 MatchExpressions: []metav1.LabelSelectorRequirement{ 11896 { 11897 Key: "key", 11898 Operator: metav1.LabelSelectorOpNotIn, 11899 Values: []string{"value1", "value2"}, 11900 }, 11901 }, 11902 }, 11903 TopologyKey: "k8s.io/zone", 11904 MatchLabelKeys: []string{"/simple"}, 11905 }, 11906 }, 11907 }, 11908 }, 11909 }), 11910 }, 11911 }, 11912 "invalid hard pod anti-affinity, key in MatchLabelKeys isn't correctly defined": { 11913 expectedError: "prefix part must be non-empty", 11914 spec: core.Pod{ 11915 ObjectMeta: metav1.ObjectMeta{ 11916 Name: "123", 11917 Namespace: "ns", 11918 }, 11919 Spec: validPodSpec(&core.Affinity{ 11920 PodAntiAffinity: &core.PodAntiAffinity{ 11921 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11922 { 11923 LabelSelector: &metav1.LabelSelector{ 11924 MatchExpressions: []metav1.LabelSelectorRequirement{ 11925 { 11926 Key: "key", 11927 Operator: metav1.LabelSelectorOpNotIn, 11928 Values: []string{"value1", "value2"}, 11929 }, 11930 }, 11931 }, 11932 TopologyKey: "k8s.io/zone", 11933 MatchLabelKeys: []string{"/simple"}, 11934 }, 11935 }, 11936 }, 11937 }), 11938 }, 11939 }, 11940 "invalid soft pod affinity, key in MismatchLabelKeys isn't correctly defined": { 11941 expectedError: "prefix part must be non-empty", 11942 spec: core.Pod{ 11943 ObjectMeta: metav1.ObjectMeta{ 11944 Name: "123", 11945 Namespace: "ns", 11946 }, 11947 Spec: validPodSpec(&core.Affinity{ 11948 PodAffinity: &core.PodAffinity{ 11949 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 11950 { 11951 Weight: 10, 11952 PodAffinityTerm: core.PodAffinityTerm{ 11953 LabelSelector: &metav1.LabelSelector{ 11954 MatchExpressions: []metav1.LabelSelectorRequirement{ 11955 { 11956 Key: "key", 11957 Operator: metav1.LabelSelectorOpNotIn, 11958 Values: []string{"value1", "value2"}, 11959 }, 11960 }, 11961 }, 11962 TopologyKey: "k8s.io/zone", 11963 MismatchLabelKeys: []string{"/simple"}, 11964 }, 11965 }, 11966 }, 11967 }, 11968 }), 11969 }, 11970 }, 11971 "invalid hard pod affinity, key in MismatchLabelKeys isn't correctly defined": { 11972 expectedError: "prefix part must be non-empty", 11973 spec: core.Pod{ 11974 ObjectMeta: metav1.ObjectMeta{ 11975 Name: "123", 11976 Namespace: "ns", 11977 }, 11978 Spec: validPodSpec(&core.Affinity{ 11979 PodAffinity: &core.PodAffinity{ 11980 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 11981 { 11982 LabelSelector: &metav1.LabelSelector{ 11983 MatchExpressions: []metav1.LabelSelectorRequirement{ 11984 { 11985 Key: "key", 11986 Operator: metav1.LabelSelectorOpNotIn, 11987 Values: []string{"value1", "value2"}, 11988 }, 11989 }, 11990 }, 11991 TopologyKey: "k8s.io/zone", 11992 MismatchLabelKeys: []string{"/simple"}, 11993 }, 11994 }, 11995 }, 11996 }), 11997 }, 11998 }, 11999 "invalid soft pod anti-affinity, key in MismatchLabelKeys isn't correctly defined": { 12000 expectedError: "prefix part must be non-empty", 12001 spec: core.Pod{ 12002 ObjectMeta: metav1.ObjectMeta{ 12003 Name: "123", 12004 Namespace: "ns", 12005 }, 12006 Spec: validPodSpec(&core.Affinity{ 12007 PodAntiAffinity: &core.PodAntiAffinity{ 12008 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 12009 { 12010 Weight: 10, 12011 PodAffinityTerm: core.PodAffinityTerm{ 12012 LabelSelector: &metav1.LabelSelector{ 12013 MatchExpressions: []metav1.LabelSelectorRequirement{ 12014 { 12015 Key: "key", 12016 Operator: metav1.LabelSelectorOpNotIn, 12017 Values: []string{"value1", "value2"}, 12018 }, 12019 }, 12020 }, 12021 TopologyKey: "k8s.io/zone", 12022 MismatchLabelKeys: []string{"/simple"}, 12023 }, 12024 }, 12025 }, 12026 }, 12027 }), 12028 }, 12029 }, 12030 "invalid hard pod anti-affinity, key in MismatchLabelKeys isn't correctly defined": { 12031 expectedError: "prefix part must be non-empty", 12032 spec: core.Pod{ 12033 ObjectMeta: metav1.ObjectMeta{ 12034 Name: "123", 12035 Namespace: "ns", 12036 }, 12037 Spec: validPodSpec(&core.Affinity{ 12038 PodAntiAffinity: &core.PodAntiAffinity{ 12039 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 12040 { 12041 LabelSelector: &metav1.LabelSelector{ 12042 MatchExpressions: []metav1.LabelSelectorRequirement{ 12043 { 12044 Key: "key", 12045 Operator: metav1.LabelSelectorOpNotIn, 12046 Values: []string{"value1", "value2"}, 12047 }, 12048 }, 12049 }, 12050 TopologyKey: "k8s.io/zone", 12051 MismatchLabelKeys: []string{"/simple"}, 12052 }, 12053 }, 12054 }, 12055 }), 12056 }, 12057 }, 12058 "invalid soft pod affinity, key exists in both matchLabelKeys and labelSelector": { 12059 expectedError: "exists in both matchLabelKeys and labelSelector", 12060 spec: core.Pod{ 12061 ObjectMeta: metav1.ObjectMeta{ 12062 Name: "123", 12063 Namespace: "ns", 12064 Labels: map[string]string{"key": "value1"}, 12065 }, 12066 Spec: validPodSpec(&core.Affinity{ 12067 PodAffinity: &core.PodAffinity{ 12068 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 12069 { 12070 Weight: 10, 12071 PodAffinityTerm: core.PodAffinityTerm{ 12072 LabelSelector: &metav1.LabelSelector{ 12073 MatchExpressions: []metav1.LabelSelectorRequirement{ 12074 // This one should be created from MatchLabelKeys. 12075 { 12076 Key: "key", 12077 Operator: metav1.LabelSelectorOpIn, 12078 Values: []string{"value1"}, 12079 }, 12080 { 12081 Key: "key", 12082 Operator: metav1.LabelSelectorOpNotIn, 12083 Values: []string{"value2"}, 12084 }, 12085 }, 12086 }, 12087 TopologyKey: "k8s.io/zone", 12088 MatchLabelKeys: []string{"key"}, 12089 }, 12090 }, 12091 }, 12092 }, 12093 }), 12094 }, 12095 }, 12096 "invalid hard pod affinity, key exists in both matchLabelKeys and labelSelector": { 12097 expectedError: "exists in both matchLabelKeys and labelSelector", 12098 spec: core.Pod{ 12099 ObjectMeta: metav1.ObjectMeta{ 12100 Name: "123", 12101 Namespace: "ns", 12102 Labels: map[string]string{"key": "value1"}, 12103 }, 12104 Spec: validPodSpec(&core.Affinity{ 12105 PodAffinity: &core.PodAffinity{ 12106 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 12107 { 12108 LabelSelector: &metav1.LabelSelector{ 12109 MatchExpressions: []metav1.LabelSelectorRequirement{ 12110 // This one should be created from MatchLabelKeys. 12111 { 12112 Key: "key", 12113 Operator: metav1.LabelSelectorOpIn, 12114 Values: []string{"value1"}, 12115 }, 12116 { 12117 Key: "key", 12118 Operator: metav1.LabelSelectorOpNotIn, 12119 Values: []string{"value2"}, 12120 }, 12121 }, 12122 }, 12123 TopologyKey: "k8s.io/zone", 12124 MatchLabelKeys: []string{"key"}, 12125 }, 12126 }, 12127 }, 12128 }), 12129 }, 12130 }, 12131 "invalid soft pod anti-affinity, key exists in both matchLabelKeys and labelSelector": { 12132 expectedError: "exists in both matchLabelKeys and labelSelector", 12133 spec: core.Pod{ 12134 ObjectMeta: metav1.ObjectMeta{ 12135 Name: "123", 12136 Namespace: "ns", 12137 Labels: map[string]string{"key": "value1"}, 12138 }, 12139 Spec: validPodSpec(&core.Affinity{ 12140 PodAntiAffinity: &core.PodAntiAffinity{ 12141 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 12142 { 12143 Weight: 10, 12144 PodAffinityTerm: core.PodAffinityTerm{ 12145 LabelSelector: &metav1.LabelSelector{ 12146 MatchExpressions: []metav1.LabelSelectorRequirement{ 12147 // This one should be created from MatchLabelKeys. 12148 { 12149 Key: "key", 12150 Operator: metav1.LabelSelectorOpIn, 12151 Values: []string{"value1"}, 12152 }, 12153 { 12154 Key: "key", 12155 Operator: metav1.LabelSelectorOpNotIn, 12156 Values: []string{"value2"}, 12157 }, 12158 }, 12159 }, 12160 TopologyKey: "k8s.io/zone", 12161 MatchLabelKeys: []string{"key"}, 12162 }, 12163 }, 12164 }, 12165 }, 12166 }), 12167 }, 12168 }, 12169 "invalid hard pod anti-affinity, key exists in both matchLabelKeys and labelSelector": { 12170 expectedError: "exists in both matchLabelKeys and labelSelector", 12171 spec: core.Pod{ 12172 ObjectMeta: metav1.ObjectMeta{ 12173 Name: "123", 12174 Namespace: "ns", 12175 Labels: map[string]string{"key": "value1"}, 12176 }, 12177 Spec: validPodSpec(&core.Affinity{ 12178 PodAntiAffinity: &core.PodAntiAffinity{ 12179 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 12180 { 12181 LabelSelector: &metav1.LabelSelector{ 12182 MatchExpressions: []metav1.LabelSelectorRequirement{ 12183 // This one should be created from MatchLabelKeys. 12184 { 12185 Key: "key", 12186 Operator: metav1.LabelSelectorOpIn, 12187 Values: []string{"value1"}, 12188 }, 12189 { 12190 Key: "key", 12191 Operator: metav1.LabelSelectorOpNotIn, 12192 Values: []string{"value2"}, 12193 }, 12194 }, 12195 }, 12196 TopologyKey: "k8s.io/zone", 12197 MatchLabelKeys: []string{"key"}, 12198 }, 12199 }, 12200 }, 12201 }), 12202 }, 12203 }, 12204 "invalid soft pod affinity, key exists in both MatchLabelKeys and MismatchLabelKeys": { 12205 expectedError: "exists in both matchLabelKeys and mismatchLabelKeys", 12206 spec: core.Pod{ 12207 ObjectMeta: metav1.ObjectMeta{ 12208 Name: "123", 12209 Namespace: "ns", 12210 }, 12211 Spec: validPodSpec(&core.Affinity{ 12212 PodAffinity: &core.PodAffinity{ 12213 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 12214 { 12215 Weight: 10, 12216 PodAffinityTerm: core.PodAffinityTerm{ 12217 LabelSelector: &metav1.LabelSelector{ 12218 MatchExpressions: []metav1.LabelSelectorRequirement{ 12219 { 12220 Key: "key", 12221 Operator: metav1.LabelSelectorOpNotIn, 12222 Values: []string{"value1", "value2"}, 12223 }, 12224 }, 12225 }, 12226 TopologyKey: "k8s.io/zone", 12227 MatchLabelKeys: []string{"samekey"}, 12228 MismatchLabelKeys: []string{"samekey"}, 12229 }, 12230 }, 12231 }, 12232 }, 12233 }), 12234 }, 12235 }, 12236 "invalid hard pod affinity, key exists in both MatchLabelKeys and MismatchLabelKeys": { 12237 expectedError: "exists in both matchLabelKeys and mismatchLabelKeys", 12238 spec: core.Pod{ 12239 ObjectMeta: metav1.ObjectMeta{ 12240 Name: "123", 12241 Namespace: "ns", 12242 }, 12243 Spec: validPodSpec(&core.Affinity{ 12244 PodAffinity: &core.PodAffinity{ 12245 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 12246 { 12247 LabelSelector: &metav1.LabelSelector{ 12248 MatchExpressions: []metav1.LabelSelectorRequirement{ 12249 { 12250 Key: "key", 12251 Operator: metav1.LabelSelectorOpNotIn, 12252 Values: []string{"value1", "value2"}, 12253 }, 12254 }, 12255 }, 12256 TopologyKey: "k8s.io/zone", 12257 MatchLabelKeys: []string{"samekey"}, 12258 MismatchLabelKeys: []string{"samekey"}, 12259 }, 12260 }, 12261 }, 12262 }), 12263 }, 12264 }, 12265 "invalid soft pod anti-affinity, key exists in both MatchLabelKeys and MismatchLabelKeys": { 12266 expectedError: "exists in both matchLabelKeys and mismatchLabelKeys", 12267 spec: core.Pod{ 12268 ObjectMeta: metav1.ObjectMeta{ 12269 Name: "123", 12270 Namespace: "ns", 12271 }, 12272 Spec: validPodSpec(&core.Affinity{ 12273 PodAntiAffinity: &core.PodAntiAffinity{ 12274 PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{ 12275 { 12276 Weight: 10, 12277 PodAffinityTerm: core.PodAffinityTerm{ 12278 LabelSelector: &metav1.LabelSelector{ 12279 MatchExpressions: []metav1.LabelSelectorRequirement{ 12280 { 12281 Key: "key", 12282 Operator: metav1.LabelSelectorOpNotIn, 12283 Values: []string{"value1", "value2"}, 12284 }, 12285 }, 12286 }, 12287 TopologyKey: "k8s.io/zone", 12288 MatchLabelKeys: []string{"samekey"}, 12289 MismatchLabelKeys: []string{"samekey"}, 12290 }, 12291 }, 12292 }, 12293 }, 12294 }), 12295 }, 12296 }, 12297 "invalid hard pod anti-affinity, key exists in both MatchLabelKeys and MismatchLabelKeys": { 12298 expectedError: "exists in both matchLabelKeys and mismatchLabelKeys", 12299 spec: core.Pod{ 12300 ObjectMeta: metav1.ObjectMeta{ 12301 Name: "123", 12302 Namespace: "ns", 12303 }, 12304 Spec: validPodSpec(&core.Affinity{ 12305 PodAntiAffinity: &core.PodAntiAffinity{ 12306 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 12307 { 12308 LabelSelector: &metav1.LabelSelector{ 12309 MatchExpressions: []metav1.LabelSelectorRequirement{ 12310 { 12311 Key: "key", 12312 Operator: metav1.LabelSelectorOpNotIn, 12313 Values: []string{"value1", "value2"}, 12314 }, 12315 }, 12316 }, 12317 TopologyKey: "k8s.io/zone", 12318 MatchLabelKeys: []string{"samekey"}, 12319 MismatchLabelKeys: []string{"samekey"}, 12320 }, 12321 }, 12322 }, 12323 }), 12324 }, 12325 }, 12326 "invalid toleration key": { 12327 expectedError: "spec.tolerations[0].key", 12328 spec: core.Pod{ 12329 ObjectMeta: metav1.ObjectMeta{ 12330 Name: "123", 12331 Namespace: "ns", 12332 }, 12333 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "nospecialchars^=@", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}), 12334 }, 12335 }, 12336 "invalid toleration operator": { 12337 expectedError: "spec.tolerations[0].operator", 12338 spec: core.Pod{ 12339 ObjectMeta: metav1.ObjectMeta{ 12340 Name: "123", 12341 Namespace: "ns", 12342 }, 12343 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "In", Value: "bar", Effect: "NoSchedule"}}), 12344 }, 12345 }, 12346 "value must be empty when `operator` is 'Exists'": { 12347 expectedError: "spec.tolerations[0].operator", 12348 spec: core.Pod{ 12349 ObjectMeta: metav1.ObjectMeta{ 12350 Name: "123", 12351 Namespace: "ns", 12352 }, 12353 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "foo", Operator: "Exists", Value: "bar", Effect: "NoSchedule"}}), 12354 }, 12355 }, 12356 12357 "operator must be 'Exists' when `key` is empty": { 12358 expectedError: "spec.tolerations[0].operator", 12359 spec: core.Pod{ 12360 ObjectMeta: metav1.ObjectMeta{ 12361 Name: "123", 12362 Namespace: "ns", 12363 }, 12364 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}), 12365 }, 12366 }, 12367 "effect must be 'NoExecute' when `TolerationSeconds` is set": { 12368 expectedError: "spec.tolerations[0].effect", 12369 spec: core.Pod{ 12370 ObjectMeta: metav1.ObjectMeta{ 12371 Name: "pod-forgiveness-invalid", 12372 Namespace: "ns", 12373 }, 12374 Spec: extendPodSpecwithTolerations(validPodSpec(nil), []core.Toleration{{Key: "node.kubernetes.io/not-ready", Operator: "Exists", Effect: "NoSchedule", TolerationSeconds: &[]int64{20}[0]}}), 12375 }, 12376 }, 12377 "must be a valid pod seccomp profile": { 12378 expectedError: "must be a valid seccomp profile", 12379 spec: core.Pod{ 12380 ObjectMeta: metav1.ObjectMeta{ 12381 Name: "123", 12382 Namespace: "ns", 12383 Annotations: map[string]string{ 12384 core.SeccompPodAnnotationKey: "foo", 12385 }, 12386 }, 12387 Spec: validPodSpec(nil), 12388 }, 12389 }, 12390 "must be a valid container seccomp profile": { 12391 expectedError: "must be a valid seccomp profile", 12392 spec: core.Pod{ 12393 ObjectMeta: metav1.ObjectMeta{ 12394 Name: "123", 12395 Namespace: "ns", 12396 Annotations: map[string]string{ 12397 core.SeccompContainerAnnotationKeyPrefix + "foo": "foo", 12398 }, 12399 }, 12400 Spec: validPodSpec(nil), 12401 }, 12402 }, 12403 "must be a non-empty container name in seccomp annotation": { 12404 expectedError: "name part must be non-empty", 12405 spec: core.Pod{ 12406 ObjectMeta: metav1.ObjectMeta{ 12407 Name: "123", 12408 Namespace: "ns", 12409 Annotations: map[string]string{ 12410 core.SeccompContainerAnnotationKeyPrefix: "foo", 12411 }, 12412 }, 12413 Spec: validPodSpec(nil), 12414 }, 12415 }, 12416 "must be a non-empty container profile in seccomp annotation": { 12417 expectedError: "must be a valid seccomp profile", 12418 spec: core.Pod{ 12419 ObjectMeta: metav1.ObjectMeta{ 12420 Name: "123", 12421 Namespace: "ns", 12422 Annotations: map[string]string{ 12423 core.SeccompContainerAnnotationKeyPrefix + "foo": "", 12424 }, 12425 }, 12426 Spec: validPodSpec(nil), 12427 }, 12428 }, 12429 "must match seccomp profile type and pod annotation": { 12430 expectedError: "seccomp type in annotation and field must match", 12431 spec: core.Pod{ 12432 ObjectMeta: metav1.ObjectMeta{ 12433 Name: "123", 12434 Namespace: "ns", 12435 Annotations: map[string]string{ 12436 core.SeccompPodAnnotationKey: "unconfined", 12437 }, 12438 }, 12439 Spec: core.PodSpec{ 12440 SecurityContext: &core.PodSecurityContext{ 12441 SeccompProfile: &core.SeccompProfile{ 12442 Type: core.SeccompProfileTypeRuntimeDefault, 12443 }, 12444 }, 12445 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12446 RestartPolicy: core.RestartPolicyAlways, 12447 DNSPolicy: core.DNSClusterFirst, 12448 }, 12449 }, 12450 }, 12451 "must match seccomp profile type and container annotation": { 12452 expectedError: "seccomp type in annotation and field must match", 12453 spec: core.Pod{ 12454 ObjectMeta: metav1.ObjectMeta{ 12455 Name: "123", 12456 Namespace: "ns", 12457 Annotations: map[string]string{ 12458 core.SeccompContainerAnnotationKeyPrefix + "ctr": "unconfined", 12459 }, 12460 }, 12461 Spec: core.PodSpec{ 12462 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 12463 SecurityContext: &core.SecurityContext{ 12464 SeccompProfile: &core.SeccompProfile{ 12465 Type: core.SeccompProfileTypeRuntimeDefault, 12466 }, 12467 }}}, 12468 RestartPolicy: core.RestartPolicyAlways, 12469 DNSPolicy: core.DNSClusterFirst, 12470 }, 12471 }, 12472 }, 12473 "must be a relative path in a node-local seccomp profile annotation": { 12474 expectedError: "must be a relative path", 12475 spec: core.Pod{ 12476 ObjectMeta: metav1.ObjectMeta{ 12477 Name: "123", 12478 Namespace: "ns", 12479 Annotations: map[string]string{ 12480 core.SeccompPodAnnotationKey: "localhost//foo", 12481 }, 12482 }, 12483 Spec: validPodSpec(nil), 12484 }, 12485 }, 12486 "must not start with '../'": { 12487 expectedError: "must not contain '..'", 12488 spec: core.Pod{ 12489 ObjectMeta: metav1.ObjectMeta{ 12490 Name: "123", 12491 Namespace: "ns", 12492 Annotations: map[string]string{ 12493 core.SeccompPodAnnotationKey: "localhost/../foo", 12494 }, 12495 }, 12496 Spec: validPodSpec(nil), 12497 }, 12498 }, 12499 "AppArmor profile must apply to a container": { 12500 expectedError: "metadata.annotations[container.apparmor.security.beta.kubernetes.io/fake-ctr]", 12501 spec: core.Pod{ 12502 ObjectMeta: metav1.ObjectMeta{ 12503 Name: "123", 12504 Namespace: "ns", 12505 Annotations: map[string]string{ 12506 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "ctr": v1.DeprecatedAppArmorBetaProfileRuntimeDefault, 12507 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "init-ctr": v1.DeprecatedAppArmorBetaProfileRuntimeDefault, 12508 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "fake-ctr": v1.DeprecatedAppArmorBetaProfileRuntimeDefault, 12509 }, 12510 }, 12511 Spec: core.PodSpec{ 12512 InitContainers: []core.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12513 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12514 RestartPolicy: core.RestartPolicyAlways, 12515 DNSPolicy: core.DNSClusterFirst, 12516 }, 12517 }, 12518 }, 12519 "AppArmor profile format must be valid": { 12520 expectedError: "invalid AppArmor profile name", 12521 spec: core.Pod{ 12522 ObjectMeta: metav1.ObjectMeta{ 12523 Name: "123", 12524 Namespace: "ns", 12525 Annotations: map[string]string{ 12526 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "ctr": "bad-name", 12527 }, 12528 }, 12529 Spec: validPodSpec(nil), 12530 }, 12531 }, 12532 "only default AppArmor profile may start with runtime/": { 12533 expectedError: "invalid AppArmor profile name", 12534 spec: core.Pod{ 12535 ObjectMeta: metav1.ObjectMeta{ 12536 Name: "123", 12537 Namespace: "ns", 12538 Annotations: map[string]string{ 12539 v1.DeprecatedAppArmorBetaContainerAnnotationKeyPrefix + "ctr": "runtime/foo", 12540 }, 12541 }, 12542 Spec: validPodSpec(nil), 12543 }, 12544 }, 12545 "unsupported pod AppArmor profile type": { 12546 expectedError: `Unsupported value: "test"`, 12547 spec: core.Pod{ 12548 ObjectMeta: metav1.ObjectMeta{ 12549 Name: "123", 12550 Namespace: "ns", 12551 }, 12552 Spec: core.PodSpec{ 12553 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12554 RestartPolicy: core.RestartPolicyAlways, 12555 DNSPolicy: core.DNSDefault, 12556 SecurityContext: &core.PodSecurityContext{ 12557 AppArmorProfile: &core.AppArmorProfile{ 12558 Type: "test", 12559 }, 12560 }, 12561 }, 12562 }, 12563 }, 12564 "unsupported container AppArmor profile type": { 12565 expectedError: `Unsupported value: "test"`, 12566 spec: core.Pod{ 12567 ObjectMeta: metav1.ObjectMeta{ 12568 Name: "123", 12569 Namespace: "ns", 12570 }, 12571 Spec: core.PodSpec{ 12572 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 12573 SecurityContext: &core.SecurityContext{ 12574 AppArmorProfile: &core.AppArmorProfile{ 12575 Type: "test", 12576 }, 12577 }, 12578 }}, 12579 RestartPolicy: core.RestartPolicyAlways, 12580 DNSPolicy: core.DNSDefault, 12581 }, 12582 }, 12583 }, 12584 "missing pod AppArmor profile type": { 12585 expectedError: "Required value: type is required when appArmorProfile is set", 12586 spec: core.Pod{ 12587 ObjectMeta: metav1.ObjectMeta{ 12588 Name: "123", 12589 Namespace: "ns", 12590 }, 12591 Spec: core.PodSpec{ 12592 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12593 RestartPolicy: core.RestartPolicyAlways, 12594 DNSPolicy: core.DNSDefault, 12595 SecurityContext: &core.PodSecurityContext{ 12596 AppArmorProfile: &core.AppArmorProfile{ 12597 Type: "", 12598 }, 12599 }, 12600 }, 12601 }, 12602 }, 12603 "missing AppArmor localhost profile": { 12604 expectedError: "Required value: must be set when AppArmor type is Localhost", 12605 spec: core.Pod{ 12606 ObjectMeta: metav1.ObjectMeta{ 12607 Name: "123", 12608 Namespace: "ns", 12609 }, 12610 Spec: core.PodSpec{ 12611 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12612 RestartPolicy: core.RestartPolicyAlways, 12613 DNSPolicy: core.DNSDefault, 12614 SecurityContext: &core.PodSecurityContext{ 12615 AppArmorProfile: &core.AppArmorProfile{ 12616 Type: core.AppArmorProfileTypeLocalhost, 12617 }, 12618 }, 12619 }, 12620 }, 12621 }, 12622 "empty AppArmor localhost profile": { 12623 expectedError: "Required value: must be set when AppArmor type is Localhost", 12624 spec: core.Pod{ 12625 ObjectMeta: metav1.ObjectMeta{ 12626 Name: "123", 12627 Namespace: "ns", 12628 }, 12629 Spec: core.PodSpec{ 12630 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12631 RestartPolicy: core.RestartPolicyAlways, 12632 DNSPolicy: core.DNSDefault, 12633 SecurityContext: &core.PodSecurityContext{ 12634 AppArmorProfile: &core.AppArmorProfile{ 12635 Type: core.AppArmorProfileTypeLocalhost, 12636 LocalhostProfile: ptr.To(""), 12637 }, 12638 }, 12639 }, 12640 }, 12641 }, 12642 "invalid AppArmor localhost profile type": { 12643 expectedError: `Invalid value: "foo-bar"`, 12644 spec: core.Pod{ 12645 ObjectMeta: metav1.ObjectMeta{ 12646 Name: "123", 12647 Namespace: "ns", 12648 }, 12649 Spec: core.PodSpec{ 12650 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12651 RestartPolicy: core.RestartPolicyAlways, 12652 DNSPolicy: core.DNSDefault, 12653 SecurityContext: &core.PodSecurityContext{ 12654 AppArmorProfile: &core.AppArmorProfile{ 12655 Type: core.AppArmorProfileTypeRuntimeDefault, 12656 LocalhostProfile: ptr.To("foo-bar"), 12657 }, 12658 }, 12659 }, 12660 }, 12661 }, 12662 "invalid AppArmor localhost profile": { 12663 expectedError: `Invalid value: "foo-bar "`, 12664 spec: core.Pod{ 12665 ObjectMeta: metav1.ObjectMeta{ 12666 Name: "123", 12667 Namespace: "ns", 12668 }, 12669 Spec: core.PodSpec{ 12670 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12671 RestartPolicy: core.RestartPolicyAlways, 12672 DNSPolicy: core.DNSDefault, 12673 SecurityContext: &core.PodSecurityContext{ 12674 AppArmorProfile: &core.AppArmorProfile{ 12675 Type: core.AppArmorProfileTypeLocalhost, 12676 LocalhostProfile: ptr.To("foo-bar "), 12677 }, 12678 }, 12679 }, 12680 }, 12681 }, 12682 "too long AppArmor localhost profile": { 12683 expectedError: "Too long: may not be longer than 4095", 12684 spec: core.Pod{ 12685 ObjectMeta: metav1.ObjectMeta{ 12686 Name: "123", 12687 Namespace: "ns", 12688 }, 12689 Spec: core.PodSpec{ 12690 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12691 RestartPolicy: core.RestartPolicyAlways, 12692 DNSPolicy: core.DNSDefault, 12693 SecurityContext: &core.PodSecurityContext{ 12694 AppArmorProfile: &core.AppArmorProfile{ 12695 Type: core.AppArmorProfileTypeLocalhost, 12696 LocalhostProfile: ptr.To(strings.Repeat("a", 4096)), 12697 }, 12698 }, 12699 }, 12700 }, 12701 }, 12702 "mismatched AppArmor field and annotation types": { 12703 expectedError: "Forbidden: apparmor type in annotation and field must match", 12704 spec: core.Pod{ 12705 ObjectMeta: metav1.ObjectMeta{ 12706 Name: "123", 12707 Namespace: "ns", 12708 Annotations: map[string]string{ 12709 core.DeprecatedAppArmorAnnotationKeyPrefix + "ctr": core.DeprecatedAppArmorAnnotationValueRuntimeDefault, 12710 }, 12711 }, 12712 Spec: core.PodSpec{ 12713 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 12714 SecurityContext: &core.SecurityContext{ 12715 AppArmorProfile: &core.AppArmorProfile{ 12716 Type: core.AppArmorProfileTypeUnconfined, 12717 }, 12718 }, 12719 }}, 12720 RestartPolicy: core.RestartPolicyAlways, 12721 DNSPolicy: core.DNSDefault, 12722 }, 12723 }, 12724 }, 12725 "mismatched AppArmor pod field and annotation types": { 12726 expectedError: "Forbidden: apparmor type in annotation and field must match", 12727 spec: core.Pod{ 12728 ObjectMeta: metav1.ObjectMeta{ 12729 Name: "123", 12730 Namespace: "ns", 12731 Annotations: map[string]string{ 12732 core.DeprecatedAppArmorAnnotationKeyPrefix + "ctr": core.DeprecatedAppArmorAnnotationValueRuntimeDefault, 12733 }, 12734 }, 12735 Spec: core.PodSpec{ 12736 SecurityContext: &core.PodSecurityContext{ 12737 AppArmorProfile: &core.AppArmorProfile{ 12738 Type: core.AppArmorProfileTypeUnconfined, 12739 }, 12740 }, 12741 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12742 RestartPolicy: core.RestartPolicyAlways, 12743 DNSPolicy: core.DNSDefault, 12744 }, 12745 }, 12746 }, 12747 "mismatched AppArmor localhost profiles": { 12748 expectedError: "Forbidden: apparmor profile in annotation and field must match", 12749 spec: core.Pod{ 12750 ObjectMeta: metav1.ObjectMeta{ 12751 Name: "123", 12752 Namespace: "ns", 12753 Annotations: map[string]string{ 12754 core.DeprecatedAppArmorAnnotationKeyPrefix + "ctr": core.DeprecatedAppArmorAnnotationValueLocalhostPrefix + "foo", 12755 }, 12756 }, 12757 Spec: core.PodSpec{ 12758 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 12759 SecurityContext: &core.SecurityContext{ 12760 AppArmorProfile: &core.AppArmorProfile{ 12761 Type: core.AppArmorProfileTypeLocalhost, 12762 LocalhostProfile: ptr.To("bar"), 12763 }, 12764 }, 12765 }}, 12766 RestartPolicy: core.RestartPolicyAlways, 12767 DNSPolicy: core.DNSDefault, 12768 }, 12769 }, 12770 }, 12771 "invalid extended resource name in container request": { 12772 expectedError: "must be a standard resource for containers", 12773 spec: core.Pod{ 12774 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12775 Spec: core.PodSpec{ 12776 Containers: []core.Container{{ 12777 Name: "invalid", 12778 Image: "image", 12779 ImagePullPolicy: "IfNotPresent", 12780 Resources: core.ResourceRequirements{ 12781 Requests: core.ResourceList{ 12782 core.ResourceName("invalid-name"): resource.MustParse("2"), 12783 }, 12784 Limits: core.ResourceList{ 12785 core.ResourceName("invalid-name"): resource.MustParse("2"), 12786 }, 12787 }, 12788 }}, 12789 RestartPolicy: core.RestartPolicyAlways, 12790 DNSPolicy: core.DNSClusterFirst, 12791 }, 12792 }, 12793 }, 12794 "invalid extended resource requirement: request must be == limit": { 12795 expectedError: "must be equal to example.com/a", 12796 spec: core.Pod{ 12797 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12798 Spec: core.PodSpec{ 12799 Containers: []core.Container{{ 12800 Name: "invalid", 12801 Image: "image", 12802 ImagePullPolicy: "IfNotPresent", 12803 Resources: core.ResourceRequirements{ 12804 Requests: core.ResourceList{ 12805 core.ResourceName("example.com/a"): resource.MustParse("2"), 12806 }, 12807 Limits: core.ResourceList{ 12808 core.ResourceName("example.com/a"): resource.MustParse("1"), 12809 }, 12810 }, 12811 }}, 12812 RestartPolicy: core.RestartPolicyAlways, 12813 DNSPolicy: core.DNSClusterFirst, 12814 }, 12815 }, 12816 }, 12817 "invalid extended resource requirement without limit": { 12818 expectedError: "Limit must be set", 12819 spec: core.Pod{ 12820 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12821 Spec: core.PodSpec{ 12822 Containers: []core.Container{{ 12823 Name: "invalid", 12824 Image: "image", 12825 ImagePullPolicy: "IfNotPresent", 12826 Resources: core.ResourceRequirements{ 12827 Requests: core.ResourceList{ 12828 core.ResourceName("example.com/a"): resource.MustParse("2"), 12829 }, 12830 }, 12831 }}, 12832 RestartPolicy: core.RestartPolicyAlways, 12833 DNSPolicy: core.DNSClusterFirst, 12834 }, 12835 }, 12836 }, 12837 "invalid fractional extended resource in container request": { 12838 expectedError: "must be an integer", 12839 spec: core.Pod{ 12840 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12841 Spec: core.PodSpec{ 12842 Containers: []core.Container{{ 12843 Name: "invalid", 12844 Image: "image", 12845 ImagePullPolicy: "IfNotPresent", 12846 Resources: core.ResourceRequirements{ 12847 Requests: core.ResourceList{ 12848 core.ResourceName("example.com/a"): resource.MustParse("500m"), 12849 }, 12850 }, 12851 }}, 12852 RestartPolicy: core.RestartPolicyAlways, 12853 DNSPolicy: core.DNSClusterFirst, 12854 }, 12855 }, 12856 }, 12857 "invalid fractional extended resource in init container request": { 12858 expectedError: "must be an integer", 12859 spec: core.Pod{ 12860 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12861 Spec: core.PodSpec{ 12862 InitContainers: []core.Container{{ 12863 Name: "invalid", 12864 Image: "image", 12865 ImagePullPolicy: "IfNotPresent", 12866 Resources: core.ResourceRequirements{ 12867 Requests: core.ResourceList{ 12868 core.ResourceName("example.com/a"): resource.MustParse("500m"), 12869 }, 12870 }, 12871 }}, 12872 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12873 RestartPolicy: core.RestartPolicyAlways, 12874 DNSPolicy: core.DNSClusterFirst, 12875 }, 12876 }, 12877 }, 12878 "invalid fractional extended resource in container limit": { 12879 expectedError: "must be an integer", 12880 spec: core.Pod{ 12881 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12882 Spec: core.PodSpec{ 12883 Containers: []core.Container{{ 12884 Name: "invalid", 12885 Image: "image", 12886 ImagePullPolicy: "IfNotPresent", 12887 Resources: core.ResourceRequirements{ 12888 Requests: core.ResourceList{ 12889 core.ResourceName("example.com/a"): resource.MustParse("5"), 12890 }, 12891 Limits: core.ResourceList{ 12892 core.ResourceName("example.com/a"): resource.MustParse("2.5"), 12893 }, 12894 }, 12895 }}, 12896 RestartPolicy: core.RestartPolicyAlways, 12897 DNSPolicy: core.DNSClusterFirst, 12898 }, 12899 }, 12900 }, 12901 "invalid fractional extended resource in init container limit": { 12902 expectedError: "must be an integer", 12903 spec: core.Pod{ 12904 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12905 Spec: core.PodSpec{ 12906 InitContainers: []core.Container{{ 12907 Name: "invalid", 12908 Image: "image", 12909 ImagePullPolicy: "IfNotPresent", 12910 Resources: core.ResourceRequirements{ 12911 Requests: core.ResourceList{ 12912 core.ResourceName("example.com/a"): resource.MustParse("2.5"), 12913 }, 12914 Limits: core.ResourceList{ 12915 core.ResourceName("example.com/a"): resource.MustParse("2.5"), 12916 }, 12917 }, 12918 }}, 12919 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12920 RestartPolicy: core.RestartPolicyAlways, 12921 DNSPolicy: core.DNSClusterFirst, 12922 }, 12923 }, 12924 }, 12925 "mirror-pod present without nodeName": { 12926 expectedError: "mirror", 12927 spec: core.Pod{ 12928 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.MirrorPodAnnotationKey: ""}}, 12929 Spec: core.PodSpec{ 12930 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12931 RestartPolicy: core.RestartPolicyAlways, 12932 DNSPolicy: core.DNSClusterFirst, 12933 }, 12934 }, 12935 }, 12936 "mirror-pod populated without nodeName": { 12937 expectedError: "mirror", 12938 spec: core.Pod{ 12939 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.MirrorPodAnnotationKey: "foo"}}, 12940 Spec: core.PodSpec{ 12941 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12942 RestartPolicy: core.RestartPolicyAlways, 12943 DNSPolicy: core.DNSClusterFirst, 12944 }, 12945 }, 12946 }, 12947 "serviceaccount token projected volume with no serviceaccount name specified": { 12948 expectedError: "must not be specified when serviceAccountName is not set", 12949 spec: core.Pod{ 12950 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 12951 Spec: core.PodSpec{ 12952 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12953 RestartPolicy: core.RestartPolicyAlways, 12954 DNSPolicy: core.DNSClusterFirst, 12955 Volumes: []core.Volume{{ 12956 Name: "projected-volume", 12957 VolumeSource: core.VolumeSource{ 12958 Projected: &core.ProjectedVolumeSource{ 12959 Sources: []core.VolumeProjection{{ 12960 ServiceAccountToken: &core.ServiceAccountTokenProjection{ 12961 Audience: "foo-audience", 12962 ExpirationSeconds: 6000, 12963 Path: "foo-path", 12964 }, 12965 }}, 12966 }, 12967 }, 12968 }}, 12969 }, 12970 }, 12971 }, 12972 "ClusterTrustBundlePEM projected volume using both byName and bySigner": { 12973 expectedError: "only one of name and signerName may be used", 12974 spec: core.Pod{ 12975 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 12976 Spec: core.PodSpec{ 12977 ServiceAccountName: "some-service-account", 12978 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 12979 RestartPolicy: core.RestartPolicyAlways, 12980 DNSPolicy: core.DNSClusterFirst, 12981 Volumes: []core.Volume{ 12982 { 12983 Name: "projected-volume", 12984 VolumeSource: core.VolumeSource{ 12985 Projected: &core.ProjectedVolumeSource{ 12986 Sources: []core.VolumeProjection{ 12987 { 12988 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 12989 Path: "foo-path", 12990 SignerName: utilpointer.String("example.com/foo"), 12991 LabelSelector: &metav1.LabelSelector{ 12992 MatchLabels: map[string]string{ 12993 "version": "live", 12994 }, 12995 }, 12996 Name: utilpointer.String("foo"), 12997 }, 12998 }, 12999 }, 13000 }, 13001 }, 13002 }, 13003 }, 13004 }, 13005 }, 13006 }, 13007 "ClusterTrustBundlePEM projected volume byName with no name": { 13008 expectedError: "must be a valid object name", 13009 spec: core.Pod{ 13010 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 13011 Spec: core.PodSpec{ 13012 ServiceAccountName: "some-service-account", 13013 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13014 RestartPolicy: core.RestartPolicyAlways, 13015 DNSPolicy: core.DNSClusterFirst, 13016 Volumes: []core.Volume{ 13017 { 13018 Name: "projected-volume", 13019 VolumeSource: core.VolumeSource{ 13020 Projected: &core.ProjectedVolumeSource{ 13021 Sources: []core.VolumeProjection{ 13022 { 13023 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 13024 Path: "foo-path", 13025 Name: utilpointer.String(""), 13026 }, 13027 }, 13028 }, 13029 }, 13030 }, 13031 }, 13032 }, 13033 }, 13034 }, 13035 }, 13036 "ClusterTrustBundlePEM projected volume bySigner with no signer name": { 13037 expectedError: "must be a valid signer name", 13038 spec: core.Pod{ 13039 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 13040 Spec: core.PodSpec{ 13041 ServiceAccountName: "some-service-account", 13042 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13043 RestartPolicy: core.RestartPolicyAlways, 13044 DNSPolicy: core.DNSClusterFirst, 13045 Volumes: []core.Volume{ 13046 { 13047 Name: "projected-volume", 13048 VolumeSource: core.VolumeSource{ 13049 Projected: &core.ProjectedVolumeSource{ 13050 Sources: []core.VolumeProjection{ 13051 { 13052 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 13053 Path: "foo-path", 13054 SignerName: utilpointer.String(""), 13055 LabelSelector: &metav1.LabelSelector{ 13056 MatchLabels: map[string]string{ 13057 "foo": "bar", 13058 }, 13059 }, 13060 }, 13061 }, 13062 }, 13063 }, 13064 }, 13065 }, 13066 }, 13067 }, 13068 }, 13069 }, 13070 "ClusterTrustBundlePEM projected volume bySigner with invalid signer name": { 13071 expectedError: "must be a fully qualified domain and path of the form", 13072 spec: core.Pod{ 13073 ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, 13074 Spec: core.PodSpec{ 13075 ServiceAccountName: "some-service-account", 13076 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13077 RestartPolicy: core.RestartPolicyAlways, 13078 DNSPolicy: core.DNSClusterFirst, 13079 Volumes: []core.Volume{ 13080 { 13081 Name: "projected-volume", 13082 VolumeSource: core.VolumeSource{ 13083 Projected: &core.ProjectedVolumeSource{ 13084 Sources: []core.VolumeProjection{ 13085 { 13086 ClusterTrustBundle: &core.ClusterTrustBundleProjection{ 13087 Path: "foo-path", 13088 SignerName: utilpointer.String("example.com/foo/invalid"), 13089 }, 13090 }, 13091 }, 13092 }, 13093 }, 13094 }, 13095 }, 13096 }, 13097 }, 13098 }, 13099 "final PVC name for ephemeral volume must be valid": { 13100 expectedError: "spec.volumes[1].name: Invalid value: \"" + longVolName + "\": PVC name \"" + longPodName + "-" + longVolName + "\": must be no more than 253 characters", 13101 spec: core.Pod{ 13102 ObjectMeta: metav1.ObjectMeta{Name: longPodName, Namespace: "ns"}, 13103 Spec: core.PodSpec{ 13104 Volumes: []core.Volume{ 13105 {Name: "pvc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "my-pvc"}}}, 13106 {Name: longVolName, VolumeSource: core.VolumeSource{Ephemeral: &core.EphemeralVolumeSource{VolumeClaimTemplate: &validPVCTemplate}}}, 13107 }, 13108 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13109 RestartPolicy: core.RestartPolicyAlways, 13110 DNSPolicy: core.DNSClusterFirst, 13111 }, 13112 }, 13113 }, 13114 "PersistentVolumeClaimVolumeSource must not reference a generated PVC": { 13115 expectedError: "spec.volumes[0].persistentVolumeClaim.claimName: Invalid value: \"123-ephemeral-volume\": must not reference a PVC that gets created for an ephemeral volume", 13116 spec: core.Pod{ 13117 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, 13118 Spec: core.PodSpec{ 13119 Volumes: []core.Volume{ 13120 {Name: "pvc-volume", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "123-ephemeral-volume"}}}, 13121 {Name: "ephemeral-volume", VolumeSource: core.VolumeSource{Ephemeral: &core.EphemeralVolumeSource{VolumeClaimTemplate: &validPVCTemplate}}}, 13122 }, 13123 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13124 RestartPolicy: core.RestartPolicyAlways, 13125 DNSPolicy: core.DNSClusterFirst, 13126 }, 13127 }, 13128 }, 13129 "invalid pod-deletion-cost": { 13130 expectedError: "metadata.annotations[controller.kubernetes.io/pod-deletion-cost]: Invalid value: \"text\": must be a 32bit integer", 13131 spec: core.Pod{ 13132 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.PodDeletionCost: "text"}}, 13133 Spec: core.PodSpec{ 13134 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13135 RestartPolicy: core.RestartPolicyAlways, 13136 DNSPolicy: core.DNSClusterFirst, 13137 }, 13138 }, 13139 }, 13140 "invalid leading zeros pod-deletion-cost": { 13141 expectedError: "metadata.annotations[controller.kubernetes.io/pod-deletion-cost]: Invalid value: \"008\": must be a 32bit integer", 13142 spec: core.Pod{ 13143 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.PodDeletionCost: "008"}}, 13144 Spec: core.PodSpec{ 13145 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13146 RestartPolicy: core.RestartPolicyAlways, 13147 DNSPolicy: core.DNSClusterFirst, 13148 }, 13149 }, 13150 }, 13151 "invalid leading plus sign pod-deletion-cost": { 13152 expectedError: "metadata.annotations[controller.kubernetes.io/pod-deletion-cost]: Invalid value: \"+10\": must be a 32bit integer", 13153 spec: core.Pod{ 13154 ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns", Annotations: map[string]string{core.PodDeletionCost: "+10"}}, 13155 Spec: core.PodSpec{ 13156 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 13157 RestartPolicy: core.RestartPolicyAlways, 13158 DNSPolicy: core.DNSClusterFirst, 13159 }, 13160 }, 13161 }, 13162 } 13163 for k, v := range errorCases { 13164 t.Run(k, func(t *testing.T) { 13165 if errs := ValidatePodCreate(&v.spec, PodValidationOptions{}); len(errs) == 0 { 13166 t.Errorf("expected failure") 13167 } else if v.expectedError == "" { 13168 t.Errorf("missing expectedError, got %q", errs.ToAggregate().Error()) 13169 } else if actualError := errs.ToAggregate().Error(); !strings.Contains(actualError, v.expectedError) { 13170 t.Errorf("expected error to contain %q, got %q", v.expectedError, actualError) 13171 } 13172 }) 13173 } 13174 } 13175 13176 func TestValidatePodCreateWithSchedulingGates(t *testing.T) { 13177 applyEssentials := func(pod *core.Pod) { 13178 pod.Spec.Containers = []core.Container{ 13179 {Name: "con", Image: "pause", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, 13180 } 13181 pod.Spec.RestartPolicy = core.RestartPolicyAlways 13182 pod.Spec.DNSPolicy = core.DNSClusterFirst 13183 } 13184 fldPath := field.NewPath("spec") 13185 13186 tests := []struct { 13187 name string 13188 pod *core.Pod 13189 wantFieldErrors field.ErrorList 13190 }{{ 13191 name: "create a Pod with nodeName and schedulingGates, feature enabled", 13192 pod: &core.Pod{ 13193 ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "ns"}, 13194 Spec: core.PodSpec{ 13195 NodeName: "node", 13196 SchedulingGates: []core.PodSchedulingGate{ 13197 {Name: "foo"}, 13198 }, 13199 }, 13200 }, 13201 wantFieldErrors: []*field.Error{field.Forbidden(fldPath.Child("nodeName"), "cannot be set until all schedulingGates have been cleared")}, 13202 }, { 13203 name: "create a Pod with schedulingGates, feature enabled", 13204 pod: &core.Pod{ 13205 ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "ns"}, 13206 Spec: core.PodSpec{ 13207 SchedulingGates: []core.PodSchedulingGate{ 13208 {Name: "foo"}, 13209 }, 13210 }, 13211 }, 13212 wantFieldErrors: nil, 13213 }, 13214 } 13215 13216 for _, tt := range tests { 13217 t.Run(tt.name, func(t *testing.T) { 13218 applyEssentials(tt.pod) 13219 errs := ValidatePodCreate(tt.pod, PodValidationOptions{}) 13220 if diff := cmp.Diff(tt.wantFieldErrors, errs); diff != "" { 13221 t.Errorf("unexpected field errors (-want, +got):\n%s", diff) 13222 } 13223 }) 13224 } 13225 } 13226 13227 func TestValidatePodUpdate(t *testing.T) { 13228 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true) 13229 var ( 13230 activeDeadlineSecondsZero = int64(0) 13231 activeDeadlineSecondsNegative = int64(-30) 13232 activeDeadlineSecondsPositive = int64(30) 13233 activeDeadlineSecondsLarger = int64(31) 13234 validfsGroupChangePolicy = core.FSGroupChangeOnRootMismatch 13235 13236 now = metav1.Now() 13237 grace = int64(30) 13238 grace2 = int64(31) 13239 ) 13240 13241 tests := []struct { 13242 new core.Pod 13243 old core.Pod 13244 err string 13245 test string 13246 }{ 13247 {new: core.Pod{}, old: core.Pod{}, err: "", test: "nothing"}, { 13248 new: core.Pod{ 13249 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13250 }, 13251 old: core.Pod{ 13252 ObjectMeta: metav1.ObjectMeta{Name: "bar"}, 13253 }, 13254 err: "metadata.name", 13255 test: "ids", 13256 }, { 13257 new: core.Pod{ 13258 ObjectMeta: metav1.ObjectMeta{ 13259 Name: "foo", 13260 Labels: map[string]string{ 13261 "foo": "bar", 13262 }, 13263 }, 13264 }, 13265 old: core.Pod{ 13266 ObjectMeta: metav1.ObjectMeta{ 13267 Name: "foo", 13268 Labels: map[string]string{ 13269 "bar": "foo", 13270 }, 13271 }, 13272 }, 13273 err: "", 13274 test: "labels", 13275 }, { 13276 new: core.Pod{ 13277 ObjectMeta: metav1.ObjectMeta{ 13278 Name: "foo", 13279 Annotations: map[string]string{ 13280 "foo": "bar", 13281 }, 13282 }, 13283 }, 13284 old: core.Pod{ 13285 ObjectMeta: metav1.ObjectMeta{ 13286 Name: "foo", 13287 Annotations: map[string]string{ 13288 "bar": "foo", 13289 }, 13290 }, 13291 }, 13292 err: "", 13293 test: "annotations", 13294 }, { 13295 new: core.Pod{ 13296 ObjectMeta: metav1.ObjectMeta{ 13297 Name: "foo", 13298 }, 13299 Spec: core.PodSpec{ 13300 Containers: []core.Container{{ 13301 Image: "foo:V1", 13302 }}, 13303 }, 13304 }, 13305 old: core.Pod{ 13306 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13307 Spec: core.PodSpec{ 13308 Containers: []core.Container{{ 13309 Image: "foo:V2", 13310 }, { 13311 Image: "bar:V2", 13312 }}, 13313 }, 13314 }, 13315 err: "may not add or remove containers", 13316 test: "less containers", 13317 }, { 13318 new: core.Pod{ 13319 ObjectMeta: metav1.ObjectMeta{ 13320 Name: "foo", 13321 }, 13322 Spec: core.PodSpec{ 13323 Containers: []core.Container{{ 13324 Image: "foo:V1", 13325 }, { 13326 Image: "bar:V2", 13327 }}, 13328 }, 13329 }, 13330 old: core.Pod{ 13331 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13332 Spec: core.PodSpec{ 13333 Containers: []core.Container{{ 13334 Image: "foo:V2", 13335 }}, 13336 }, 13337 }, 13338 err: "may not add or remove containers", 13339 test: "more containers", 13340 }, { 13341 new: core.Pod{ 13342 ObjectMeta: metav1.ObjectMeta{ 13343 Name: "foo", 13344 }, 13345 Spec: core.PodSpec{ 13346 InitContainers: []core.Container{{ 13347 Image: "foo:V1", 13348 }}, 13349 }, 13350 }, 13351 old: core.Pod{ 13352 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13353 Spec: core.PodSpec{ 13354 InitContainers: []core.Container{{ 13355 Image: "foo:V2", 13356 }, { 13357 Image: "bar:V2", 13358 }}, 13359 }, 13360 }, 13361 err: "may not add or remove containers", 13362 test: "more init containers", 13363 }, { 13364 new: core.Pod{ 13365 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13366 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13367 }, 13368 old: core.Pod{ 13369 ObjectMeta: metav1.ObjectMeta{Name: "foo", DeletionTimestamp: &now}, 13370 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13371 }, 13372 err: "metadata.deletionTimestamp", 13373 test: "deletion timestamp removed", 13374 }, { 13375 new: core.Pod{ 13376 ObjectMeta: metav1.ObjectMeta{Name: "foo", DeletionTimestamp: &now}, 13377 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13378 }, 13379 old: core.Pod{ 13380 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13381 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13382 }, 13383 err: "metadata.deletionTimestamp", 13384 test: "deletion timestamp added", 13385 }, { 13386 new: core.Pod{ 13387 ObjectMeta: metav1.ObjectMeta{Name: "foo", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &grace}, 13388 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13389 }, 13390 old: core.Pod{ 13391 ObjectMeta: metav1.ObjectMeta{Name: "foo", DeletionTimestamp: &now, DeletionGracePeriodSeconds: &grace2}, 13392 Spec: core.PodSpec{Containers: []core.Container{{Image: "foo:V1"}}}, 13393 }, 13394 err: "metadata.deletionGracePeriodSeconds", 13395 test: "deletion grace period seconds changed", 13396 }, { 13397 new: core.Pod{ 13398 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13399 Spec: core.PodSpec{ 13400 Containers: []core.Container{{ 13401 Name: "container", 13402 Image: "foo:V1", 13403 TerminationMessagePolicy: "File", 13404 ImagePullPolicy: "Always", 13405 }}, 13406 }, 13407 }, 13408 old: core.Pod{ 13409 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13410 Spec: core.PodSpec{ 13411 Containers: []core.Container{{ 13412 Name: "container", 13413 Image: "foo:V2", 13414 TerminationMessagePolicy: "File", 13415 ImagePullPolicy: "Always", 13416 }}, 13417 }, 13418 }, 13419 err: "", 13420 test: "image change", 13421 }, { 13422 new: core.Pod{ 13423 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13424 Spec: core.PodSpec{ 13425 InitContainers: []core.Container{{ 13426 Name: "container", 13427 Image: "foo:V1", 13428 TerminationMessagePolicy: "File", 13429 ImagePullPolicy: "Always", 13430 }}, 13431 }, 13432 }, 13433 old: core.Pod{ 13434 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13435 Spec: core.PodSpec{ 13436 InitContainers: []core.Container{{ 13437 Name: "container", 13438 Image: "foo:V2", 13439 TerminationMessagePolicy: "File", 13440 ImagePullPolicy: "Always", 13441 }}, 13442 }, 13443 }, 13444 err: "", 13445 test: "init container image change", 13446 }, { 13447 new: core.Pod{ 13448 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13449 Spec: core.PodSpec{ 13450 Containers: []core.Container{{ 13451 Name: "container", 13452 TerminationMessagePolicy: "File", 13453 ImagePullPolicy: "Always", 13454 }}, 13455 }, 13456 }, 13457 old: core.Pod{ 13458 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13459 Spec: core.PodSpec{ 13460 Containers: []core.Container{{ 13461 Name: "container", 13462 Image: "foo:V2", 13463 TerminationMessagePolicy: "File", 13464 ImagePullPolicy: "Always", 13465 }}, 13466 }, 13467 }, 13468 err: "spec.containers[0].image", 13469 test: "image change to empty", 13470 }, { 13471 new: core.Pod{ 13472 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13473 Spec: core.PodSpec{ 13474 InitContainers: []core.Container{{ 13475 Name: "container", 13476 TerminationMessagePolicy: "File", 13477 ImagePullPolicy: "Always", 13478 }}, 13479 }, 13480 }, 13481 old: core.Pod{ 13482 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13483 Spec: core.PodSpec{ 13484 InitContainers: []core.Container{{ 13485 Name: "container", 13486 Image: "foo:V2", 13487 TerminationMessagePolicy: "File", 13488 ImagePullPolicy: "Always", 13489 }}, 13490 }, 13491 }, 13492 err: "spec.initContainers[0].image", 13493 test: "init container image change to empty", 13494 }, { 13495 new: core.Pod{ 13496 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13497 Spec: core.PodSpec{ 13498 EphemeralContainers: []core.EphemeralContainer{{ 13499 EphemeralContainerCommon: core.EphemeralContainerCommon{ 13500 Name: "ephemeral", 13501 Image: "busybox", 13502 }, 13503 }}, 13504 }, 13505 }, 13506 old: core.Pod{ 13507 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 13508 Spec: core.PodSpec{}, 13509 }, 13510 err: "Forbidden: pod updates may not change fields other than", 13511 test: "ephemeralContainer changes are not allowed via normal pod update", 13512 }, { 13513 new: core.Pod{ 13514 Spec: core.PodSpec{}, 13515 }, 13516 old: core.Pod{ 13517 Spec: core.PodSpec{}, 13518 }, 13519 err: "", 13520 test: "activeDeadlineSeconds no change, nil", 13521 }, { 13522 new: core.Pod{ 13523 Spec: core.PodSpec{ 13524 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13525 }, 13526 }, 13527 old: core.Pod{ 13528 Spec: core.PodSpec{ 13529 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13530 }, 13531 }, 13532 err: "", 13533 test: "activeDeadlineSeconds no change, set", 13534 }, { 13535 new: core.Pod{ 13536 Spec: core.PodSpec{ 13537 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13538 }, 13539 }, 13540 old: core.Pod{}, 13541 err: "", 13542 test: "activeDeadlineSeconds change to positive from nil", 13543 }, { 13544 new: core.Pod{ 13545 Spec: core.PodSpec{ 13546 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13547 }, 13548 }, 13549 old: core.Pod{ 13550 Spec: core.PodSpec{ 13551 ActiveDeadlineSeconds: &activeDeadlineSecondsLarger, 13552 }, 13553 }, 13554 err: "", 13555 test: "activeDeadlineSeconds change to smaller positive", 13556 }, { 13557 new: core.Pod{ 13558 Spec: core.PodSpec{ 13559 ActiveDeadlineSeconds: &activeDeadlineSecondsLarger, 13560 }, 13561 }, 13562 old: core.Pod{ 13563 Spec: core.PodSpec{ 13564 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13565 }, 13566 }, 13567 err: "spec.activeDeadlineSeconds", 13568 test: "activeDeadlineSeconds change to larger positive", 13569 }, 13570 13571 { 13572 new: core.Pod{ 13573 Spec: core.PodSpec{ 13574 ActiveDeadlineSeconds: &activeDeadlineSecondsNegative, 13575 }, 13576 }, 13577 old: core.Pod{}, 13578 err: "spec.activeDeadlineSeconds", 13579 test: "activeDeadlineSeconds change to negative from nil", 13580 }, { 13581 new: core.Pod{ 13582 Spec: core.PodSpec{ 13583 ActiveDeadlineSeconds: &activeDeadlineSecondsNegative, 13584 }, 13585 }, 13586 old: core.Pod{ 13587 Spec: core.PodSpec{ 13588 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13589 }, 13590 }, 13591 err: "spec.activeDeadlineSeconds", 13592 test: "activeDeadlineSeconds change to negative from positive", 13593 }, { 13594 new: core.Pod{ 13595 Spec: core.PodSpec{ 13596 ActiveDeadlineSeconds: &activeDeadlineSecondsZero, 13597 }, 13598 }, 13599 old: core.Pod{ 13600 Spec: core.PodSpec{ 13601 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13602 }, 13603 }, 13604 err: "spec.activeDeadlineSeconds", 13605 test: "activeDeadlineSeconds change to zero from positive", 13606 }, { 13607 new: core.Pod{ 13608 Spec: core.PodSpec{ 13609 ActiveDeadlineSeconds: &activeDeadlineSecondsZero, 13610 }, 13611 }, 13612 old: core.Pod{}, 13613 err: "spec.activeDeadlineSeconds", 13614 test: "activeDeadlineSeconds change to zero from nil", 13615 }, { 13616 new: core.Pod{}, 13617 old: core.Pod{ 13618 Spec: core.PodSpec{ 13619 ActiveDeadlineSeconds: &activeDeadlineSecondsPositive, 13620 }, 13621 }, 13622 err: "spec.activeDeadlineSeconds", 13623 test: "activeDeadlineSeconds change to nil from positive", 13624 }, { 13625 new: core.Pod{ 13626 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13627 Spec: core.PodSpec{ 13628 Containers: []core.Container{{ 13629 Name: "container", 13630 TerminationMessagePolicy: "File", 13631 ImagePullPolicy: "Always", 13632 Image: "foo:V2", 13633 Resources: core.ResourceRequirements{ 13634 Limits: getResources("200m", "0", "1Gi"), 13635 }, 13636 }}, 13637 }, 13638 }, 13639 old: core.Pod{ 13640 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13641 Spec: core.PodSpec{ 13642 Containers: []core.Container{{ 13643 Name: "container", 13644 TerminationMessagePolicy: "File", 13645 ImagePullPolicy: "Always", 13646 Image: "foo:V2", 13647 Resources: core.ResourceRequirements{ 13648 Limits: getResources("100m", "0", "1Gi"), 13649 }, 13650 }}, 13651 }, 13652 }, 13653 err: "", 13654 test: "cpu limit change", 13655 }, { 13656 new: core.Pod{ 13657 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13658 Spec: core.PodSpec{ 13659 Containers: []core.Container{{ 13660 Name: "container", 13661 TerminationMessagePolicy: "File", 13662 ImagePullPolicy: "Always", 13663 Image: "foo:V1", 13664 Resources: core.ResourceRequirements{ 13665 Limits: getResourceLimits("100m", "100Mi"), 13666 }, 13667 }}, 13668 }, 13669 }, 13670 old: core.Pod{ 13671 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13672 Spec: core.PodSpec{ 13673 Containers: []core.Container{{ 13674 Name: "container", 13675 TerminationMessagePolicy: "File", 13676 ImagePullPolicy: "Always", 13677 Image: "foo:V2", 13678 Resources: core.ResourceRequirements{ 13679 Limits: getResourceLimits("100m", "200Mi"), 13680 }, 13681 }}, 13682 }, 13683 }, 13684 err: "", 13685 test: "memory limit change", 13686 }, { 13687 new: core.Pod{ 13688 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13689 Spec: core.PodSpec{ 13690 Containers: []core.Container{{ 13691 Name: "container", 13692 TerminationMessagePolicy: "File", 13693 ImagePullPolicy: "Always", 13694 Image: "foo:V1", 13695 Resources: core.ResourceRequirements{ 13696 Limits: getResources("100m", "100Mi", "1Gi"), 13697 }, 13698 }}, 13699 }, 13700 }, 13701 old: core.Pod{ 13702 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13703 Spec: core.PodSpec{ 13704 Containers: []core.Container{{ 13705 Name: "container", 13706 TerminationMessagePolicy: "File", 13707 ImagePullPolicy: "Always", 13708 Image: "foo:V2", 13709 Resources: core.ResourceRequirements{ 13710 Limits: getResources("100m", "100Mi", "2Gi"), 13711 }, 13712 }}, 13713 }, 13714 }, 13715 err: "Forbidden: pod updates may not change fields other than", 13716 test: "storage limit change", 13717 }, { 13718 new: core.Pod{ 13719 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13720 Spec: core.PodSpec{ 13721 Containers: []core.Container{{ 13722 Name: "container", 13723 TerminationMessagePolicy: "File", 13724 ImagePullPolicy: "Always", 13725 Image: "foo:V1", 13726 Resources: core.ResourceRequirements{ 13727 Requests: getResourceLimits("100m", "0"), 13728 }, 13729 }}, 13730 }, 13731 }, 13732 old: core.Pod{ 13733 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13734 Spec: core.PodSpec{ 13735 Containers: []core.Container{{ 13736 Name: "container", 13737 TerminationMessagePolicy: "File", 13738 ImagePullPolicy: "Always", 13739 Image: "foo:V2", 13740 Resources: core.ResourceRequirements{ 13741 Requests: getResourceLimits("200m", "0"), 13742 }, 13743 }}, 13744 }, 13745 }, 13746 err: "", 13747 test: "cpu request change", 13748 }, { 13749 new: core.Pod{ 13750 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13751 Spec: core.PodSpec{ 13752 Containers: []core.Container{{ 13753 Name: "container", 13754 TerminationMessagePolicy: "File", 13755 ImagePullPolicy: "Always", 13756 Image: "foo:V1", 13757 Resources: core.ResourceRequirements{ 13758 Requests: getResourceLimits("0", "200Mi"), 13759 }, 13760 }}, 13761 }, 13762 }, 13763 old: core.Pod{ 13764 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13765 Spec: core.PodSpec{ 13766 Containers: []core.Container{{ 13767 Name: "container", 13768 TerminationMessagePolicy: "File", 13769 ImagePullPolicy: "Always", 13770 Image: "foo:V2", 13771 Resources: core.ResourceRequirements{ 13772 Requests: getResourceLimits("0", "100Mi"), 13773 }, 13774 }}, 13775 }, 13776 }, 13777 err: "", 13778 test: "memory request change", 13779 }, { 13780 new: core.Pod{ 13781 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13782 Spec: core.PodSpec{ 13783 Containers: []core.Container{{ 13784 Name: "container", 13785 TerminationMessagePolicy: "File", 13786 ImagePullPolicy: "Always", 13787 Image: "foo:V1", 13788 Resources: core.ResourceRequirements{ 13789 Requests: getResources("100m", "0", "2Gi"), 13790 }, 13791 }}, 13792 }, 13793 }, 13794 old: core.Pod{ 13795 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13796 Spec: core.PodSpec{ 13797 Containers: []core.Container{{ 13798 Name: "container", 13799 TerminationMessagePolicy: "File", 13800 ImagePullPolicy: "Always", 13801 Image: "foo:V2", 13802 Resources: core.ResourceRequirements{ 13803 Requests: getResources("100m", "0", "1Gi"), 13804 }, 13805 }}, 13806 }, 13807 }, 13808 err: "Forbidden: pod updates may not change fields other than", 13809 test: "storage request change", 13810 }, { 13811 new: core.Pod{ 13812 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13813 Spec: core.PodSpec{ 13814 Containers: []core.Container{{ 13815 Name: "container", 13816 TerminationMessagePolicy: "File", 13817 ImagePullPolicy: "Always", 13818 Image: "foo:V1", 13819 Resources: core.ResourceRequirements{ 13820 Limits: getResources("200m", "400Mi", "1Gi"), 13821 Requests: getResources("200m", "400Mi", "1Gi"), 13822 }, 13823 }}, 13824 }, 13825 }, 13826 old: core.Pod{ 13827 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13828 Spec: core.PodSpec{ 13829 Containers: []core.Container{{ 13830 Name: "container", 13831 TerminationMessagePolicy: "File", 13832 ImagePullPolicy: "Always", 13833 Image: "foo:V1", 13834 Resources: core.ResourceRequirements{ 13835 Limits: getResources("100m", "100Mi", "1Gi"), 13836 Requests: getResources("100m", "100Mi", "1Gi"), 13837 }, 13838 }}, 13839 }, 13840 }, 13841 err: "", 13842 test: "Pod QoS unchanged, guaranteed -> guaranteed", 13843 }, { 13844 new: core.Pod{ 13845 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13846 Spec: core.PodSpec{ 13847 Containers: []core.Container{{ 13848 Name: "container", 13849 TerminationMessagePolicy: "File", 13850 ImagePullPolicy: "Always", 13851 Image: "foo:V1", 13852 Resources: core.ResourceRequirements{ 13853 Limits: getResources("200m", "200Mi", "2Gi"), 13854 Requests: getResources("100m", "100Mi", "1Gi"), 13855 }, 13856 }}, 13857 }, 13858 }, 13859 old: core.Pod{ 13860 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13861 Spec: core.PodSpec{ 13862 Containers: []core.Container{{ 13863 Name: "container", 13864 TerminationMessagePolicy: "File", 13865 ImagePullPolicy: "Always", 13866 Image: "foo:V1", 13867 Resources: core.ResourceRequirements{ 13868 Limits: getResources("400m", "400Mi", "2Gi"), 13869 Requests: getResources("200m", "200Mi", "1Gi"), 13870 }, 13871 }}, 13872 }, 13873 }, 13874 err: "", 13875 test: "Pod QoS unchanged, burstable -> burstable", 13876 }, { 13877 new: core.Pod{ 13878 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13879 Spec: core.PodSpec{ 13880 Containers: []core.Container{{ 13881 Name: "container", 13882 TerminationMessagePolicy: "File", 13883 ImagePullPolicy: "Always", 13884 Image: "foo:V2", 13885 Resources: core.ResourceRequirements{ 13886 Limits: getResourceLimits("200m", "200Mi"), 13887 Requests: getResourceLimits("100m", "100Mi"), 13888 }, 13889 }}, 13890 }, 13891 }, 13892 old: core.Pod{ 13893 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13894 Spec: core.PodSpec{ 13895 Containers: []core.Container{{ 13896 Name: "container", 13897 TerminationMessagePolicy: "File", 13898 ImagePullPolicy: "Always", 13899 Image: "foo:V2", 13900 Resources: core.ResourceRequirements{ 13901 Requests: getResourceLimits("100m", "100Mi"), 13902 }, 13903 }}, 13904 }, 13905 }, 13906 err: "", 13907 test: "Pod QoS unchanged, burstable -> burstable, add limits", 13908 }, { 13909 new: core.Pod{ 13910 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13911 Spec: core.PodSpec{ 13912 Containers: []core.Container{{ 13913 Name: "container", 13914 TerminationMessagePolicy: "File", 13915 ImagePullPolicy: "Always", 13916 Image: "foo:V2", 13917 Resources: core.ResourceRequirements{ 13918 Requests: getResourceLimits("100m", "100Mi"), 13919 }, 13920 }}, 13921 }, 13922 }, 13923 old: core.Pod{ 13924 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13925 Spec: core.PodSpec{ 13926 Containers: []core.Container{{ 13927 Name: "container", 13928 TerminationMessagePolicy: "File", 13929 ImagePullPolicy: "Always", 13930 Image: "foo:V2", 13931 Resources: core.ResourceRequirements{ 13932 Limits: getResourceLimits("200m", "200Mi"), 13933 Requests: getResourceLimits("100m", "100Mi"), 13934 }, 13935 }}, 13936 }, 13937 }, 13938 err: "", 13939 test: "Pod QoS unchanged, burstable -> burstable, remove limits", 13940 }, { 13941 new: core.Pod{ 13942 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13943 Spec: core.PodSpec{ 13944 Containers: []core.Container{{ 13945 Name: "container", 13946 TerminationMessagePolicy: "File", 13947 ImagePullPolicy: "Always", 13948 Image: "foo:V2", 13949 Resources: core.ResourceRequirements{ 13950 Limits: getResources("400m", "", "1Gi"), 13951 Requests: getResources("300m", "", "1Gi"), 13952 }, 13953 }}, 13954 }, 13955 }, 13956 old: core.Pod{ 13957 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13958 Spec: core.PodSpec{ 13959 Containers: []core.Container{{ 13960 Name: "container", 13961 TerminationMessagePolicy: "File", 13962 ImagePullPolicy: "Always", 13963 Image: "foo:V2", 13964 Resources: core.ResourceRequirements{ 13965 Limits: getResources("200m", "500Mi", "1Gi"), 13966 }, 13967 }}, 13968 }, 13969 }, 13970 err: "", 13971 test: "Pod QoS unchanged, burstable -> burstable, add requests", 13972 }, { 13973 new: core.Pod{ 13974 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13975 Spec: core.PodSpec{ 13976 Containers: []core.Container{{ 13977 Name: "container", 13978 TerminationMessagePolicy: "File", 13979 ImagePullPolicy: "Always", 13980 Image: "foo:V2", 13981 Resources: core.ResourceRequirements{ 13982 Limits: getResources("400m", "500Mi", "2Gi"), 13983 }, 13984 }}, 13985 }, 13986 }, 13987 old: core.Pod{ 13988 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 13989 Spec: core.PodSpec{ 13990 Containers: []core.Container{{ 13991 Name: "container", 13992 TerminationMessagePolicy: "File", 13993 ImagePullPolicy: "Always", 13994 Image: "foo:V2", 13995 Resources: core.ResourceRequirements{ 13996 Limits: getResources("200m", "300Mi", "2Gi"), 13997 Requests: getResourceLimits("100m", "200Mi"), 13998 }, 13999 }}, 14000 }, 14001 }, 14002 err: "", 14003 test: "Pod QoS unchanged, burstable -> burstable, remove requests", 14004 }, { 14005 new: core.Pod{ 14006 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14007 Spec: core.PodSpec{ 14008 Containers: []core.Container{{ 14009 Name: "container", 14010 TerminationMessagePolicy: "File", 14011 ImagePullPolicy: "Always", 14012 Image: "foo:V2", 14013 Resources: core.ResourceRequirements{ 14014 Limits: getResourceLimits("200m", "200Mi"), 14015 Requests: getResourceLimits("100m", "100Mi"), 14016 }, 14017 }}, 14018 }, 14019 }, 14020 old: core.Pod{ 14021 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14022 Spec: core.PodSpec{ 14023 Containers: []core.Container{{ 14024 Name: "container", 14025 TerminationMessagePolicy: "File", 14026 ImagePullPolicy: "Always", 14027 Image: "foo:V2", 14028 Resources: core.ResourceRequirements{ 14029 Limits: getResourceLimits("100m", "100Mi"), 14030 Requests: getResourceLimits("100m", "100Mi"), 14031 }, 14032 }}, 14033 }, 14034 }, 14035 err: "Pod QoS is immutable", 14036 test: "Pod QoS change, guaranteed -> burstable", 14037 }, { 14038 new: core.Pod{ 14039 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14040 Spec: core.PodSpec{ 14041 Containers: []core.Container{{ 14042 Name: "container", 14043 TerminationMessagePolicy: "File", 14044 ImagePullPolicy: "Always", 14045 Image: "foo:V2", 14046 Resources: core.ResourceRequirements{ 14047 Limits: getResourceLimits("100m", "100Mi"), 14048 Requests: getResourceLimits("100m", "100Mi"), 14049 }, 14050 }}, 14051 }, 14052 }, 14053 old: core.Pod{ 14054 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14055 Spec: core.PodSpec{ 14056 Containers: []core.Container{{ 14057 Name: "container", 14058 TerminationMessagePolicy: "File", 14059 ImagePullPolicy: "Always", 14060 Image: "foo:V2", 14061 Resources: core.ResourceRequirements{ 14062 Requests: getResourceLimits("100m", "100Mi"), 14063 }, 14064 }}, 14065 }, 14066 }, 14067 err: "Pod QoS is immutable", 14068 test: "Pod QoS change, burstable -> guaranteed", 14069 }, { 14070 new: core.Pod{ 14071 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14072 Spec: core.PodSpec{ 14073 Containers: []core.Container{{ 14074 Name: "container", 14075 TerminationMessagePolicy: "File", 14076 ImagePullPolicy: "Always", 14077 Image: "foo:V2", 14078 Resources: core.ResourceRequirements{ 14079 Limits: getResourceLimits("200m", "200Mi"), 14080 Requests: getResourceLimits("100m", "100Mi"), 14081 }, 14082 }}, 14083 }, 14084 }, 14085 old: core.Pod{ 14086 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14087 Spec: core.PodSpec{ 14088 Containers: []core.Container{{ 14089 Name: "container", 14090 TerminationMessagePolicy: "File", 14091 ImagePullPolicy: "Always", 14092 Image: "foo:V2", 14093 }}, 14094 }, 14095 }, 14096 err: "Pod QoS is immutable", 14097 test: "Pod QoS change, besteffort -> burstable", 14098 }, { 14099 new: core.Pod{ 14100 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14101 Spec: core.PodSpec{ 14102 Containers: []core.Container{{ 14103 Name: "container", 14104 TerminationMessagePolicy: "File", 14105 ImagePullPolicy: "Always", 14106 Image: "foo:V2", 14107 }}, 14108 }, 14109 }, 14110 old: core.Pod{ 14111 ObjectMeta: metav1.ObjectMeta{Name: "pod"}, 14112 Spec: core.PodSpec{ 14113 Containers: []core.Container{{ 14114 Name: "container", 14115 TerminationMessagePolicy: "File", 14116 ImagePullPolicy: "Always", 14117 Image: "foo:V2", 14118 Resources: core.ResourceRequirements{ 14119 Limits: getResourceLimits("200m", "200Mi"), 14120 Requests: getResourceLimits("100m", "100Mi"), 14121 }, 14122 }}, 14123 }, 14124 }, 14125 err: "Pod QoS is immutable", 14126 test: "Pod QoS change, burstable -> besteffort", 14127 }, { 14128 new: core.Pod{ 14129 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 14130 Spec: core.PodSpec{ 14131 Containers: []core.Container{{ 14132 Image: "foo:V1", 14133 }}, 14134 SecurityContext: &core.PodSecurityContext{ 14135 FSGroupChangePolicy: &validfsGroupChangePolicy, 14136 }, 14137 }, 14138 }, 14139 old: core.Pod{ 14140 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 14141 Spec: core.PodSpec{ 14142 Containers: []core.Container{{ 14143 Image: "foo:V2", 14144 }}, 14145 SecurityContext: &core.PodSecurityContext{ 14146 FSGroupChangePolicy: nil, 14147 }, 14148 }, 14149 }, 14150 err: "spec: Forbidden: pod updates may not change fields", 14151 test: "fsGroupChangePolicy change", 14152 }, { 14153 new: core.Pod{ 14154 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 14155 Spec: core.PodSpec{ 14156 Containers: []core.Container{{ 14157 Image: "foo:V1", 14158 Ports: []core.ContainerPort{ 14159 {HostPort: 8080, ContainerPort: 80}, 14160 }, 14161 }}, 14162 }, 14163 }, 14164 old: core.Pod{ 14165 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, 14166 Spec: core.PodSpec{ 14167 Containers: []core.Container{{ 14168 Image: "foo:V2", 14169 Ports: []core.ContainerPort{ 14170 {HostPort: 8000, ContainerPort: 80}, 14171 }, 14172 }}, 14173 }, 14174 }, 14175 err: "spec: Forbidden: pod updates may not change fields", 14176 test: "port change", 14177 }, { 14178 new: core.Pod{ 14179 ObjectMeta: metav1.ObjectMeta{ 14180 Name: "foo", 14181 Labels: map[string]string{ 14182 "foo": "bar", 14183 }, 14184 }, 14185 }, 14186 old: core.Pod{ 14187 ObjectMeta: metav1.ObjectMeta{ 14188 Name: "foo", 14189 Labels: map[string]string{ 14190 "Bar": "foo", 14191 }, 14192 }, 14193 }, 14194 err: "", 14195 test: "bad label change", 14196 }, { 14197 new: core.Pod{ 14198 ObjectMeta: metav1.ObjectMeta{ 14199 Name: "foo", 14200 }, 14201 Spec: core.PodSpec{ 14202 NodeName: "node1", 14203 Tolerations: []core.Toleration{{Key: "key1", Value: "value2"}}, 14204 }, 14205 }, 14206 old: core.Pod{ 14207 ObjectMeta: metav1.ObjectMeta{ 14208 Name: "foo", 14209 }, 14210 Spec: core.PodSpec{ 14211 NodeName: "node1", 14212 Tolerations: []core.Toleration{{Key: "key1", Value: "value1"}}, 14213 }, 14214 }, 14215 err: "spec.tolerations: Forbidden", 14216 test: "existing toleration value modified in pod spec updates", 14217 }, { 14218 new: core.Pod{ 14219 ObjectMeta: metav1.ObjectMeta{ 14220 Name: "foo", 14221 }, 14222 Spec: core.PodSpec{ 14223 NodeName: "node1", 14224 Tolerations: []core.Toleration{{Key: "key1", Value: "value2", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: nil}}, 14225 }, 14226 }, 14227 old: core.Pod{ 14228 ObjectMeta: metav1.ObjectMeta{ 14229 Name: "foo", 14230 }, 14231 Spec: core.PodSpec{ 14232 NodeName: "node1", 14233 Tolerations: []core.Toleration{{Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{10}[0]}}, 14234 }, 14235 }, 14236 err: "spec.tolerations: Forbidden", 14237 test: "existing toleration value modified in pod spec updates with modified tolerationSeconds", 14238 }, { 14239 new: core.Pod{ 14240 ObjectMeta: metav1.ObjectMeta{ 14241 Name: "foo", 14242 }, 14243 Spec: core.PodSpec{ 14244 NodeName: "node1", 14245 Tolerations: []core.Toleration{{Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{10}[0]}}, 14246 }, 14247 }, 14248 old: core.Pod{ 14249 ObjectMeta: metav1.ObjectMeta{ 14250 Name: "foo", 14251 }, 14252 Spec: core.PodSpec{ 14253 NodeName: "node1", 14254 Tolerations: []core.Toleration{{Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{20}[0]}}, 14255 }}, 14256 err: "", 14257 test: "modified tolerationSeconds in existing toleration value in pod spec updates", 14258 }, { 14259 new: core.Pod{ 14260 ObjectMeta: metav1.ObjectMeta{ 14261 Name: "foo", 14262 }, 14263 Spec: core.PodSpec{ 14264 Tolerations: []core.Toleration{{Key: "key1", Value: "value2"}}, 14265 }, 14266 }, 14267 old: core.Pod{ 14268 ObjectMeta: metav1.ObjectMeta{ 14269 Name: "foo", 14270 }, 14271 Spec: core.PodSpec{ 14272 NodeName: "", 14273 Tolerations: []core.Toleration{{Key: "key1", Value: "value1"}}, 14274 }, 14275 }, 14276 err: "spec.tolerations: Forbidden", 14277 test: "toleration modified in updates to an unscheduled pod", 14278 }, { 14279 new: core.Pod{ 14280 ObjectMeta: metav1.ObjectMeta{ 14281 Name: "foo", 14282 }, 14283 Spec: core.PodSpec{ 14284 NodeName: "node1", 14285 Tolerations: []core.Toleration{{Key: "key1", Value: "value1"}}, 14286 }, 14287 }, 14288 old: core.Pod{ 14289 ObjectMeta: metav1.ObjectMeta{ 14290 Name: "foo", 14291 }, 14292 Spec: core.PodSpec{ 14293 NodeName: "node1", 14294 Tolerations: []core.Toleration{{Key: "key1", Value: "value1"}}, 14295 }, 14296 }, 14297 err: "", 14298 test: "tolerations unmodified in updates to a scheduled pod", 14299 }, { 14300 new: core.Pod{ 14301 ObjectMeta: metav1.ObjectMeta{ 14302 Name: "foo", 14303 }, 14304 Spec: core.PodSpec{ 14305 NodeName: "node1", 14306 Tolerations: []core.Toleration{ 14307 {Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{20}[0]}, 14308 {Key: "key2", Value: "value2", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{30}[0]}, 14309 }, 14310 }}, 14311 old: core.Pod{ 14312 ObjectMeta: metav1.ObjectMeta{ 14313 Name: "foo", 14314 }, 14315 Spec: core.PodSpec{ 14316 NodeName: "node1", 14317 Tolerations: []core.Toleration{{Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{10}[0]}}, 14318 }, 14319 }, 14320 err: "", 14321 test: "added valid new toleration to existing tolerations in pod spec updates", 14322 }, { 14323 new: core.Pod{ 14324 ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: core.PodSpec{ 14325 NodeName: "node1", 14326 Tolerations: []core.Toleration{ 14327 {Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{20}[0]}, 14328 {Key: "key2", Value: "value2", Operator: "Equal", Effect: "NoSchedule", TolerationSeconds: &[]int64{30}[0]}, 14329 }, 14330 }}, 14331 old: core.Pod{ 14332 ObjectMeta: metav1.ObjectMeta{ 14333 Name: "foo", 14334 }, 14335 Spec: core.PodSpec{ 14336 NodeName: "node1", Tolerations: []core.Toleration{{Key: "key1", Value: "value1", Operator: "Equal", Effect: "NoExecute", TolerationSeconds: &[]int64{10}[0]}}, 14337 }}, 14338 err: "spec.tolerations[1].effect", 14339 test: "added invalid new toleration to existing tolerations in pod spec updates", 14340 }, { 14341 new: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: core.PodSpec{NodeName: "foo"}}, 14342 old: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, 14343 err: "spec: Forbidden: pod updates may not change fields", 14344 test: "removed nodeName from pod spec", 14345 }, { 14346 new: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{core.MirrorPodAnnotationKey: ""}}, Spec: core.PodSpec{NodeName: "foo"}}, 14347 old: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: core.PodSpec{NodeName: "foo"}}, 14348 err: "metadata.annotations[kubernetes.io/config.mirror]", 14349 test: "added mirror pod annotation", 14350 }, { 14351 new: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: core.PodSpec{NodeName: "foo"}}, 14352 old: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{core.MirrorPodAnnotationKey: ""}}, Spec: core.PodSpec{NodeName: "foo"}}, 14353 err: "metadata.annotations[kubernetes.io/config.mirror]", 14354 test: "removed mirror pod annotation", 14355 }, { 14356 new: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{core.MirrorPodAnnotationKey: "foo"}}, Spec: core.PodSpec{NodeName: "foo"}}, 14357 old: core.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{core.MirrorPodAnnotationKey: "bar"}}, Spec: core.PodSpec{NodeName: "foo"}}, 14358 err: "metadata.annotations[kubernetes.io/config.mirror]", 14359 test: "changed mirror pod annotation", 14360 }, { 14361 new: core.Pod{ 14362 ObjectMeta: metav1.ObjectMeta{ 14363 Name: "foo", 14364 }, 14365 Spec: core.PodSpec{ 14366 NodeName: "node1", 14367 PriorityClassName: "bar-priority", 14368 }, 14369 }, 14370 old: core.Pod{ 14371 ObjectMeta: metav1.ObjectMeta{ 14372 Name: "foo", 14373 }, 14374 Spec: core.PodSpec{ 14375 NodeName: "node1", 14376 PriorityClassName: "foo-priority", 14377 }, 14378 }, 14379 err: "spec: Forbidden: pod updates", 14380 test: "changed priority class name", 14381 }, { 14382 new: core.Pod{ 14383 ObjectMeta: metav1.ObjectMeta{ 14384 Name: "foo", 14385 }, 14386 Spec: core.PodSpec{ 14387 NodeName: "node1", 14388 PriorityClassName: "", 14389 }, 14390 }, 14391 old: core.Pod{ 14392 ObjectMeta: metav1.ObjectMeta{ 14393 Name: "foo", 14394 }, 14395 Spec: core.PodSpec{ 14396 NodeName: "node1", 14397 PriorityClassName: "foo-priority", 14398 }, 14399 }, 14400 err: "spec: Forbidden: pod updates", 14401 test: "removed priority class name", 14402 }, { 14403 new: core.Pod{ 14404 ObjectMeta: metav1.ObjectMeta{ 14405 Name: "foo", 14406 }, 14407 Spec: core.PodSpec{ 14408 TerminationGracePeriodSeconds: utilpointer.Int64(1), 14409 }, 14410 }, 14411 old: core.Pod{ 14412 ObjectMeta: metav1.ObjectMeta{ 14413 Name: "foo", 14414 }, 14415 Spec: core.PodSpec{ 14416 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 14417 }, 14418 }, 14419 err: "", 14420 test: "update termination grace period seconds", 14421 }, { 14422 new: core.Pod{ 14423 ObjectMeta: metav1.ObjectMeta{ 14424 Name: "foo", 14425 }, 14426 Spec: core.PodSpec{ 14427 TerminationGracePeriodSeconds: utilpointer.Int64(0), 14428 }, 14429 }, 14430 old: core.Pod{ 14431 ObjectMeta: metav1.ObjectMeta{ 14432 Name: "foo", 14433 }, 14434 Spec: core.PodSpec{ 14435 TerminationGracePeriodSeconds: utilpointer.Int64(-1), 14436 }, 14437 }, 14438 err: "spec: Forbidden: pod updates", 14439 test: "update termination grace period seconds not 1", 14440 }, { 14441 new: core.Pod{ 14442 ObjectMeta: metav1.ObjectMeta{ 14443 Name: "foo", 14444 }, 14445 Spec: core.PodSpec{ 14446 OS: &core.PodOS{Name: core.Windows}, 14447 SecurityContext: &core.PodSecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummy"}}, 14448 }, 14449 }, 14450 old: core.Pod{ 14451 ObjectMeta: metav1.ObjectMeta{ 14452 Name: "foo", 14453 }, 14454 Spec: core.PodSpec{ 14455 OS: &core.PodOS{Name: core.Linux}, 14456 SecurityContext: &core.PodSecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummy"}}, 14457 }, 14458 }, 14459 err: "Forbidden: pod updates may not change fields other than `spec.containers[*].image", 14460 test: "pod OS changing from Linux to Windows, IdentifyPodOS featuregate set", 14461 }, { 14462 new: core.Pod{ 14463 ObjectMeta: metav1.ObjectMeta{ 14464 Name: "foo", 14465 }, 14466 Spec: core.PodSpec{ 14467 OS: &core.PodOS{Name: core.Windows}, 14468 SecurityContext: &core.PodSecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummy"}}, 14469 }, 14470 }, 14471 old: core.Pod{ 14472 ObjectMeta: metav1.ObjectMeta{ 14473 Name: "foo", 14474 }, 14475 Spec: core.PodSpec{ 14476 OS: &core.PodOS{Name: core.Linux}, 14477 SecurityContext: &core.PodSecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummy"}}, 14478 }, 14479 }, 14480 err: "spec.securityContext.seLinuxOptions: Forbidden", 14481 test: "pod OS changing from Linux to Windows, IdentifyPodOS featuregate set, we'd get SELinux errors as well", 14482 }, { 14483 new: core.Pod{ 14484 ObjectMeta: metav1.ObjectMeta{ 14485 Name: "foo", 14486 }, 14487 Spec: core.PodSpec{ 14488 OS: &core.PodOS{Name: "dummy"}, 14489 }, 14490 }, 14491 old: core.Pod{ 14492 ObjectMeta: metav1.ObjectMeta{ 14493 Name: "foo", 14494 }, 14495 Spec: core.PodSpec{}, 14496 }, 14497 err: "Forbidden: pod updates may not change fields other than `spec.containers[*].image", 14498 test: "invalid PodOS update, IdentifyPodOS featuregate set", 14499 }, { 14500 new: core.Pod{ 14501 ObjectMeta: metav1.ObjectMeta{ 14502 Name: "foo", 14503 }, 14504 Spec: core.PodSpec{ 14505 OS: &core.PodOS{Name: core.Linux}, 14506 }, 14507 }, 14508 old: core.Pod{ 14509 ObjectMeta: metav1.ObjectMeta{ 14510 Name: "foo", 14511 }, 14512 Spec: core.PodSpec{ 14513 OS: &core.PodOS{Name: core.Windows}, 14514 }, 14515 }, 14516 err: "Forbidden: pod updates may not change fields other than ", 14517 test: "update pod spec OS to a valid value, featuregate disabled", 14518 }, { 14519 new: core.Pod{ 14520 Spec: core.PodSpec{ 14521 SchedulingGates: []core.PodSchedulingGate{{Name: "foo"}}, 14522 }, 14523 }, 14524 old: core.Pod{}, 14525 err: "Forbidden: only deletion is allowed, but found new scheduling gate 'foo'", 14526 test: "update pod spec schedulingGates: add new scheduling gate", 14527 }, { 14528 new: core.Pod{ 14529 Spec: core.PodSpec{ 14530 SchedulingGates: []core.PodSchedulingGate{{Name: "bar"}}, 14531 }, 14532 }, 14533 old: core.Pod{ 14534 Spec: core.PodSpec{ 14535 SchedulingGates: []core.PodSchedulingGate{{Name: "foo"}}, 14536 }, 14537 }, 14538 err: "Forbidden: only deletion is allowed, but found new scheduling gate 'bar'", 14539 test: "update pod spec schedulingGates: mutating an existing scheduling gate", 14540 }, { 14541 new: core.Pod{ 14542 Spec: core.PodSpec{ 14543 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14544 }, 14545 }, 14546 old: core.Pod{ 14547 Spec: core.PodSpec{ 14548 SchedulingGates: []core.PodSchedulingGate{{Name: "foo"}, {Name: "bar"}}, 14549 }, 14550 }, 14551 err: "Forbidden: only deletion is allowed, but found new scheduling gate 'baz'", 14552 test: "update pod spec schedulingGates: mutating an existing scheduling gate along with deletion", 14553 }, { 14554 new: core.Pod{}, 14555 old: core.Pod{ 14556 Spec: core.PodSpec{ 14557 SchedulingGates: []core.PodSchedulingGate{{Name: "foo"}}, 14558 }, 14559 }, 14560 err: "", 14561 test: "update pod spec schedulingGates: legal deletion", 14562 }, { 14563 old: core.Pod{ 14564 Spec: core.PodSpec{ 14565 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14566 }, 14567 }, 14568 new: core.Pod{ 14569 Spec: core.PodSpec{ 14570 NodeSelector: map[string]string{ 14571 "foo": "bar", 14572 }, 14573 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14574 }, 14575 }, 14576 test: "adding node selector is allowed for gated pods", 14577 }, { 14578 old: core.Pod{ 14579 Spec: core.PodSpec{ 14580 NodeSelector: map[string]string{ 14581 "foo": "bar", 14582 }, 14583 }, 14584 }, 14585 new: core.Pod{ 14586 Spec: core.PodSpec{ 14587 NodeSelector: map[string]string{ 14588 "foo": "bar", 14589 "foo2": "bar2", 14590 }, 14591 }, 14592 }, 14593 err: "Forbidden: pod updates may not change fields other than `spec.containers[*].image", 14594 test: "adding node selector is not allowed for non-gated pods", 14595 }, { 14596 old: core.Pod{ 14597 Spec: core.PodSpec{ 14598 NodeSelector: map[string]string{ 14599 "foo": "bar", 14600 }, 14601 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14602 }, 14603 }, 14604 new: core.Pod{ 14605 Spec: core.PodSpec{ 14606 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14607 }, 14608 }, 14609 err: "spec.nodeSelector: Invalid value:", 14610 test: "removing node selector is not allowed for gated pods", 14611 }, { 14612 old: core.Pod{ 14613 Spec: core.PodSpec{ 14614 NodeSelector: map[string]string{ 14615 "foo": "bar", 14616 }, 14617 }, 14618 }, 14619 new: core.Pod{}, 14620 err: "Forbidden: pod updates may not change fields other than `spec.containers[*].image", 14621 test: "removing node selector is not allowed for non-gated pods", 14622 }, { 14623 old: core.Pod{ 14624 Spec: core.PodSpec{ 14625 NodeSelector: map[string]string{ 14626 "foo": "bar", 14627 }, 14628 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14629 }, 14630 }, 14631 new: core.Pod{ 14632 Spec: core.PodSpec{ 14633 NodeSelector: map[string]string{ 14634 "foo": "bar", 14635 "foo2": "bar2", 14636 }, 14637 }, 14638 }, 14639 test: "old pod spec has scheduling gate, new pod spec does not, and node selector is added", 14640 }, { 14641 old: core.Pod{ 14642 Spec: core.PodSpec{ 14643 NodeSelector: map[string]string{ 14644 "foo": "bar", 14645 }, 14646 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14647 }, 14648 }, 14649 new: core.Pod{ 14650 Spec: core.PodSpec{ 14651 NodeSelector: map[string]string{ 14652 "foo": "new value", 14653 }, 14654 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14655 }, 14656 }, 14657 err: "spec.nodeSelector: Invalid value:", 14658 test: "modifying value of existing node selector is not allowed", 14659 }, { 14660 old: core.Pod{ 14661 Spec: core.PodSpec{ 14662 Affinity: &core.Affinity{ 14663 NodeAffinity: &core.NodeAffinity{ 14664 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14665 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14666 MatchExpressions: []core.NodeSelectorRequirement{{ 14667 Key: "expr", 14668 Operator: core.NodeSelectorOpIn, 14669 Values: []string{"foo"}, 14670 }}, 14671 }}, 14672 }, 14673 }, 14674 }, 14675 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14676 }, 14677 }, 14678 new: core.Pod{ 14679 Spec: core.PodSpec{ 14680 Affinity: &core.Affinity{ 14681 NodeAffinity: &core.NodeAffinity{ 14682 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14683 // Add 1 MatchExpression and 1 MatchField. 14684 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14685 MatchExpressions: []core.NodeSelectorRequirement{{ 14686 Key: "expr", 14687 Operator: core.NodeSelectorOpIn, 14688 Values: []string{"foo"}, 14689 }, { 14690 Key: "expr2", 14691 Operator: core.NodeSelectorOpIn, 14692 Values: []string{"foo2"}, 14693 }}, 14694 MatchFields: []core.NodeSelectorRequirement{{ 14695 Key: "metadata.name", 14696 Operator: core.NodeSelectorOpIn, 14697 Values: []string{"foo"}, 14698 }}, 14699 }}, 14700 }, 14701 }, 14702 }, 14703 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14704 }, 14705 }, 14706 test: "addition to nodeAffinity is allowed for gated pods", 14707 }, { 14708 old: core.Pod{ 14709 Spec: core.PodSpec{ 14710 Affinity: &core.Affinity{ 14711 NodeAffinity: &core.NodeAffinity{ 14712 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14713 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14714 MatchExpressions: []core.NodeSelectorRequirement{{ 14715 Key: "expr", 14716 Operator: core.NodeSelectorOpIn, 14717 Values: []string{"foo"}, 14718 }}, 14719 }}, 14720 }, 14721 }, 14722 }, 14723 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14724 }, 14725 }, 14726 new: core.Pod{ 14727 Spec: core.PodSpec{ 14728 Affinity: &core.Affinity{ 14729 NodeAffinity: &core.NodeAffinity{}, 14730 }, 14731 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14732 }, 14733 }, 14734 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: Invalid value:", 14735 test: "old RequiredDuringSchedulingIgnoredDuringExecution is non-nil, new RequiredDuringSchedulingIgnoredDuringExecution is nil, pod is gated", 14736 }, { 14737 old: core.Pod{ 14738 Spec: core.PodSpec{ 14739 Affinity: &core.Affinity{ 14740 NodeAffinity: &core.NodeAffinity{ 14741 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14742 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14743 MatchExpressions: []core.NodeSelectorRequirement{{ 14744 Key: "expr", 14745 Operator: core.NodeSelectorOpIn, 14746 Values: []string{"foo"}, 14747 }}, 14748 }}, 14749 }, 14750 }, 14751 }, 14752 }, 14753 }, 14754 new: core.Pod{ 14755 Spec: core.PodSpec{ 14756 Affinity: &core.Affinity{ 14757 NodeAffinity: &core.NodeAffinity{ 14758 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14759 // Add 1 MatchExpression and 1 MatchField. 14760 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14761 MatchExpressions: []core.NodeSelectorRequirement{{ 14762 Key: "expr", 14763 Operator: core.NodeSelectorOpIn, 14764 Values: []string{"foo"}, 14765 }, { 14766 Key: "expr2", 14767 Operator: core.NodeSelectorOpIn, 14768 Values: []string{"foo2"}, 14769 }}, 14770 MatchFields: []core.NodeSelectorRequirement{{ 14771 Key: "metadata.name", 14772 Operator: core.NodeSelectorOpIn, 14773 Values: []string{"foo"}, 14774 }}, 14775 }}, 14776 }, 14777 }, 14778 }, 14779 }, 14780 }, 14781 err: "Forbidden: pod updates may not change fields other than `spec.containers[*].image", 14782 test: "addition to nodeAffinity is not allowed for non-gated pods", 14783 }, { 14784 old: core.Pod{ 14785 Spec: core.PodSpec{ 14786 Affinity: &core.Affinity{ 14787 NodeAffinity: &core.NodeAffinity{ 14788 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14789 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14790 MatchExpressions: []core.NodeSelectorRequirement{{ 14791 Key: "expr", 14792 Operator: core.NodeSelectorOpIn, 14793 Values: []string{"foo"}, 14794 }}, 14795 }}, 14796 }, 14797 }, 14798 }, 14799 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14800 }, 14801 }, 14802 new: core.Pod{ 14803 Spec: core.PodSpec{ 14804 Affinity: &core.Affinity{ 14805 NodeAffinity: &core.NodeAffinity{ 14806 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14807 // Add 1 MatchExpression and 1 MatchField. 14808 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14809 MatchExpressions: []core.NodeSelectorRequirement{{ 14810 Key: "expr", 14811 Operator: core.NodeSelectorOpIn, 14812 Values: []string{"foo"}, 14813 }, { 14814 Key: "expr2", 14815 Operator: core.NodeSelectorOpIn, 14816 Values: []string{"foo2"}, 14817 }}, 14818 MatchFields: []core.NodeSelectorRequirement{{ 14819 Key: "metadata.name", 14820 Operator: core.NodeSelectorOpIn, 14821 Values: []string{"foo"}, 14822 }}, 14823 }}, 14824 }, 14825 }, 14826 }, 14827 }, 14828 }, 14829 test: "old pod spec has scheduling gate, new pod spec does not, and node affinity addition occurs", 14830 }, { 14831 old: core.Pod{ 14832 Spec: core.PodSpec{ 14833 Affinity: &core.Affinity{ 14834 NodeAffinity: &core.NodeAffinity{ 14835 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14836 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14837 MatchExpressions: []core.NodeSelectorRequirement{{ 14838 Key: "expr", 14839 Operator: core.NodeSelectorOpIn, 14840 Values: []string{"foo"}, 14841 }}, 14842 }}, 14843 }, 14844 }, 14845 }, 14846 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14847 }, 14848 }, 14849 new: core.Pod{ 14850 Spec: core.PodSpec{ 14851 Affinity: &core.Affinity{ 14852 NodeAffinity: &core.NodeAffinity{ 14853 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14854 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14855 MatchFields: []core.NodeSelectorRequirement{{ 14856 Key: "metadata.name", 14857 Operator: core.NodeSelectorOpIn, 14858 Values: []string{"foo"}, 14859 }}, 14860 }}, 14861 }, 14862 }, 14863 }, 14864 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14865 }, 14866 }, 14867 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", 14868 test: "nodeAffinity deletion from MatchExpressions not allowed", 14869 }, { 14870 old: core.Pod{ 14871 Spec: core.PodSpec{ 14872 Affinity: &core.Affinity{ 14873 NodeAffinity: &core.NodeAffinity{ 14874 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14875 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14876 MatchExpressions: []core.NodeSelectorRequirement{{ 14877 Key: "expr", 14878 Operator: core.NodeSelectorOpIn, 14879 Values: []string{"foo"}, 14880 }}, 14881 MatchFields: []core.NodeSelectorRequirement{{ 14882 Key: "metadata.name", 14883 Operator: core.NodeSelectorOpIn, 14884 Values: []string{"foo"}, 14885 }}, 14886 }}, 14887 }, 14888 }, 14889 }, 14890 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14891 }, 14892 }, 14893 new: core.Pod{ 14894 Spec: core.PodSpec{ 14895 Affinity: &core.Affinity{ 14896 NodeAffinity: &core.NodeAffinity{ 14897 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14898 // Add 1 MatchExpression and 1 MatchField. 14899 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14900 MatchExpressions: []core.NodeSelectorRequirement{{ 14901 Key: "expr", 14902 Operator: core.NodeSelectorOpIn, 14903 Values: []string{"foo"}, 14904 }}, 14905 }}, 14906 }, 14907 }, 14908 }, 14909 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14910 }, 14911 }, 14912 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", 14913 test: "nodeAffinity deletion from MatchFields not allowed", 14914 }, { 14915 old: core.Pod{ 14916 Spec: core.PodSpec{ 14917 Affinity: &core.Affinity{ 14918 NodeAffinity: &core.NodeAffinity{ 14919 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14920 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14921 MatchExpressions: []core.NodeSelectorRequirement{{ 14922 Key: "expr", 14923 Operator: core.NodeSelectorOpIn, 14924 Values: []string{"foo"}, 14925 }}, 14926 MatchFields: []core.NodeSelectorRequirement{{ 14927 Key: "metadata.name", 14928 Operator: core.NodeSelectorOpIn, 14929 Values: []string{"foo"}, 14930 }}, 14931 }}, 14932 }, 14933 }, 14934 }, 14935 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14936 }, 14937 }, 14938 new: core.Pod{ 14939 Spec: core.PodSpec{ 14940 Affinity: &core.Affinity{ 14941 NodeAffinity: &core.NodeAffinity{ 14942 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14943 // Add 1 MatchExpression and 1 MatchField. 14944 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14945 MatchExpressions: []core.NodeSelectorRequirement{{ 14946 Key: "expr", 14947 Operator: core.NodeSelectorOpIn, 14948 Values: []string{"bar"}, 14949 }}, 14950 MatchFields: []core.NodeSelectorRequirement{{ 14951 Key: "metadata.name", 14952 Operator: core.NodeSelectorOpIn, 14953 Values: []string{"foo"}, 14954 }}, 14955 }}, 14956 }, 14957 }, 14958 }, 14959 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14960 }, 14961 }, 14962 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", 14963 test: "nodeAffinity modification of item in MatchExpressions not allowed", 14964 }, { 14965 old: core.Pod{ 14966 Spec: core.PodSpec{ 14967 Affinity: &core.Affinity{ 14968 NodeAffinity: &core.NodeAffinity{ 14969 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14970 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14971 MatchExpressions: []core.NodeSelectorRequirement{{ 14972 Key: "expr", 14973 Operator: core.NodeSelectorOpIn, 14974 Values: []string{"foo"}, 14975 }}, 14976 MatchFields: []core.NodeSelectorRequirement{{ 14977 Key: "metadata.name", 14978 Operator: core.NodeSelectorOpIn, 14979 Values: []string{"foo"}, 14980 }}, 14981 }}, 14982 }, 14983 }, 14984 }, 14985 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 14986 }, 14987 }, 14988 new: core.Pod{ 14989 Spec: core.PodSpec{ 14990 Affinity: &core.Affinity{ 14991 NodeAffinity: &core.NodeAffinity{ 14992 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 14993 NodeSelectorTerms: []core.NodeSelectorTerm{{ 14994 MatchExpressions: []core.NodeSelectorRequirement{{ 14995 Key: "expr", 14996 Operator: core.NodeSelectorOpIn, 14997 Values: []string{"foo"}, 14998 }}, 14999 MatchFields: []core.NodeSelectorRequirement{{ 15000 Key: "metadata.name", 15001 Operator: core.NodeSelectorOpIn, 15002 Values: []string{"bar"}, 15003 }}, 15004 }}, 15005 }, 15006 }, 15007 }, 15008 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15009 }, 15010 }, 15011 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", 15012 test: "nodeAffinity modification of item in MatchFields not allowed", 15013 }, { 15014 old: core.Pod{ 15015 Spec: core.PodSpec{ 15016 Affinity: &core.Affinity{ 15017 NodeAffinity: &core.NodeAffinity{ 15018 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15019 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15020 MatchExpressions: []core.NodeSelectorRequirement{{ 15021 Key: "expr", 15022 Operator: core.NodeSelectorOpIn, 15023 Values: []string{"foo"}, 15024 }}, 15025 MatchFields: []core.NodeSelectorRequirement{{ 15026 Key: "metadata.name", 15027 Operator: core.NodeSelectorOpIn, 15028 Values: []string{"foo"}, 15029 }}, 15030 }}, 15031 }, 15032 }, 15033 }, 15034 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15035 }, 15036 }, 15037 new: core.Pod{ 15038 Spec: core.PodSpec{ 15039 Affinity: &core.Affinity{ 15040 NodeAffinity: &core.NodeAffinity{ 15041 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15042 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15043 MatchExpressions: []core.NodeSelectorRequirement{{ 15044 Key: "expr", 15045 Operator: core.NodeSelectorOpIn, 15046 Values: []string{"foo"}, 15047 }}, 15048 MatchFields: []core.NodeSelectorRequirement{{ 15049 Key: "metadata.name", 15050 Operator: core.NodeSelectorOpIn, 15051 Values: []string{"bar"}, 15052 }}, 15053 }, { 15054 MatchExpressions: []core.NodeSelectorRequirement{{ 15055 Key: "expr", 15056 Operator: core.NodeSelectorOpIn, 15057 Values: []string{"foo2"}, 15058 }}, 15059 MatchFields: []core.NodeSelectorRequirement{{ 15060 Key: "metadata.name", 15061 Operator: core.NodeSelectorOpIn, 15062 Values: []string{"bar2"}, 15063 }}, 15064 }}, 15065 }, 15066 }, 15067 }, 15068 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15069 }, 15070 }, 15071 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: Invalid value:", 15072 test: "nodeSelectorTerms addition on gated pod should fail", 15073 }, { 15074 old: core.Pod{ 15075 Spec: core.PodSpec{ 15076 Affinity: &core.Affinity{ 15077 NodeAffinity: &core.NodeAffinity{ 15078 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15079 Weight: 1.0, 15080 Preference: core.NodeSelectorTerm{ 15081 MatchExpressions: []core.NodeSelectorRequirement{{ 15082 Key: "expr", 15083 Operator: core.NodeSelectorOpIn, 15084 Values: []string{"foo"}, 15085 }}, 15086 }, 15087 }}, 15088 }, 15089 }, 15090 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15091 }, 15092 }, 15093 new: core.Pod{ 15094 Spec: core.PodSpec{ 15095 Affinity: &core.Affinity{ 15096 NodeAffinity: &core.NodeAffinity{ 15097 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15098 Weight: 1.0, 15099 Preference: core.NodeSelectorTerm{ 15100 MatchExpressions: []core.NodeSelectorRequirement{{ 15101 Key: "expr", 15102 Operator: core.NodeSelectorOpIn, 15103 Values: []string{"foo2"}, 15104 }}, 15105 }, 15106 }}, 15107 }, 15108 }, 15109 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15110 }, 15111 }, 15112 test: "preferredDuringSchedulingIgnoredDuringExecution can modified for gated pods", 15113 }, { 15114 old: core.Pod{ 15115 Spec: core.PodSpec{ 15116 Affinity: &core.Affinity{ 15117 NodeAffinity: &core.NodeAffinity{ 15118 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15119 Weight: 1.0, 15120 Preference: core.NodeSelectorTerm{ 15121 MatchExpressions: []core.NodeSelectorRequirement{{ 15122 Key: "expr", 15123 Operator: core.NodeSelectorOpIn, 15124 Values: []string{"foo"}, 15125 }}, 15126 }, 15127 }}, 15128 }, 15129 }, 15130 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15131 }, 15132 }, 15133 new: core.Pod{ 15134 Spec: core.PodSpec{ 15135 Affinity: &core.Affinity{ 15136 NodeAffinity: &core.NodeAffinity{ 15137 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15138 Weight: 1.0, 15139 Preference: core.NodeSelectorTerm{ 15140 MatchExpressions: []core.NodeSelectorRequirement{{ 15141 Key: "expr", 15142 Operator: core.NodeSelectorOpIn, 15143 Values: []string{"foo"}, 15144 }, { 15145 Key: "expr2", 15146 Operator: core.NodeSelectorOpIn, 15147 Values: []string{"foo2"}, 15148 }}, 15149 MatchFields: []core.NodeSelectorRequirement{{ 15150 Key: "metadata.name", 15151 Operator: core.NodeSelectorOpIn, 15152 Values: []string{"bar"}, 15153 }}, 15154 }, 15155 }}, 15156 }, 15157 }, 15158 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15159 }, 15160 }, 15161 test: "preferredDuringSchedulingIgnoredDuringExecution can have additions for gated pods", 15162 }, { 15163 old: core.Pod{ 15164 Spec: core.PodSpec{ 15165 Affinity: &core.Affinity{ 15166 NodeAffinity: &core.NodeAffinity{ 15167 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15168 Weight: 1.0, 15169 Preference: core.NodeSelectorTerm{ 15170 MatchExpressions: []core.NodeSelectorRequirement{{ 15171 Key: "expr", 15172 Operator: core.NodeSelectorOpIn, 15173 Values: []string{"foo"}, 15174 }}, 15175 }, 15176 }}, 15177 }, 15178 }, 15179 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15180 }, 15181 }, 15182 new: core.Pod{ 15183 Spec: core.PodSpec{ 15184 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15185 }, 15186 }, 15187 test: "preferredDuringSchedulingIgnoredDuringExecution can have removals for gated pods", 15188 }, { 15189 old: core.Pod{ 15190 Spec: core.PodSpec{ 15191 Affinity: &core.Affinity{ 15192 NodeAffinity: &core.NodeAffinity{ 15193 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15194 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15195 MatchExpressions: []core.NodeSelectorRequirement{{ 15196 Key: "expr", 15197 Operator: core.NodeSelectorOpIn, 15198 Values: []string{"foo"}, 15199 }}, 15200 }}, 15201 }, 15202 }, 15203 }, 15204 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15205 }, 15206 }, 15207 new: core.Pod{ 15208 Spec: core.PodSpec{ 15209 Affinity: &core.Affinity{}, 15210 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15211 }, 15212 }, 15213 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms: Invalid value:", 15214 test: "new node affinity is nil", 15215 }, { 15216 old: core.Pod{ 15217 Spec: core.PodSpec{ 15218 Affinity: &core.Affinity{ 15219 NodeAffinity: &core.NodeAffinity{ 15220 PreferredDuringSchedulingIgnoredDuringExecution: []core.PreferredSchedulingTerm{{ 15221 Weight: 1.0, 15222 Preference: core.NodeSelectorTerm{ 15223 MatchExpressions: []core.NodeSelectorRequirement{{ 15224 Key: "expr", 15225 Operator: core.NodeSelectorOpIn, 15226 Values: []string{"foo"}, 15227 }}, 15228 }, 15229 }}, 15230 }, 15231 }, 15232 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15233 }, 15234 }, 15235 new: core.Pod{ 15236 Spec: core.PodSpec{ 15237 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15238 }, 15239 }, 15240 test: "preferredDuringSchedulingIgnoredDuringExecution can have removals for gated pods", 15241 }, { 15242 old: core.Pod{ 15243 Spec: core.PodSpec{ 15244 Affinity: &core.Affinity{ 15245 NodeAffinity: &core.NodeAffinity{ 15246 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15247 NodeSelectorTerms: []core.NodeSelectorTerm{ 15248 {}, 15249 }, 15250 }, 15251 }, 15252 }, 15253 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15254 }, 15255 }, 15256 new: core.Pod{ 15257 Spec: core.PodSpec{ 15258 Affinity: &core.Affinity{ 15259 NodeAffinity: &core.NodeAffinity{ 15260 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15261 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15262 MatchExpressions: []core.NodeSelectorRequirement{{ 15263 Key: "expr", 15264 Operator: core.NodeSelectorOpIn, 15265 Values: []string{"foo"}, 15266 }}, 15267 }}, 15268 }, 15269 }, 15270 }, 15271 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15272 }, 15273 }, 15274 err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", 15275 test: "empty NodeSelectorTerm (selects nothing) cannot become populated (selects something)", 15276 }, { 15277 old: core.Pod{ 15278 Spec: core.PodSpec{ 15279 Affinity: nil, 15280 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15281 }, 15282 }, 15283 new: core.Pod{ 15284 Spec: core.PodSpec{ 15285 Affinity: &core.Affinity{ 15286 NodeAffinity: &core.NodeAffinity{ 15287 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15288 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15289 MatchExpressions: []core.NodeSelectorRequirement{{ 15290 Key: "expr", 15291 Operator: core.NodeSelectorOpIn, 15292 Values: []string{"foo"}, 15293 }}, 15294 }}, 15295 }, 15296 }, 15297 }, 15298 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15299 }, 15300 }, 15301 test: "nil affinity can be mutated for gated pods", 15302 }, 15303 { 15304 old: core.Pod{ 15305 Spec: core.PodSpec{ 15306 Affinity: nil, 15307 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15308 }, 15309 }, 15310 new: core.Pod{ 15311 Spec: core.PodSpec{ 15312 Affinity: &core.Affinity{ 15313 NodeAffinity: &core.NodeAffinity{ 15314 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15315 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15316 MatchExpressions: []core.NodeSelectorRequirement{{ 15317 Key: "expr", 15318 Operator: core.NodeSelectorOpIn, 15319 Values: []string{"foo"}, 15320 }}, 15321 }}, 15322 }, 15323 }, 15324 PodAffinity: &core.PodAffinity{ 15325 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 15326 { 15327 TopologyKey: "foo", 15328 LabelSelector: &metav1.LabelSelector{ 15329 MatchLabels: map[string]string{"foo": "bar"}, 15330 }, 15331 }, 15332 }, 15333 }, 15334 }, 15335 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15336 }, 15337 }, 15338 err: "pod updates may not change fields other than", 15339 test: "the podAffinity cannot be updated on gated pods", 15340 }, 15341 { 15342 old: core.Pod{ 15343 Spec: core.PodSpec{ 15344 Affinity: nil, 15345 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15346 }, 15347 }, 15348 new: core.Pod{ 15349 Spec: core.PodSpec{ 15350 Affinity: &core.Affinity{ 15351 NodeAffinity: &core.NodeAffinity{ 15352 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 15353 NodeSelectorTerms: []core.NodeSelectorTerm{{ 15354 MatchExpressions: []core.NodeSelectorRequirement{{ 15355 Key: "expr", 15356 Operator: core.NodeSelectorOpIn, 15357 Values: []string{"foo"}, 15358 }}, 15359 }}, 15360 }, 15361 }, 15362 PodAntiAffinity: &core.PodAntiAffinity{ 15363 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 15364 { 15365 TopologyKey: "foo", 15366 LabelSelector: &metav1.LabelSelector{ 15367 MatchLabels: map[string]string{"foo": "bar"}, 15368 }, 15369 }, 15370 }, 15371 }, 15372 }, 15373 SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, 15374 }, 15375 }, 15376 err: "pod updates may not change fields other than", 15377 test: "the podAntiAffinity cannot be updated on gated pods", 15378 }, 15379 } 15380 for _, test := range tests { 15381 test.new.ObjectMeta.ResourceVersion = "1" 15382 test.old.ObjectMeta.ResourceVersion = "1" 15383 15384 // set required fields if old and new match and have no opinion on the value 15385 if test.new.Name == "" && test.old.Name == "" { 15386 test.new.Name = "name" 15387 test.old.Name = "name" 15388 } 15389 if test.new.Namespace == "" && test.old.Namespace == "" { 15390 test.new.Namespace = "namespace" 15391 test.old.Namespace = "namespace" 15392 } 15393 if test.new.Spec.Containers == nil && test.old.Spec.Containers == nil { 15394 test.new.Spec.Containers = []core.Container{{Name: "autoadded", Image: "image", TerminationMessagePolicy: "File", ImagePullPolicy: "Always"}} 15395 test.old.Spec.Containers = []core.Container{{Name: "autoadded", Image: "image", TerminationMessagePolicy: "File", ImagePullPolicy: "Always"}} 15396 } 15397 if len(test.new.Spec.DNSPolicy) == 0 && len(test.old.Spec.DNSPolicy) == 0 { 15398 test.new.Spec.DNSPolicy = core.DNSClusterFirst 15399 test.old.Spec.DNSPolicy = core.DNSClusterFirst 15400 } 15401 if len(test.new.Spec.RestartPolicy) == 0 && len(test.old.Spec.RestartPolicy) == 0 { 15402 test.new.Spec.RestartPolicy = "Always" 15403 test.old.Spec.RestartPolicy = "Always" 15404 } 15405 15406 errs := ValidatePodUpdate(&test.new, &test.old, PodValidationOptions{}) 15407 if test.err == "" { 15408 if len(errs) != 0 { 15409 t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.new, test.old) 15410 } 15411 } else { 15412 if len(errs) == 0 { 15413 t.Errorf("unexpected valid: %s\nA: %+v\nB: %+v", test.test, test.new, test.old) 15414 } else if actualErr := errs.ToAggregate().Error(); !strings.Contains(actualErr, test.err) { 15415 t.Errorf("unexpected error message: %s\nExpected error: %s\nActual error: %s", test.test, test.err, actualErr) 15416 } 15417 } 15418 } 15419 } 15420 15421 func TestValidatePodStatusUpdate(t *testing.T) { 15422 tests := []struct { 15423 new core.Pod 15424 old core.Pod 15425 err string 15426 test string 15427 }{{ 15428 core.Pod{ 15429 ObjectMeta: metav1.ObjectMeta{ 15430 Name: "foo", 15431 }, 15432 Spec: core.PodSpec{ 15433 NodeName: "node1", 15434 }, 15435 Status: core.PodStatus{ 15436 NominatedNodeName: "node1", 15437 }, 15438 }, 15439 core.Pod{ 15440 ObjectMeta: metav1.ObjectMeta{ 15441 Name: "foo", 15442 }, 15443 Spec: core.PodSpec{ 15444 NodeName: "node1", 15445 }, 15446 Status: core.PodStatus{}, 15447 }, 15448 "", 15449 "removed nominatedNodeName", 15450 }, { 15451 core.Pod{ 15452 ObjectMeta: metav1.ObjectMeta{ 15453 Name: "foo", 15454 }, 15455 Spec: core.PodSpec{ 15456 NodeName: "node1", 15457 }, 15458 }, 15459 core.Pod{ 15460 ObjectMeta: metav1.ObjectMeta{ 15461 Name: "foo", 15462 }, 15463 Spec: core.PodSpec{ 15464 NodeName: "node1", 15465 }, 15466 Status: core.PodStatus{ 15467 NominatedNodeName: "node1", 15468 }, 15469 }, 15470 "", 15471 "add valid nominatedNodeName", 15472 }, { 15473 core.Pod{ 15474 ObjectMeta: metav1.ObjectMeta{ 15475 Name: "foo", 15476 }, 15477 Spec: core.PodSpec{ 15478 NodeName: "node1", 15479 }, 15480 Status: core.PodStatus{ 15481 NominatedNodeName: "Node1", 15482 }, 15483 }, 15484 core.Pod{ 15485 ObjectMeta: metav1.ObjectMeta{ 15486 Name: "foo", 15487 }, 15488 Spec: core.PodSpec{ 15489 NodeName: "node1", 15490 }, 15491 }, 15492 "nominatedNodeName", 15493 "Add invalid nominatedNodeName", 15494 }, { 15495 core.Pod{ 15496 ObjectMeta: metav1.ObjectMeta{ 15497 Name: "foo", 15498 }, 15499 Spec: core.PodSpec{ 15500 NodeName: "node1", 15501 }, 15502 Status: core.PodStatus{ 15503 NominatedNodeName: "node1", 15504 }, 15505 }, 15506 core.Pod{ 15507 ObjectMeta: metav1.ObjectMeta{ 15508 Name: "foo", 15509 }, 15510 Spec: core.PodSpec{ 15511 NodeName: "node1", 15512 }, 15513 Status: core.PodStatus{ 15514 NominatedNodeName: "node2", 15515 }, 15516 }, 15517 "", 15518 "Update nominatedNodeName", 15519 }, { 15520 core.Pod{ 15521 ObjectMeta: metav1.ObjectMeta{ 15522 Name: "foo", 15523 }, 15524 Status: core.PodStatus{ 15525 InitContainerStatuses: []core.ContainerStatus{{ 15526 ContainerID: "docker://numbers", 15527 Image: "alpine", 15528 Name: "init", 15529 Ready: false, 15530 Started: proto.Bool(false), 15531 State: core.ContainerState{ 15532 Waiting: &core.ContainerStateWaiting{ 15533 Reason: "PodInitializing", 15534 }, 15535 }, 15536 }}, 15537 ContainerStatuses: []core.ContainerStatus{{ 15538 ContainerID: "docker://numbers", 15539 Image: "nginx:alpine", 15540 Name: "main", 15541 Ready: false, 15542 Started: proto.Bool(false), 15543 State: core.ContainerState{ 15544 Waiting: &core.ContainerStateWaiting{ 15545 Reason: "PodInitializing", 15546 }, 15547 }, 15548 }}, 15549 }, 15550 }, 15551 core.Pod{ 15552 ObjectMeta: metav1.ObjectMeta{ 15553 Name: "foo", 15554 }, 15555 }, 15556 "", 15557 "Container statuses pending", 15558 }, { 15559 core.Pod{ 15560 ObjectMeta: metav1.ObjectMeta{ 15561 Name: "foo", 15562 }, 15563 Status: core.PodStatus{ 15564 InitContainerStatuses: []core.ContainerStatus{{ 15565 ContainerID: "docker://numbers", 15566 Image: "alpine", 15567 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15568 Name: "init", 15569 Ready: true, 15570 State: core.ContainerState{ 15571 Terminated: &core.ContainerStateTerminated{ 15572 ContainerID: "docker://numbers", 15573 Reason: "Completed", 15574 }, 15575 }, 15576 }}, 15577 ContainerStatuses: []core.ContainerStatus{{ 15578 ContainerID: "docker://numbers", 15579 Image: "nginx:alpine", 15580 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15581 Name: "nginx", 15582 Ready: true, 15583 Started: proto.Bool(true), 15584 State: core.ContainerState{ 15585 Running: &core.ContainerStateRunning{ 15586 StartedAt: metav1.NewTime(time.Now()), 15587 }, 15588 }, 15589 }}, 15590 }, 15591 }, 15592 core.Pod{ 15593 ObjectMeta: metav1.ObjectMeta{ 15594 Name: "foo", 15595 }, 15596 Status: core.PodStatus{ 15597 InitContainerStatuses: []core.ContainerStatus{{ 15598 ContainerID: "docker://numbers", 15599 Image: "alpine", 15600 Name: "init", 15601 Ready: false, 15602 State: core.ContainerState{ 15603 Waiting: &core.ContainerStateWaiting{ 15604 Reason: "PodInitializing", 15605 }, 15606 }, 15607 }}, 15608 ContainerStatuses: []core.ContainerStatus{{ 15609 ContainerID: "docker://numbers", 15610 Image: "nginx:alpine", 15611 Name: "main", 15612 Ready: false, 15613 Started: proto.Bool(false), 15614 State: core.ContainerState{ 15615 Waiting: &core.ContainerStateWaiting{ 15616 Reason: "PodInitializing", 15617 }, 15618 }, 15619 }}, 15620 }, 15621 }, 15622 "", 15623 "Container statuses running", 15624 }, { 15625 core.Pod{ 15626 ObjectMeta: metav1.ObjectMeta{ 15627 Name: "foo", 15628 }, 15629 Status: core.PodStatus{ 15630 ContainerStatuses: []core.ContainerStatus{{ 15631 ContainerID: "docker://numbers", 15632 Image: "nginx:alpine", 15633 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15634 Name: "nginx", 15635 Ready: true, 15636 Started: proto.Bool(true), 15637 State: core.ContainerState{ 15638 Running: &core.ContainerStateRunning{ 15639 StartedAt: metav1.NewTime(time.Now()), 15640 }, 15641 }, 15642 }}, 15643 EphemeralContainerStatuses: []core.ContainerStatus{{ 15644 ContainerID: "docker://numbers", 15645 Image: "busybox", 15646 Name: "debug", 15647 Ready: false, 15648 State: core.ContainerState{ 15649 Waiting: &core.ContainerStateWaiting{ 15650 Reason: "PodInitializing", 15651 }, 15652 }, 15653 }}, 15654 }, 15655 }, 15656 core.Pod{ 15657 ObjectMeta: metav1.ObjectMeta{ 15658 Name: "foo", 15659 }, 15660 Status: core.PodStatus{ 15661 ContainerStatuses: []core.ContainerStatus{{ 15662 ContainerID: "docker://numbers", 15663 Image: "nginx:alpine", 15664 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15665 Name: "nginx", 15666 Ready: true, 15667 Started: proto.Bool(true), 15668 State: core.ContainerState{ 15669 Running: &core.ContainerStateRunning{ 15670 StartedAt: metav1.NewTime(time.Now()), 15671 }, 15672 }, 15673 }}, 15674 }, 15675 }, 15676 "", 15677 "Container statuses add ephemeral container", 15678 }, { 15679 core.Pod{ 15680 ObjectMeta: metav1.ObjectMeta{ 15681 Name: "foo", 15682 }, 15683 Status: core.PodStatus{ 15684 ContainerStatuses: []core.ContainerStatus{{ 15685 ContainerID: "docker://numbers", 15686 Image: "nginx:alpine", 15687 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15688 Name: "nginx", 15689 Ready: true, 15690 Started: proto.Bool(true), 15691 State: core.ContainerState{ 15692 Running: &core.ContainerStateRunning{ 15693 StartedAt: metav1.NewTime(time.Now()), 15694 }, 15695 }, 15696 }}, 15697 EphemeralContainerStatuses: []core.ContainerStatus{{ 15698 ContainerID: "docker://numbers", 15699 Image: "busybox", 15700 ImageID: "docker-pullable://busybox@sha256:d0gf00d", 15701 Name: "debug", 15702 Ready: false, 15703 State: core.ContainerState{ 15704 Running: &core.ContainerStateRunning{ 15705 StartedAt: metav1.NewTime(time.Now()), 15706 }, 15707 }, 15708 }}, 15709 }, 15710 }, 15711 core.Pod{ 15712 ObjectMeta: metav1.ObjectMeta{ 15713 Name: "foo", 15714 }, 15715 Status: core.PodStatus{ 15716 ContainerStatuses: []core.ContainerStatus{{ 15717 ContainerID: "docker://numbers", 15718 Image: "nginx:alpine", 15719 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15720 Name: "nginx", 15721 Ready: true, 15722 Started: proto.Bool(true), 15723 State: core.ContainerState{ 15724 Running: &core.ContainerStateRunning{ 15725 StartedAt: metav1.NewTime(time.Now()), 15726 }, 15727 }, 15728 }}, 15729 EphemeralContainerStatuses: []core.ContainerStatus{{ 15730 ContainerID: "docker://numbers", 15731 Image: "busybox", 15732 Name: "debug", 15733 Ready: false, 15734 State: core.ContainerState{ 15735 Waiting: &core.ContainerStateWaiting{ 15736 Reason: "PodInitializing", 15737 }, 15738 }, 15739 }}, 15740 }, 15741 }, 15742 "", 15743 "Container statuses ephemeral container running", 15744 }, { 15745 core.Pod{ 15746 ObjectMeta: metav1.ObjectMeta{ 15747 Name: "foo", 15748 }, 15749 Status: core.PodStatus{ 15750 ContainerStatuses: []core.ContainerStatus{{ 15751 ContainerID: "docker://numbers", 15752 Image: "nginx:alpine", 15753 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15754 Name: "nginx", 15755 Ready: true, 15756 Started: proto.Bool(true), 15757 State: core.ContainerState{ 15758 Running: &core.ContainerStateRunning{ 15759 StartedAt: metav1.NewTime(time.Now()), 15760 }, 15761 }, 15762 }}, 15763 EphemeralContainerStatuses: []core.ContainerStatus{{ 15764 ContainerID: "docker://numbers", 15765 Image: "busybox", 15766 ImageID: "docker-pullable://busybox@sha256:d0gf00d", 15767 Name: "debug", 15768 Ready: false, 15769 State: core.ContainerState{ 15770 Terminated: &core.ContainerStateTerminated{ 15771 ContainerID: "docker://numbers", 15772 Reason: "Completed", 15773 StartedAt: metav1.NewTime(time.Now()), 15774 FinishedAt: metav1.NewTime(time.Now()), 15775 }, 15776 }, 15777 }}, 15778 }, 15779 }, 15780 core.Pod{ 15781 ObjectMeta: metav1.ObjectMeta{ 15782 Name: "foo", 15783 }, 15784 Status: core.PodStatus{ 15785 ContainerStatuses: []core.ContainerStatus{{ 15786 ContainerID: "docker://numbers", 15787 Image: "nginx:alpine", 15788 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15789 Name: "nginx", 15790 Ready: true, 15791 Started: proto.Bool(true), 15792 State: core.ContainerState{ 15793 Running: &core.ContainerStateRunning{ 15794 StartedAt: metav1.NewTime(time.Now()), 15795 }, 15796 }, 15797 }}, 15798 EphemeralContainerStatuses: []core.ContainerStatus{{ 15799 ContainerID: "docker://numbers", 15800 Image: "busybox", 15801 ImageID: "docker-pullable://busybox@sha256:d0gf00d", 15802 Name: "debug", 15803 Ready: false, 15804 State: core.ContainerState{ 15805 Running: &core.ContainerStateRunning{ 15806 StartedAt: metav1.NewTime(time.Now()), 15807 }, 15808 }, 15809 }}, 15810 }, 15811 }, 15812 "", 15813 "Container statuses ephemeral container exited", 15814 }, { 15815 core.Pod{ 15816 ObjectMeta: metav1.ObjectMeta{ 15817 Name: "foo", 15818 }, 15819 Status: core.PodStatus{ 15820 InitContainerStatuses: []core.ContainerStatus{{ 15821 ContainerID: "docker://numbers", 15822 Image: "alpine", 15823 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15824 Name: "init", 15825 Ready: true, 15826 State: core.ContainerState{ 15827 Terminated: &core.ContainerStateTerminated{ 15828 ContainerID: "docker://numbers", 15829 Reason: "Completed", 15830 }, 15831 }, 15832 }}, 15833 ContainerStatuses: []core.ContainerStatus{{ 15834 ContainerID: "docker://numbers", 15835 Image: "nginx:alpine", 15836 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15837 Name: "nginx", 15838 Ready: true, 15839 Started: proto.Bool(true), 15840 State: core.ContainerState{ 15841 Terminated: &core.ContainerStateTerminated{ 15842 ContainerID: "docker://numbers", 15843 Reason: "Completed", 15844 StartedAt: metav1.NewTime(time.Now()), 15845 FinishedAt: metav1.NewTime(time.Now()), 15846 }, 15847 }, 15848 }}, 15849 EphemeralContainerStatuses: []core.ContainerStatus{{ 15850 ContainerID: "docker://numbers", 15851 Image: "busybox", 15852 ImageID: "docker-pullable://busybox@sha256:d0gf00d", 15853 Name: "debug", 15854 Ready: false, 15855 State: core.ContainerState{ 15856 Terminated: &core.ContainerStateTerminated{ 15857 ContainerID: "docker://numbers", 15858 Reason: "Completed", 15859 StartedAt: metav1.NewTime(time.Now()), 15860 FinishedAt: metav1.NewTime(time.Now()), 15861 }, 15862 }, 15863 }}, 15864 }, 15865 }, 15866 core.Pod{ 15867 ObjectMeta: metav1.ObjectMeta{ 15868 Name: "foo", 15869 }, 15870 Status: core.PodStatus{ 15871 InitContainerStatuses: []core.ContainerStatus{{ 15872 ContainerID: "docker://numbers", 15873 Image: "alpine", 15874 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15875 Name: "init", 15876 Ready: true, 15877 State: core.ContainerState{ 15878 Terminated: &core.ContainerStateTerminated{ 15879 ContainerID: "docker://numbers", 15880 Reason: "Completed", 15881 }, 15882 }, 15883 }}, 15884 ContainerStatuses: []core.ContainerStatus{{ 15885 ContainerID: "docker://numbers", 15886 Image: "nginx:alpine", 15887 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 15888 Name: "nginx", 15889 Ready: true, 15890 Started: proto.Bool(true), 15891 State: core.ContainerState{ 15892 Running: &core.ContainerStateRunning{ 15893 StartedAt: metav1.NewTime(time.Now()), 15894 }, 15895 }, 15896 }}, 15897 EphemeralContainerStatuses: []core.ContainerStatus{{ 15898 ContainerID: "docker://numbers", 15899 Image: "busybox", 15900 ImageID: "docker-pullable://busybox@sha256:d0gf00d", 15901 Name: "debug", 15902 Ready: false, 15903 State: core.ContainerState{ 15904 Running: &core.ContainerStateRunning{ 15905 StartedAt: metav1.NewTime(time.Now()), 15906 }, 15907 }, 15908 }}, 15909 }, 15910 }, 15911 "", 15912 "Container statuses all containers terminated", 15913 }, { 15914 core.Pod{ 15915 ObjectMeta: metav1.ObjectMeta{ 15916 Name: "foo", 15917 }, 15918 Status: core.PodStatus{ 15919 ResourceClaimStatuses: []core.PodResourceClaimStatus{ 15920 {Name: "no-such-claim", ResourceClaimName: utilpointer.String("my-claim")}, 15921 }, 15922 }, 15923 }, 15924 core.Pod{ 15925 ObjectMeta: metav1.ObjectMeta{ 15926 Name: "foo", 15927 }, 15928 }, 15929 "status.resourceClaimStatuses[0].name: Invalid value: \"no-such-claim\": must match the name of an entry in `spec.resourceClaims`", 15930 "Non-existent PodResourceClaim", 15931 }, { 15932 core.Pod{ 15933 ObjectMeta: metav1.ObjectMeta{ 15934 Name: "foo", 15935 }, 15936 Spec: core.PodSpec{ 15937 ResourceClaims: []core.PodResourceClaim{ 15938 {Name: "my-claim"}, 15939 }, 15940 }, 15941 Status: core.PodStatus{ 15942 ResourceClaimStatuses: []core.PodResourceClaimStatus{ 15943 {Name: "my-claim", ResourceClaimName: utilpointer.String("%$!#")}, 15944 }, 15945 }, 15946 }, 15947 core.Pod{ 15948 ObjectMeta: metav1.ObjectMeta{ 15949 Name: "foo", 15950 }, 15951 Spec: core.PodSpec{ 15952 ResourceClaims: []core.PodResourceClaim{ 15953 {Name: "my-claim"}, 15954 }, 15955 }, 15956 }, 15957 `status.resourceClaimStatuses[0].name: Invalid value: "%$!#": a lowercase RFC 1123 subdomain must consist of`, 15958 "Invalid ResourceClaim name", 15959 }, { 15960 core.Pod{ 15961 ObjectMeta: metav1.ObjectMeta{ 15962 Name: "foo", 15963 }, 15964 Spec: core.PodSpec{ 15965 ResourceClaims: []core.PodResourceClaim{ 15966 {Name: "my-claim"}, 15967 {Name: "my-other-claim"}, 15968 }, 15969 }, 15970 Status: core.PodStatus{ 15971 ResourceClaimStatuses: []core.PodResourceClaimStatus{ 15972 {Name: "my-claim", ResourceClaimName: utilpointer.String("foo-my-claim-12345")}, 15973 {Name: "my-other-claim", ResourceClaimName: nil}, 15974 {Name: "my-other-claim", ResourceClaimName: nil}, 15975 }, 15976 }, 15977 }, 15978 core.Pod{ 15979 ObjectMeta: metav1.ObjectMeta{ 15980 Name: "foo", 15981 }, 15982 Spec: core.PodSpec{ 15983 ResourceClaims: []core.PodResourceClaim{ 15984 {Name: "my-claim"}, 15985 }, 15986 }, 15987 }, 15988 `status.resourceClaimStatuses[2].name: Duplicate value: "my-other-claim"`, 15989 "Duplicate ResourceClaimStatuses.Name", 15990 }, { 15991 core.Pod{ 15992 ObjectMeta: metav1.ObjectMeta{ 15993 Name: "foo", 15994 }, 15995 Spec: core.PodSpec{ 15996 ResourceClaims: []core.PodResourceClaim{ 15997 {Name: "my-claim"}, 15998 {Name: "my-other-claim"}, 15999 }, 16000 }, 16001 Status: core.PodStatus{ 16002 ResourceClaimStatuses: []core.PodResourceClaimStatus{ 16003 {Name: "my-claim", ResourceClaimName: utilpointer.String("foo-my-claim-12345")}, 16004 {Name: "my-other-claim", ResourceClaimName: nil}, 16005 }, 16006 }, 16007 }, 16008 core.Pod{ 16009 ObjectMeta: metav1.ObjectMeta{ 16010 Name: "foo", 16011 }, 16012 Spec: core.PodSpec{ 16013 ResourceClaims: []core.PodResourceClaim{ 16014 {Name: "my-claim"}, 16015 }, 16016 }, 16017 }, 16018 "", 16019 "ResourceClaimStatuses okay", 16020 }, { 16021 core.Pod{ 16022 ObjectMeta: metav1.ObjectMeta{ 16023 Name: "foo", 16024 }, 16025 Spec: core.PodSpec{ 16026 InitContainers: []core.Container{ 16027 { 16028 Name: "init", 16029 }, 16030 }, 16031 Containers: []core.Container{ 16032 { 16033 Name: "nginx", 16034 }, 16035 }, 16036 }, 16037 Status: core.PodStatus{ 16038 InitContainerStatuses: []core.ContainerStatus{{ 16039 ContainerID: "docker://numbers", 16040 Image: "alpine", 16041 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16042 Name: "init", 16043 Ready: true, 16044 State: core.ContainerState{ 16045 Running: &core.ContainerStateRunning{ 16046 StartedAt: metav1.NewTime(time.Now()), 16047 }, 16048 }, 16049 }}, 16050 ContainerStatuses: []core.ContainerStatus{{ 16051 ContainerID: "docker://numbers", 16052 Image: "nginx:alpine", 16053 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16054 Name: "nginx", 16055 Ready: true, 16056 Started: proto.Bool(true), 16057 State: core.ContainerState{ 16058 Running: &core.ContainerStateRunning{ 16059 StartedAt: metav1.NewTime(time.Now()), 16060 }, 16061 }, 16062 }}, 16063 }, 16064 }, 16065 core.Pod{ 16066 ObjectMeta: metav1.ObjectMeta{ 16067 Name: "foo", 16068 }, 16069 Spec: core.PodSpec{ 16070 InitContainers: []core.Container{ 16071 { 16072 Name: "init", 16073 }, 16074 }, 16075 Containers: []core.Container{ 16076 { 16077 Name: "nginx", 16078 }, 16079 }, 16080 RestartPolicy: core.RestartPolicyNever, 16081 }, 16082 Status: core.PodStatus{ 16083 InitContainerStatuses: []core.ContainerStatus{{ 16084 ContainerID: "docker://numbers", 16085 Image: "alpine", 16086 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16087 Name: "init", 16088 Ready: false, 16089 State: core.ContainerState{ 16090 Terminated: &core.ContainerStateTerminated{ 16091 ContainerID: "docker://numbers", 16092 Reason: "Completed", 16093 }, 16094 }, 16095 }}, 16096 ContainerStatuses: []core.ContainerStatus{{ 16097 ContainerID: "docker://numbers", 16098 Image: "nginx:alpine", 16099 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16100 Name: "nginx", 16101 Ready: true, 16102 Started: proto.Bool(true), 16103 State: core.ContainerState{ 16104 Running: &core.ContainerStateRunning{ 16105 StartedAt: metav1.NewTime(time.Now()), 16106 }, 16107 }, 16108 }}, 16109 }, 16110 }, 16111 `status.initContainerStatuses[0].state: Forbidden: may not be transitioned to non-terminated state`, 16112 "init container cannot restart if RestartPolicyNever", 16113 }, { 16114 core.Pod{ 16115 ObjectMeta: metav1.ObjectMeta{ 16116 Name: "foo", 16117 }, 16118 Spec: core.PodSpec{ 16119 InitContainers: []core.Container{ 16120 { 16121 Name: "restartable-init", 16122 RestartPolicy: &containerRestartPolicyAlways, 16123 }, 16124 }, 16125 Containers: []core.Container{ 16126 { 16127 Name: "nginx", 16128 }, 16129 }, 16130 RestartPolicy: core.RestartPolicyNever, 16131 }, 16132 Status: core.PodStatus{ 16133 InitContainerStatuses: []core.ContainerStatus{{ 16134 ContainerID: "docker://numbers", 16135 Image: "alpine", 16136 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16137 Name: "restartable-init", 16138 Ready: true, 16139 State: core.ContainerState{ 16140 Running: &core.ContainerStateRunning{ 16141 StartedAt: metav1.NewTime(time.Now()), 16142 }, 16143 }, 16144 }}, 16145 ContainerStatuses: []core.ContainerStatus{{ 16146 ContainerID: "docker://numbers", 16147 Image: "nginx:alpine", 16148 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16149 Name: "nginx", 16150 Ready: true, 16151 Started: proto.Bool(true), 16152 State: core.ContainerState{ 16153 Running: &core.ContainerStateRunning{ 16154 StartedAt: metav1.NewTime(time.Now()), 16155 }, 16156 }, 16157 }}, 16158 }, 16159 }, 16160 core.Pod{ 16161 ObjectMeta: metav1.ObjectMeta{ 16162 Name: "foo", 16163 }, 16164 Spec: core.PodSpec{ 16165 InitContainers: []core.Container{ 16166 { 16167 Name: "restartable-init", 16168 RestartPolicy: &containerRestartPolicyAlways, 16169 }, 16170 }, 16171 Containers: []core.Container{ 16172 { 16173 Name: "nginx", 16174 }, 16175 }, 16176 RestartPolicy: core.RestartPolicyNever, 16177 }, 16178 Status: core.PodStatus{ 16179 InitContainerStatuses: []core.ContainerStatus{{ 16180 ContainerID: "docker://numbers", 16181 Image: "alpine", 16182 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16183 Name: "restartable-init", 16184 Ready: false, 16185 State: core.ContainerState{ 16186 Terminated: &core.ContainerStateTerminated{ 16187 ContainerID: "docker://numbers", 16188 Reason: "Completed", 16189 }, 16190 }, 16191 }}, 16192 ContainerStatuses: []core.ContainerStatus{{ 16193 ContainerID: "docker://numbers", 16194 Image: "nginx:alpine", 16195 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16196 Name: "nginx", 16197 Ready: true, 16198 Started: proto.Bool(true), 16199 State: core.ContainerState{ 16200 Running: &core.ContainerStateRunning{ 16201 StartedAt: metav1.NewTime(time.Now()), 16202 }, 16203 }, 16204 }}, 16205 }, 16206 }, 16207 "", 16208 "restartable init container can restart if RestartPolicyNever", 16209 }, { 16210 core.Pod{ 16211 ObjectMeta: metav1.ObjectMeta{ 16212 Name: "foo", 16213 }, 16214 Spec: core.PodSpec{ 16215 InitContainers: []core.Container{ 16216 { 16217 Name: "restartable-init", 16218 RestartPolicy: &containerRestartPolicyAlways, 16219 }, 16220 }, 16221 Containers: []core.Container{ 16222 { 16223 Name: "nginx", 16224 }, 16225 }, 16226 RestartPolicy: core.RestartPolicyOnFailure, 16227 }, 16228 Status: core.PodStatus{ 16229 InitContainerStatuses: []core.ContainerStatus{{ 16230 ContainerID: "docker://numbers", 16231 Image: "alpine", 16232 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16233 Name: "restartable-init", 16234 Ready: true, 16235 State: core.ContainerState{ 16236 Running: &core.ContainerStateRunning{ 16237 StartedAt: metav1.NewTime(time.Now()), 16238 }, 16239 }, 16240 }}, 16241 ContainerStatuses: []core.ContainerStatus{{ 16242 ContainerID: "docker://numbers", 16243 Image: "nginx:alpine", 16244 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16245 Name: "nginx", 16246 Ready: true, 16247 Started: proto.Bool(true), 16248 State: core.ContainerState{ 16249 Running: &core.ContainerStateRunning{ 16250 StartedAt: metav1.NewTime(time.Now()), 16251 }, 16252 }, 16253 }}, 16254 }, 16255 }, 16256 core.Pod{ 16257 ObjectMeta: metav1.ObjectMeta{ 16258 Name: "foo", 16259 }, 16260 Spec: core.PodSpec{ 16261 InitContainers: []core.Container{ 16262 { 16263 Name: "restartable-init", 16264 RestartPolicy: &containerRestartPolicyAlways, 16265 }, 16266 }, 16267 Containers: []core.Container{ 16268 { 16269 Name: "nginx", 16270 }, 16271 }, 16272 RestartPolicy: core.RestartPolicyOnFailure, 16273 }, 16274 Status: core.PodStatus{ 16275 InitContainerStatuses: []core.ContainerStatus{{ 16276 ContainerID: "docker://numbers", 16277 Image: "alpine", 16278 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16279 Name: "restartable-init", 16280 Ready: false, 16281 State: core.ContainerState{ 16282 Terminated: &core.ContainerStateTerminated{ 16283 ContainerID: "docker://numbers", 16284 Reason: "Completed", 16285 }, 16286 }, 16287 }}, 16288 ContainerStatuses: []core.ContainerStatus{{ 16289 ContainerID: "docker://numbers", 16290 Image: "nginx:alpine", 16291 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16292 Name: "nginx", 16293 Ready: true, 16294 Started: proto.Bool(true), 16295 State: core.ContainerState{ 16296 Running: &core.ContainerStateRunning{ 16297 StartedAt: metav1.NewTime(time.Now()), 16298 }, 16299 }, 16300 }}, 16301 }, 16302 }, 16303 "", 16304 "restartable init container can restart if RestartPolicyOnFailure", 16305 }, { 16306 core.Pod{ 16307 ObjectMeta: metav1.ObjectMeta{ 16308 Name: "foo", 16309 }, 16310 Spec: core.PodSpec{ 16311 InitContainers: []core.Container{ 16312 { 16313 Name: "restartable-init", 16314 RestartPolicy: &containerRestartPolicyAlways, 16315 }, 16316 }, 16317 Containers: []core.Container{ 16318 { 16319 Name: "nginx", 16320 }, 16321 }, 16322 RestartPolicy: core.RestartPolicyAlways, 16323 }, 16324 Status: core.PodStatus{ 16325 InitContainerStatuses: []core.ContainerStatus{{ 16326 ContainerID: "docker://numbers", 16327 Image: "alpine", 16328 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16329 Name: "restartable-init", 16330 Ready: true, 16331 State: core.ContainerState{ 16332 Running: &core.ContainerStateRunning{ 16333 StartedAt: metav1.NewTime(time.Now()), 16334 }, 16335 }, 16336 }}, 16337 ContainerStatuses: []core.ContainerStatus{{ 16338 ContainerID: "docker://numbers", 16339 Image: "nginx:alpine", 16340 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16341 Name: "nginx", 16342 Ready: true, 16343 Started: proto.Bool(true), 16344 State: core.ContainerState{ 16345 Running: &core.ContainerStateRunning{ 16346 StartedAt: metav1.NewTime(time.Now()), 16347 }, 16348 }, 16349 }}, 16350 }, 16351 }, 16352 core.Pod{ 16353 ObjectMeta: metav1.ObjectMeta{ 16354 Name: "foo", 16355 }, 16356 Spec: core.PodSpec{ 16357 InitContainers: []core.Container{ 16358 { 16359 Name: "restartable-init", 16360 RestartPolicy: &containerRestartPolicyAlways, 16361 }, 16362 }, 16363 Containers: []core.Container{ 16364 { 16365 Name: "nginx", 16366 }, 16367 }, 16368 RestartPolicy: core.RestartPolicyAlways, 16369 }, 16370 Status: core.PodStatus{ 16371 InitContainerStatuses: []core.ContainerStatus{{ 16372 ContainerID: "docker://numbers", 16373 Image: "alpine", 16374 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16375 Name: "restartable-init", 16376 Ready: false, 16377 State: core.ContainerState{ 16378 Terminated: &core.ContainerStateTerminated{ 16379 ContainerID: "docker://numbers", 16380 Reason: "Completed", 16381 }, 16382 }, 16383 }}, 16384 ContainerStatuses: []core.ContainerStatus{{ 16385 ContainerID: "docker://numbers", 16386 Image: "nginx:alpine", 16387 ImageID: "docker-pullable://nginx@sha256:d0gf00d", 16388 Name: "nginx", 16389 Ready: true, 16390 Started: proto.Bool(true), 16391 State: core.ContainerState{ 16392 Running: &core.ContainerStateRunning{ 16393 StartedAt: metav1.NewTime(time.Now()), 16394 }, 16395 }, 16396 }}, 16397 }, 16398 }, 16399 "", 16400 "restartable init container can restart if RestartPolicyAlways", 16401 }, 16402 } 16403 16404 for _, test := range tests { 16405 test.new.ObjectMeta.ResourceVersion = "1" 16406 test.old.ObjectMeta.ResourceVersion = "1" 16407 errs := ValidatePodStatusUpdate(&test.new, &test.old, PodValidationOptions{}) 16408 if test.err == "" { 16409 if len(errs) != 0 { 16410 t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.new, test.old) 16411 } 16412 } else { 16413 if len(errs) == 0 { 16414 t.Errorf("unexpected valid: %s\nA: %+v\nB: %+v", test.test, test.new, test.old) 16415 } else if actualErr := errs.ToAggregate().Error(); !strings.Contains(actualErr, test.err) { 16416 t.Errorf("unexpected error message: %s\nExpected error: %s\nActual error: %s", test.test, test.err, actualErr) 16417 } 16418 } 16419 } 16420 } 16421 16422 func makeValidService() core.Service { 16423 clusterInternalTrafficPolicy := core.ServiceInternalTrafficPolicyCluster 16424 return core.Service{ 16425 ObjectMeta: metav1.ObjectMeta{ 16426 Name: "valid", 16427 Namespace: "valid", 16428 Labels: map[string]string{}, 16429 Annotations: map[string]string{}, 16430 ResourceVersion: "1", 16431 }, 16432 Spec: core.ServiceSpec{ 16433 Selector: map[string]string{"key": "val"}, 16434 SessionAffinity: "None", 16435 Type: core.ServiceTypeClusterIP, 16436 Ports: []core.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt32(8675)}}, 16437 InternalTrafficPolicy: &clusterInternalTrafficPolicy, 16438 }, 16439 } 16440 } 16441 16442 func TestValidatePodEphemeralContainersUpdate(t *testing.T) { 16443 makePod := func(ephemeralContainers []core.EphemeralContainer) *core.Pod { 16444 return &core.Pod{ 16445 ObjectMeta: metav1.ObjectMeta{ 16446 Annotations: map[string]string{}, 16447 Labels: map[string]string{}, 16448 Name: "pod", 16449 Namespace: "ns", 16450 ResourceVersion: "1", 16451 }, 16452 Spec: core.PodSpec{ 16453 Containers: []core.Container{{ 16454 Name: "cnt", 16455 Image: "image", 16456 ImagePullPolicy: "IfNotPresent", 16457 TerminationMessagePolicy: "File", 16458 }}, 16459 DNSPolicy: core.DNSClusterFirst, 16460 EphemeralContainers: ephemeralContainers, 16461 RestartPolicy: core.RestartPolicyOnFailure, 16462 }, 16463 } 16464 } 16465 16466 // Some tests use Windows host pods as an example of fields that might 16467 // conflict between an ephemeral container and the rest of the pod. 16468 capabilities.SetForTests(capabilities.Capabilities{ 16469 AllowPrivileged: true, 16470 }) 16471 makeWindowsHostPod := func(ephemeralContainers []core.EphemeralContainer) *core.Pod { 16472 return &core.Pod{ 16473 ObjectMeta: metav1.ObjectMeta{ 16474 Annotations: map[string]string{}, 16475 Labels: map[string]string{}, 16476 Name: "pod", 16477 Namespace: "ns", 16478 ResourceVersion: "1", 16479 }, 16480 Spec: core.PodSpec{ 16481 Containers: []core.Container{{ 16482 Name: "cnt", 16483 Image: "image", 16484 ImagePullPolicy: "IfNotPresent", 16485 SecurityContext: &core.SecurityContext{ 16486 WindowsOptions: &core.WindowsSecurityContextOptions{ 16487 HostProcess: proto.Bool(true), 16488 }, 16489 }, 16490 TerminationMessagePolicy: "File", 16491 }}, 16492 DNSPolicy: core.DNSClusterFirst, 16493 EphemeralContainers: ephemeralContainers, 16494 RestartPolicy: core.RestartPolicyOnFailure, 16495 SecurityContext: &core.PodSecurityContext{ 16496 HostNetwork: true, 16497 WindowsOptions: &core.WindowsSecurityContextOptions{ 16498 HostProcess: proto.Bool(true), 16499 }, 16500 }, 16501 }, 16502 } 16503 } 16504 16505 tests := []struct { 16506 name string 16507 new, old *core.Pod 16508 err string 16509 }{{ 16510 "no ephemeral containers", 16511 makePod([]core.EphemeralContainer{}), 16512 makePod([]core.EphemeralContainer{}), 16513 "", 16514 }, { 16515 "No change in Ephemeral Containers", 16516 makePod([]core.EphemeralContainer{{ 16517 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16518 Name: "debugger", 16519 Image: "busybox", 16520 ImagePullPolicy: "IfNotPresent", 16521 TerminationMessagePolicy: "File", 16522 }, 16523 }, { 16524 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16525 Name: "debugger2", 16526 Image: "busybox", 16527 ImagePullPolicy: "IfNotPresent", 16528 TerminationMessagePolicy: "File", 16529 }, 16530 }}), 16531 makePod([]core.EphemeralContainer{{ 16532 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16533 Name: "debugger", 16534 Image: "busybox", 16535 ImagePullPolicy: "IfNotPresent", 16536 TerminationMessagePolicy: "File", 16537 }, 16538 }, { 16539 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16540 Name: "debugger2", 16541 Image: "busybox", 16542 ImagePullPolicy: "IfNotPresent", 16543 TerminationMessagePolicy: "File", 16544 }, 16545 }}), 16546 "", 16547 }, { 16548 "Ephemeral Container list order changes", 16549 makePod([]core.EphemeralContainer{{ 16550 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16551 Name: "debugger", 16552 Image: "busybox", 16553 ImagePullPolicy: "IfNotPresent", 16554 TerminationMessagePolicy: "File", 16555 }, 16556 }, { 16557 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16558 Name: "debugger2", 16559 Image: "busybox", 16560 ImagePullPolicy: "IfNotPresent", 16561 TerminationMessagePolicy: "File", 16562 }, 16563 }}), 16564 makePod([]core.EphemeralContainer{{ 16565 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16566 Name: "debugger2", 16567 Image: "busybox", 16568 ImagePullPolicy: "IfNotPresent", 16569 TerminationMessagePolicy: "File", 16570 }, 16571 }, { 16572 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16573 Name: "debugger", 16574 Image: "busybox", 16575 ImagePullPolicy: "IfNotPresent", 16576 TerminationMessagePolicy: "File", 16577 }, 16578 }}), 16579 "", 16580 }, { 16581 "Add an Ephemeral Container", 16582 makePod([]core.EphemeralContainer{{ 16583 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16584 Name: "debugger", 16585 Image: "busybox", 16586 ImagePullPolicy: "IfNotPresent", 16587 TerminationMessagePolicy: "File", 16588 }, 16589 }}), 16590 makePod([]core.EphemeralContainer{}), 16591 "", 16592 }, { 16593 "Add two Ephemeral Containers", 16594 makePod([]core.EphemeralContainer{{ 16595 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16596 Name: "debugger1", 16597 Image: "busybox", 16598 ImagePullPolicy: "IfNotPresent", 16599 TerminationMessagePolicy: "File", 16600 }, 16601 }, { 16602 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16603 Name: "debugger2", 16604 Image: "busybox", 16605 ImagePullPolicy: "IfNotPresent", 16606 TerminationMessagePolicy: "File", 16607 }, 16608 }}), 16609 makePod([]core.EphemeralContainer{}), 16610 "", 16611 }, { 16612 "Add to an existing Ephemeral Containers", 16613 makePod([]core.EphemeralContainer{{ 16614 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16615 Name: "debugger", 16616 Image: "busybox", 16617 ImagePullPolicy: "IfNotPresent", 16618 TerminationMessagePolicy: "File", 16619 }, 16620 }, { 16621 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16622 Name: "debugger2", 16623 Image: "busybox", 16624 ImagePullPolicy: "IfNotPresent", 16625 TerminationMessagePolicy: "File", 16626 }, 16627 }}), 16628 makePod([]core.EphemeralContainer{{ 16629 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16630 Name: "debugger", 16631 Image: "busybox", 16632 ImagePullPolicy: "IfNotPresent", 16633 TerminationMessagePolicy: "File", 16634 }, 16635 }}), 16636 "", 16637 }, { 16638 "Add to an existing Ephemeral Containers, list order changes", 16639 makePod([]core.EphemeralContainer{{ 16640 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16641 Name: "debugger3", 16642 Image: "busybox", 16643 ImagePullPolicy: "IfNotPresent", 16644 TerminationMessagePolicy: "File", 16645 }, 16646 }, { 16647 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16648 Name: "debugger2", 16649 Image: "busybox", 16650 ImagePullPolicy: "IfNotPresent", 16651 TerminationMessagePolicy: "File", 16652 }, 16653 }, { 16654 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16655 Name: "debugger", 16656 Image: "busybox", 16657 ImagePullPolicy: "IfNotPresent", 16658 TerminationMessagePolicy: "File", 16659 }, 16660 }}), 16661 makePod([]core.EphemeralContainer{{ 16662 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16663 Name: "debugger", 16664 Image: "busybox", 16665 ImagePullPolicy: "IfNotPresent", 16666 TerminationMessagePolicy: "File", 16667 }, 16668 }, { 16669 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16670 Name: "debugger2", 16671 Image: "busybox", 16672 ImagePullPolicy: "IfNotPresent", 16673 TerminationMessagePolicy: "File", 16674 }, 16675 }}), 16676 "", 16677 }, { 16678 "Remove an Ephemeral Container", 16679 makePod([]core.EphemeralContainer{}), 16680 makePod([]core.EphemeralContainer{{ 16681 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16682 Name: "debugger", 16683 Image: "busybox", 16684 ImagePullPolicy: "IfNotPresent", 16685 TerminationMessagePolicy: "File", 16686 }, 16687 }}), 16688 "may not be removed", 16689 }, { 16690 "Replace an Ephemeral Container", 16691 makePod([]core.EphemeralContainer{{ 16692 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16693 Name: "firstone", 16694 Image: "busybox", 16695 ImagePullPolicy: "IfNotPresent", 16696 TerminationMessagePolicy: "File", 16697 }, 16698 }}), 16699 makePod([]core.EphemeralContainer{{ 16700 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16701 Name: "thentheother", 16702 Image: "busybox", 16703 ImagePullPolicy: "IfNotPresent", 16704 TerminationMessagePolicy: "File", 16705 }, 16706 }}), 16707 "may not be removed", 16708 }, { 16709 "Change an Ephemeral Containers", 16710 makePod([]core.EphemeralContainer{{ 16711 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16712 Name: "debugger1", 16713 Image: "busybox", 16714 ImagePullPolicy: "IfNotPresent", 16715 TerminationMessagePolicy: "File", 16716 }, 16717 }, { 16718 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16719 Name: "debugger2", 16720 Image: "busybox", 16721 ImagePullPolicy: "IfNotPresent", 16722 TerminationMessagePolicy: "File", 16723 }, 16724 }}), 16725 makePod([]core.EphemeralContainer{{ 16726 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16727 Name: "debugger1", 16728 Image: "debian", 16729 ImagePullPolicy: "IfNotPresent", 16730 TerminationMessagePolicy: "File", 16731 }, 16732 }, { 16733 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16734 Name: "debugger2", 16735 Image: "busybox", 16736 ImagePullPolicy: "IfNotPresent", 16737 TerminationMessagePolicy: "File", 16738 }, 16739 }}), 16740 "may not be changed", 16741 }, { 16742 "Ephemeral container with potential conflict with regular containers, but conflict not present", 16743 makeWindowsHostPod([]core.EphemeralContainer{{ 16744 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16745 Name: "debugger1", 16746 Image: "image", 16747 ImagePullPolicy: "IfNotPresent", 16748 SecurityContext: &core.SecurityContext{ 16749 WindowsOptions: &core.WindowsSecurityContextOptions{ 16750 HostProcess: proto.Bool(true), 16751 }, 16752 }, 16753 TerminationMessagePolicy: "File", 16754 }, 16755 }}), 16756 makeWindowsHostPod(nil), 16757 "", 16758 }, { 16759 "Ephemeral container with potential conflict with regular containers, and conflict is present", 16760 makeWindowsHostPod([]core.EphemeralContainer{{ 16761 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16762 Name: "debugger1", 16763 Image: "image", 16764 ImagePullPolicy: "IfNotPresent", 16765 SecurityContext: &core.SecurityContext{ 16766 WindowsOptions: &core.WindowsSecurityContextOptions{ 16767 HostProcess: proto.Bool(false), 16768 }, 16769 }, 16770 TerminationMessagePolicy: "File", 16771 }, 16772 }}), 16773 makeWindowsHostPod(nil), 16774 "spec.ephemeralContainers[0].securityContext.windowsOptions.hostProcess: Invalid value: false: pod hostProcess value must be identical", 16775 }, { 16776 "Add ephemeral container to static pod", 16777 func() *core.Pod { 16778 p := makePod(nil) 16779 p.Spec.NodeName = "some-name" 16780 p.ObjectMeta.Annotations = map[string]string{ 16781 core.MirrorPodAnnotationKey: "foo", 16782 } 16783 p.Spec.EphemeralContainers = []core.EphemeralContainer{{ 16784 EphemeralContainerCommon: core.EphemeralContainerCommon{ 16785 Name: "debugger1", 16786 Image: "debian", 16787 ImagePullPolicy: "IfNotPresent", 16788 TerminationMessagePolicy: "File", 16789 }, 16790 }} 16791 return p 16792 }(), 16793 func() *core.Pod { 16794 p := makePod(nil) 16795 p.Spec.NodeName = "some-name" 16796 p.ObjectMeta.Annotations = map[string]string{ 16797 core.MirrorPodAnnotationKey: "foo", 16798 } 16799 return p 16800 }(), 16801 "Forbidden: static pods do not support ephemeral containers", 16802 }, 16803 } 16804 16805 for _, tc := range tests { 16806 errs := ValidatePodEphemeralContainersUpdate(tc.new, tc.old, PodValidationOptions{}) 16807 if tc.err == "" { 16808 if len(errs) != 0 { 16809 t.Errorf("unexpected invalid for test: %s\nErrors returned: %+v\nLocal diff of test objects (-old +new):\n%s", tc.name, errs, cmp.Diff(tc.old, tc.new)) 16810 } 16811 } else { 16812 if len(errs) == 0 { 16813 t.Errorf("unexpected valid for test: %s\nLocal diff of test objects (-old +new):\n%s", tc.name, cmp.Diff(tc.old, tc.new)) 16814 } else if actualErr := errs.ToAggregate().Error(); !strings.Contains(actualErr, tc.err) { 16815 t.Errorf("unexpected error message: %s\nExpected error: %s\nActual error: %s", tc.name, tc.err, actualErr) 16816 } 16817 } 16818 } 16819 } 16820 16821 func TestValidateServiceCreate(t *testing.T) { 16822 requireDualStack := core.IPFamilyPolicyRequireDualStack 16823 singleStack := core.IPFamilyPolicySingleStack 16824 preferDualStack := core.IPFamilyPolicyPreferDualStack 16825 16826 testCases := []struct { 16827 name string 16828 tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it 16829 numErrs int 16830 featureGates []featuregate.Feature 16831 }{{ 16832 name: "missing namespace", 16833 tweakSvc: func(s *core.Service) { 16834 s.Namespace = "" 16835 }, 16836 numErrs: 1, 16837 }, { 16838 name: "invalid namespace", 16839 tweakSvc: func(s *core.Service) { 16840 s.Namespace = "-123" 16841 }, 16842 numErrs: 1, 16843 }, { 16844 name: "missing name", 16845 tweakSvc: func(s *core.Service) { 16846 s.Name = "" 16847 }, 16848 numErrs: 1, 16849 }, { 16850 name: "invalid name", 16851 tweakSvc: func(s *core.Service) { 16852 s.Name = "-123" 16853 }, 16854 numErrs: 1, 16855 }, { 16856 name: "too long name", 16857 tweakSvc: func(s *core.Service) { 16858 s.Name = strings.Repeat("a", 64) 16859 }, 16860 numErrs: 1, 16861 }, { 16862 name: "invalid generateName", 16863 tweakSvc: func(s *core.Service) { 16864 s.GenerateName = "-123" 16865 }, 16866 numErrs: 1, 16867 }, { 16868 name: "too long generateName", 16869 tweakSvc: func(s *core.Service) { 16870 s.GenerateName = strings.Repeat("a", 64) 16871 }, 16872 numErrs: 1, 16873 }, { 16874 name: "invalid label", 16875 tweakSvc: func(s *core.Service) { 16876 s.Labels["NoUppercaseOrSpecialCharsLike=Equals"] = "bar" 16877 }, 16878 numErrs: 1, 16879 }, { 16880 name: "invalid annotation", 16881 tweakSvc: func(s *core.Service) { 16882 s.Annotations["NoSpecialCharsLike=Equals"] = "bar" 16883 }, 16884 numErrs: 1, 16885 }, { 16886 name: "nil selector", 16887 tweakSvc: func(s *core.Service) { 16888 s.Spec.Selector = nil 16889 }, 16890 numErrs: 0, 16891 }, { 16892 name: "invalid selector", 16893 tweakSvc: func(s *core.Service) { 16894 s.Spec.Selector["NoSpecialCharsLike=Equals"] = "bar" 16895 }, 16896 numErrs: 1, 16897 }, { 16898 name: "missing session affinity", 16899 tweakSvc: func(s *core.Service) { 16900 s.Spec.SessionAffinity = "" 16901 }, 16902 numErrs: 1, 16903 }, { 16904 name: "missing type", 16905 tweakSvc: func(s *core.Service) { 16906 s.Spec.Type = "" 16907 }, 16908 numErrs: 1, 16909 }, { 16910 name: "missing ports", 16911 tweakSvc: func(s *core.Service) { 16912 s.Spec.Ports = nil 16913 }, 16914 numErrs: 1, 16915 }, { 16916 name: "missing ports but headless", 16917 tweakSvc: func(s *core.Service) { 16918 s.Spec.Ports = nil 16919 s.Spec.ClusterIP = core.ClusterIPNone 16920 s.Spec.ClusterIPs = []string{core.ClusterIPNone} 16921 }, 16922 numErrs: 0, 16923 }, { 16924 name: "empty port[0] name", 16925 tweakSvc: func(s *core.Service) { 16926 s.Spec.Ports[0].Name = "" 16927 }, 16928 numErrs: 0, 16929 }, { 16930 name: "empty port[1] name", 16931 tweakSvc: func(s *core.Service) { 16932 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "", Protocol: "TCP", Port: 12345, TargetPort: intstr.FromInt32(12345)}) 16933 }, 16934 numErrs: 1, 16935 }, { 16936 name: "empty multi-port port[0] name", 16937 tweakSvc: func(s *core.Service) { 16938 s.Spec.Ports[0].Name = "" 16939 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "p", Protocol: "TCP", Port: 12345, TargetPort: intstr.FromInt32(12345)}) 16940 }, 16941 numErrs: 1, 16942 }, { 16943 name: "invalid port name", 16944 tweakSvc: func(s *core.Service) { 16945 s.Spec.Ports[0].Name = "INVALID" 16946 }, 16947 numErrs: 1, 16948 }, { 16949 name: "missing protocol", 16950 tweakSvc: func(s *core.Service) { 16951 s.Spec.Ports[0].Protocol = "" 16952 }, 16953 numErrs: 1, 16954 }, { 16955 name: "invalid protocol", 16956 tweakSvc: func(s *core.Service) { 16957 s.Spec.Ports[0].Protocol = "INVALID" 16958 }, 16959 numErrs: 1, 16960 }, { 16961 name: "invalid cluster ip", 16962 tweakSvc: func(s *core.Service) { 16963 s.Spec.ClusterIP = "invalid" 16964 s.Spec.ClusterIPs = []string{"invalid"} 16965 }, 16966 numErrs: 1, 16967 }, { 16968 name: "missing port", 16969 tweakSvc: func(s *core.Service) { 16970 s.Spec.Ports[0].Port = 0 16971 }, 16972 numErrs: 1, 16973 }, { 16974 name: "invalid port", 16975 tweakSvc: func(s *core.Service) { 16976 s.Spec.Ports[0].Port = 65536 16977 }, 16978 numErrs: 1, 16979 }, { 16980 name: "invalid TargetPort int", 16981 tweakSvc: func(s *core.Service) { 16982 s.Spec.Ports[0].TargetPort = intstr.FromInt32(65536) 16983 }, 16984 numErrs: 1, 16985 }, { 16986 name: "valid port headless", 16987 tweakSvc: func(s *core.Service) { 16988 s.Spec.Ports[0].Port = 11722 16989 s.Spec.Ports[0].TargetPort = intstr.FromInt32(11722) 16990 s.Spec.ClusterIP = core.ClusterIPNone 16991 s.Spec.ClusterIPs = []string{core.ClusterIPNone} 16992 }, 16993 numErrs: 0, 16994 }, { 16995 name: "invalid port headless 1", 16996 tweakSvc: func(s *core.Service) { 16997 s.Spec.Ports[0].Port = 11722 16998 s.Spec.Ports[0].TargetPort = intstr.FromInt32(11721) 16999 s.Spec.ClusterIP = core.ClusterIPNone 17000 s.Spec.ClusterIPs = []string{core.ClusterIPNone} 17001 }, 17002 // in the v1 API, targetPorts on headless services were tolerated. 17003 // once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility. 17004 // numErrs: 1, 17005 numErrs: 0, 17006 }, { 17007 name: "invalid port headless 2", 17008 tweakSvc: func(s *core.Service) { 17009 s.Spec.Ports[0].Port = 11722 17010 s.Spec.Ports[0].TargetPort = intstr.FromString("target") 17011 s.Spec.ClusterIP = core.ClusterIPNone 17012 s.Spec.ClusterIPs = []string{core.ClusterIPNone} 17013 }, 17014 // in the v1 API, targetPorts on headless services were tolerated. 17015 // once we have version-specific validation, we can reject this on newer API versions, but until then, we have to tolerate it for compatibility. 17016 // numErrs: 1, 17017 numErrs: 0, 17018 }, { 17019 name: "invalid publicIPs localhost", 17020 tweakSvc: func(s *core.Service) { 17021 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17022 s.Spec.ExternalIPs = []string{"127.0.0.1"} 17023 }, 17024 numErrs: 1, 17025 }, { 17026 name: "invalid publicIPs unspecified", 17027 tweakSvc: func(s *core.Service) { 17028 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17029 s.Spec.ExternalIPs = []string{"0.0.0.0"} 17030 }, 17031 numErrs: 1, 17032 }, { 17033 name: "invalid publicIPs loopback", 17034 tweakSvc: func(s *core.Service) { 17035 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17036 s.Spec.ExternalIPs = []string{"127.0.0.1"} 17037 }, 17038 numErrs: 1, 17039 }, { 17040 name: "invalid publicIPs host", 17041 tweakSvc: func(s *core.Service) { 17042 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17043 s.Spec.ExternalIPs = []string{"myhost.mydomain"} 17044 }, 17045 numErrs: 1, 17046 }, { 17047 name: "valid publicIPs", 17048 tweakSvc: func(s *core.Service) { 17049 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17050 s.Spec.ExternalIPs = []string{"1.2.3.4"} 17051 }, 17052 numErrs: 0, 17053 }, { 17054 name: "dup port name", 17055 tweakSvc: func(s *core.Service) { 17056 s.Spec.Ports[0].Name = "p" 17057 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "p", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17058 }, 17059 numErrs: 1, 17060 }, { 17061 name: "valid load balancer protocol UDP 1", 17062 tweakSvc: func(s *core.Service) { 17063 s.Spec.Type = core.ServiceTypeLoadBalancer 17064 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17065 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17066 s.Spec.Ports[0].Protocol = "UDP" 17067 }, 17068 numErrs: 0, 17069 }, { 17070 name: "valid load balancer protocol UDP 2", 17071 tweakSvc: func(s *core.Service) { 17072 s.Spec.Type = core.ServiceTypeLoadBalancer 17073 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17074 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17075 s.Spec.Ports[0] = core.ServicePort{Name: "q", Port: 12345, Protocol: "UDP", TargetPort: intstr.FromInt32(12345)} 17076 }, 17077 numErrs: 0, 17078 }, { 17079 name: "load balancer with mix protocol", 17080 tweakSvc: func(s *core.Service) { 17081 s.Spec.Type = core.ServiceTypeLoadBalancer 17082 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17083 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17084 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "UDP", TargetPort: intstr.FromInt32(12345)}) 17085 }, 17086 numErrs: 0, 17087 }, { 17088 name: "valid 1", 17089 tweakSvc: func(s *core.Service) { 17090 // do nothing 17091 }, 17092 numErrs: 0, 17093 }, { 17094 name: "valid 2", 17095 tweakSvc: func(s *core.Service) { 17096 s.Spec.Ports[0].Protocol = "UDP" 17097 s.Spec.Ports[0].TargetPort = intstr.FromInt32(12345) 17098 }, 17099 numErrs: 0, 17100 }, { 17101 name: "valid 3", 17102 tweakSvc: func(s *core.Service) { 17103 s.Spec.Ports[0].TargetPort = intstr.FromString("http") 17104 }, 17105 numErrs: 0, 17106 }, { 17107 name: "valid cluster ip - none ", 17108 tweakSvc: func(s *core.Service) { 17109 s.Spec.ClusterIP = core.ClusterIPNone 17110 s.Spec.ClusterIPs = []string{core.ClusterIPNone} 17111 }, 17112 numErrs: 0, 17113 }, { 17114 name: "valid cluster ip - empty", 17115 tweakSvc: func(s *core.Service) { 17116 s.Spec.ClusterIPs = nil 17117 s.Spec.Ports[0].TargetPort = intstr.FromString("http") 17118 }, 17119 numErrs: 0, 17120 }, { 17121 name: "valid type - clusterIP", 17122 tweakSvc: func(s *core.Service) { 17123 s.Spec.Type = core.ServiceTypeClusterIP 17124 }, 17125 numErrs: 0, 17126 }, { 17127 name: "valid type - loadbalancer", 17128 tweakSvc: func(s *core.Service) { 17129 s.Spec.Type = core.ServiceTypeLoadBalancer 17130 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17131 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17132 }, 17133 numErrs: 0, 17134 }, { 17135 name: "valid type - loadbalancer with allocateLoadBalancerNodePorts=false", 17136 tweakSvc: func(s *core.Service) { 17137 s.Spec.Type = core.ServiceTypeLoadBalancer 17138 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17139 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(false) 17140 }, 17141 numErrs: 0, 17142 }, { 17143 name: "invalid type - missing AllocateLoadBalancerNodePorts for loadbalancer type", 17144 tweakSvc: func(s *core.Service) { 17145 s.Spec.Type = core.ServiceTypeLoadBalancer 17146 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17147 }, 17148 numErrs: 1, 17149 }, { 17150 name: "valid type loadbalancer 2 ports", 17151 tweakSvc: func(s *core.Service) { 17152 s.Spec.Type = core.ServiceTypeLoadBalancer 17153 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17154 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17155 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17156 }, 17157 numErrs: 0, 17158 }, { 17159 name: "valid external load balancer 2 ports", 17160 tweakSvc: func(s *core.Service) { 17161 s.Spec.Type = core.ServiceTypeLoadBalancer 17162 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17163 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17164 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17165 }, 17166 numErrs: 0, 17167 }, { 17168 name: "duplicate nodeports", 17169 tweakSvc: func(s *core.Service) { 17170 s.Spec.Type = core.ServiceTypeNodePort 17171 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17172 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 17173 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "r", Port: 2, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(2)}) 17174 }, 17175 numErrs: 1, 17176 }, { 17177 name: "duplicate nodeports (different protocols)", 17178 tweakSvc: func(s *core.Service) { 17179 s.Spec.Type = core.ServiceTypeNodePort 17180 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17181 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 17182 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "r", Port: 2, Protocol: "UDP", NodePort: 1, TargetPort: intstr.FromInt32(2)}) 17183 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "s", Port: 3, Protocol: "SCTP", NodePort: 1, TargetPort: intstr.FromInt32(3)}) 17184 }, 17185 numErrs: 0, 17186 }, { 17187 name: "invalid duplicate ports (with same protocol)", 17188 tweakSvc: func(s *core.Service) { 17189 s.Spec.Type = core.ServiceTypeClusterIP 17190 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(8080)}) 17191 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "r", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(80)}) 17192 }, 17193 numErrs: 1, 17194 }, { 17195 name: "valid duplicate ports (with different protocols)", 17196 tweakSvc: func(s *core.Service) { 17197 s.Spec.Type = core.ServiceTypeClusterIP 17198 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(8080)}) 17199 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "r", Port: 12345, Protocol: "UDP", TargetPort: intstr.FromInt32(80)}) 17200 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "s", Port: 12345, Protocol: "SCTP", TargetPort: intstr.FromInt32(8088)}) 17201 }, 17202 numErrs: 0, 17203 }, { 17204 name: "valid type - cluster", 17205 tweakSvc: func(s *core.Service) { 17206 s.Spec.Type = core.ServiceTypeClusterIP 17207 }, 17208 numErrs: 0, 17209 }, { 17210 name: "valid type - nodeport", 17211 tweakSvc: func(s *core.Service) { 17212 s.Spec.Type = core.ServiceTypeNodePort 17213 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17214 }, 17215 numErrs: 0, 17216 }, { 17217 name: "valid type - loadbalancer with allocateLoadBalancerNodePorts=true", 17218 tweakSvc: func(s *core.Service) { 17219 s.Spec.Type = core.ServiceTypeLoadBalancer 17220 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17221 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17222 }, 17223 numErrs: 0, 17224 }, { 17225 name: "valid type loadbalancer 2 ports", 17226 tweakSvc: func(s *core.Service) { 17227 s.Spec.Type = core.ServiceTypeLoadBalancer 17228 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17229 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17230 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17231 }, 17232 numErrs: 0, 17233 }, { 17234 name: "valid type loadbalancer with NodePort", 17235 tweakSvc: func(s *core.Service) { 17236 s.Spec.Type = core.ServiceTypeLoadBalancer 17237 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17238 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17239 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt32(12345)}) 17240 }, 17241 numErrs: 0, 17242 }, { 17243 name: "valid type=NodePort service with NodePort", 17244 tweakSvc: func(s *core.Service) { 17245 s.Spec.Type = core.ServiceTypeNodePort 17246 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17247 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt32(12345)}) 17248 }, 17249 numErrs: 0, 17250 }, { 17251 name: "valid type=NodePort service without NodePort", 17252 tweakSvc: func(s *core.Service) { 17253 s.Spec.Type = core.ServiceTypeNodePort 17254 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17255 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17256 }, 17257 numErrs: 0, 17258 }, { 17259 name: "valid cluster service without NodePort", 17260 tweakSvc: func(s *core.Service) { 17261 s.Spec.Type = core.ServiceTypeClusterIP 17262 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17263 }, 17264 numErrs: 0, 17265 }, { 17266 name: "invalid cluster service with NodePort", 17267 tweakSvc: func(s *core.Service) { 17268 s.Spec.Type = core.ServiceTypeClusterIP 17269 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345, TargetPort: intstr.FromInt32(12345)}) 17270 }, 17271 numErrs: 1, 17272 }, { 17273 name: "invalid public service with duplicate NodePort", 17274 tweakSvc: func(s *core.Service) { 17275 s.Spec.Type = core.ServiceTypeNodePort 17276 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17277 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "p1", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 17278 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "p2", Port: 2, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(2)}) 17279 }, 17280 numErrs: 1, 17281 }, { 17282 name: "valid type=LoadBalancer", 17283 tweakSvc: func(s *core.Service) { 17284 s.Spec.Type = core.ServiceTypeLoadBalancer 17285 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17286 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17287 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17288 }, 17289 numErrs: 0, 17290 }, { 17291 // For now we open firewalls, and its insecure if we open 10250, remove this 17292 // when we have better protections in place. 17293 name: "invalid port type=LoadBalancer", 17294 tweakSvc: func(s *core.Service) { 17295 s.Spec.Type = core.ServiceTypeLoadBalancer 17296 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17297 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17298 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "kubelet", Port: 10250, Protocol: "TCP", TargetPort: intstr.FromInt32(12345)}) 17299 }, 17300 numErrs: 1, 17301 }, { 17302 name: "valid LoadBalancer source range annotation", 17303 tweakSvc: func(s *core.Service) { 17304 s.Spec.Type = core.ServiceTypeLoadBalancer 17305 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17306 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17307 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "1.2.3.0/24, 5.6.0.0/16" 17308 }, 17309 numErrs: 0, 17310 }, { 17311 name: "valid empty LoadBalancer source range annotation", 17312 tweakSvc: func(s *core.Service) { 17313 s.Spec.Type = core.ServiceTypeLoadBalancer 17314 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17315 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17316 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "" 17317 }, 17318 numErrs: 0, 17319 }, { 17320 name: "valid whitespace-only LoadBalancer source range annotation", 17321 tweakSvc: func(s *core.Service) { 17322 s.Spec.Type = core.ServiceTypeLoadBalancer 17323 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17324 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17325 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = " " 17326 }, 17327 numErrs: 0, 17328 }, { 17329 name: "invalid LoadBalancer source range annotation (hostname)", 17330 tweakSvc: func(s *core.Service) { 17331 s.Spec.Type = core.ServiceTypeLoadBalancer 17332 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17333 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17334 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "foo.bar" 17335 }, 17336 numErrs: 1, 17337 }, { 17338 name: "invalid LoadBalancer source range annotation (invalid CIDR)", 17339 tweakSvc: func(s *core.Service) { 17340 s.Spec.Type = core.ServiceTypeLoadBalancer 17341 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17342 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17343 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "1.2.3.4/33" 17344 }, 17345 numErrs: 1, 17346 }, { 17347 name: "invalid LoadBalancer source range annotation for non LoadBalancer type service", 17348 tweakSvc: func(s *core.Service) { 17349 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "1.2.3.0/24" 17350 }, 17351 numErrs: 1, 17352 }, { 17353 name: "invalid empty-but-set LoadBalancer source range annotation for non LoadBalancer type service", 17354 tweakSvc: func(s *core.Service) { 17355 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "" 17356 }, 17357 numErrs: 1, 17358 }, { 17359 name: "valid LoadBalancer source range", 17360 tweakSvc: func(s *core.Service) { 17361 s.Spec.Type = core.ServiceTypeLoadBalancer 17362 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17363 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17364 s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.0/24", "5.6.0.0/16"} 17365 }, 17366 numErrs: 0, 17367 }, { 17368 name: "valid LoadBalancer source range with whitespace", 17369 tweakSvc: func(s *core.Service) { 17370 s.Spec.Type = core.ServiceTypeLoadBalancer 17371 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17372 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17373 s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.0/24 ", " 5.6.0.0/16"} 17374 }, 17375 numErrs: 0, 17376 }, { 17377 name: "invalid empty LoadBalancer source range", 17378 tweakSvc: func(s *core.Service) { 17379 s.Spec.Type = core.ServiceTypeLoadBalancer 17380 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17381 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17382 s.Spec.LoadBalancerSourceRanges = []string{" "} 17383 }, 17384 numErrs: 1, 17385 }, { 17386 name: "invalid LoadBalancer source range (hostname)", 17387 tweakSvc: func(s *core.Service) { 17388 s.Spec.Type = core.ServiceTypeLoadBalancer 17389 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17390 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17391 s.Spec.LoadBalancerSourceRanges = []string{"foo.bar"} 17392 }, 17393 numErrs: 1, 17394 }, { 17395 name: "invalid LoadBalancer source range (invalid CIDR)", 17396 tweakSvc: func(s *core.Service) { 17397 s.Spec.Type = core.ServiceTypeLoadBalancer 17398 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17399 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17400 s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.4/33"} 17401 }, 17402 numErrs: 1, 17403 }, { 17404 name: "invalid source range for non LoadBalancer type service", 17405 tweakSvc: func(s *core.Service) { 17406 s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.0/24", "5.6.0.0/16"} 17407 }, 17408 numErrs: 1, 17409 }, { 17410 name: "invalid source range annotation ignored with valid source range field", 17411 tweakSvc: func(s *core.Service) { 17412 s.Spec.Type = core.ServiceTypeLoadBalancer 17413 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17414 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17415 s.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "foo.bar" 17416 s.Spec.LoadBalancerSourceRanges = []string{"1.2.3.0/24", "5.6.0.0/16"} 17417 }, 17418 numErrs: 0, 17419 }, { 17420 name: "valid ExternalName", 17421 tweakSvc: func(s *core.Service) { 17422 s.Spec.Type = core.ServiceTypeExternalName 17423 s.Spec.ExternalName = "foo.bar.example.com" 17424 }, 17425 numErrs: 0, 17426 }, { 17427 name: "valid ExternalName (trailing dot)", 17428 tweakSvc: func(s *core.Service) { 17429 s.Spec.Type = core.ServiceTypeExternalName 17430 s.Spec.ExternalName = "foo.bar.example.com." 17431 }, 17432 numErrs: 0, 17433 }, { 17434 name: "invalid ExternalName clusterIP (valid IP)", 17435 tweakSvc: func(s *core.Service) { 17436 s.Spec.Type = core.ServiceTypeExternalName 17437 s.Spec.ClusterIP = "1.2.3.4" 17438 s.Spec.ClusterIPs = []string{"1.2.3.4"} 17439 s.Spec.ExternalName = "foo.bar.example.com" 17440 }, 17441 numErrs: 1, 17442 }, { 17443 name: "invalid ExternalName clusterIP (None)", 17444 tweakSvc: func(s *core.Service) { 17445 s.Spec.Type = core.ServiceTypeExternalName 17446 s.Spec.ClusterIP = "None" 17447 s.Spec.ClusterIPs = []string{"None"} 17448 s.Spec.ExternalName = "foo.bar.example.com" 17449 }, 17450 numErrs: 1, 17451 }, { 17452 name: "invalid ExternalName (not a DNS name)", 17453 tweakSvc: func(s *core.Service) { 17454 s.Spec.Type = core.ServiceTypeExternalName 17455 s.Spec.ExternalName = "-123" 17456 }, 17457 numErrs: 1, 17458 }, { 17459 name: "LoadBalancer type cannot have None ClusterIP", 17460 tweakSvc: func(s *core.Service) { 17461 s.Spec.ClusterIP = "None" 17462 s.Spec.ClusterIPs = []string{"None"} 17463 s.Spec.Type = core.ServiceTypeLoadBalancer 17464 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17465 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17466 }, 17467 numErrs: 1, 17468 }, { 17469 name: "invalid node port with clusterIP None", 17470 tweakSvc: func(s *core.Service) { 17471 s.Spec.Type = core.ServiceTypeNodePort 17472 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17473 s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 17474 s.Spec.ClusterIP = "None" 17475 s.Spec.ClusterIPs = []string{"None"} 17476 }, 17477 numErrs: 1, 17478 }, 17479 // ESIPP section begins. 17480 { 17481 name: "invalid externalTraffic field", 17482 tweakSvc: func(s *core.Service) { 17483 s.Spec.Type = core.ServiceTypeLoadBalancer 17484 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17485 s.Spec.ExternalTrafficPolicy = "invalid" 17486 }, 17487 numErrs: 1, 17488 }, { 17489 name: "nil internalTraffic field when feature gate is on", 17490 tweakSvc: func(s *core.Service) { 17491 s.Spec.InternalTrafficPolicy = nil 17492 }, 17493 numErrs: 1, 17494 }, { 17495 name: "internalTrafficPolicy field nil when type is ExternalName", 17496 tweakSvc: func(s *core.Service) { 17497 s.Spec.InternalTrafficPolicy = nil 17498 s.Spec.Type = core.ServiceTypeExternalName 17499 s.Spec.ExternalName = "foo.bar.com" 17500 }, 17501 numErrs: 0, 17502 }, { 17503 // Typically this should fail validation, but in v1.22 we have existing clusters 17504 // that may have allowed internalTrafficPolicy when Type=ExternalName. 17505 // This test case ensures we don't break compatibility for internalTrafficPolicy 17506 // when Type=ExternalName 17507 name: "internalTrafficPolicy field is set when type is ExternalName", 17508 tweakSvc: func(s *core.Service) { 17509 cluster := core.ServiceInternalTrafficPolicyCluster 17510 s.Spec.InternalTrafficPolicy = &cluster 17511 s.Spec.Type = core.ServiceTypeExternalName 17512 s.Spec.ExternalName = "foo.bar.com" 17513 }, 17514 numErrs: 0, 17515 }, { 17516 name: "invalid internalTraffic field", 17517 tweakSvc: func(s *core.Service) { 17518 invalid := core.ServiceInternalTrafficPolicy("invalid") 17519 s.Spec.InternalTrafficPolicy = &invalid 17520 }, 17521 numErrs: 1, 17522 }, { 17523 name: "internalTrafficPolicy field set to Cluster", 17524 tweakSvc: func(s *core.Service) { 17525 cluster := core.ServiceInternalTrafficPolicyCluster 17526 s.Spec.InternalTrafficPolicy = &cluster 17527 }, 17528 numErrs: 0, 17529 }, { 17530 name: "internalTrafficPolicy field set to Local", 17531 tweakSvc: func(s *core.Service) { 17532 local := core.ServiceInternalTrafficPolicyLocal 17533 s.Spec.InternalTrafficPolicy = &local 17534 }, 17535 numErrs: 0, 17536 }, { 17537 name: "negative healthCheckNodePort field", 17538 tweakSvc: func(s *core.Service) { 17539 s.Spec.Type = core.ServiceTypeLoadBalancer 17540 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17541 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 17542 s.Spec.HealthCheckNodePort = -1 17543 }, 17544 numErrs: 1, 17545 }, { 17546 name: "negative healthCheckNodePort field", 17547 tweakSvc: func(s *core.Service) { 17548 s.Spec.Type = core.ServiceTypeLoadBalancer 17549 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17550 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 17551 s.Spec.HealthCheckNodePort = 31100 17552 }, 17553 numErrs: 0, 17554 }, 17555 // ESIPP section ends. 17556 { 17557 name: "invalid timeoutSeconds field", 17558 tweakSvc: func(s *core.Service) { 17559 s.Spec.Type = core.ServiceTypeClusterIP 17560 s.Spec.SessionAffinity = core.ServiceAffinityClientIP 17561 s.Spec.SessionAffinityConfig = &core.SessionAffinityConfig{ 17562 ClientIP: &core.ClientIPConfig{ 17563 TimeoutSeconds: utilpointer.Int32(-1), 17564 }, 17565 } 17566 }, 17567 numErrs: 1, 17568 }, { 17569 name: "sessionAffinityConfig can't be set when session affinity is None", 17570 tweakSvc: func(s *core.Service) { 17571 s.Spec.Type = core.ServiceTypeLoadBalancer 17572 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17573 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17574 s.Spec.SessionAffinity = core.ServiceAffinityNone 17575 s.Spec.SessionAffinityConfig = &core.SessionAffinityConfig{ 17576 ClientIP: &core.ClientIPConfig{ 17577 TimeoutSeconds: utilpointer.Int32(90), 17578 }, 17579 } 17580 }, 17581 numErrs: 1, 17582 }, 17583 /* ip families validation */ 17584 { 17585 name: "invalid, service with invalid ipFamilies", 17586 tweakSvc: func(s *core.Service) { 17587 invalidServiceIPFamily := core.IPFamily("not-a-valid-ip-family") 17588 s.Spec.IPFamilies = []core.IPFamily{invalidServiceIPFamily} 17589 }, 17590 numErrs: 1, 17591 }, { 17592 name: "invalid, service with invalid ipFamilies (2nd)", 17593 tweakSvc: func(s *core.Service) { 17594 invalidServiceIPFamily := core.IPFamily("not-a-valid-ip-family") 17595 s.Spec.IPFamilyPolicy = &requireDualStack 17596 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, invalidServiceIPFamily} 17597 }, 17598 numErrs: 1, 17599 }, { 17600 name: "IPFamilyPolicy(singleStack) is set for two families", 17601 tweakSvc: func(s *core.Service) { 17602 s.Spec.IPFamilyPolicy = &singleStack 17603 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17604 }, 17605 numErrs: 0, // this validated in alloc code. 17606 }, { 17607 name: "valid, IPFamilyPolicy(preferDualStack) is set for two families (note: alloc sets families)", 17608 tweakSvc: func(s *core.Service) { 17609 s.Spec.IPFamilyPolicy = &preferDualStack 17610 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17611 }, 17612 numErrs: 0, 17613 }, 17614 17615 { 17616 name: "invalid, service with 2+ ipFamilies", 17617 tweakSvc: func(s *core.Service) { 17618 s.Spec.IPFamilyPolicy = &requireDualStack 17619 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol, core.IPv4Protocol} 17620 }, 17621 numErrs: 1, 17622 }, { 17623 name: "invalid, service with same ip families", 17624 tweakSvc: func(s *core.Service) { 17625 s.Spec.IPFamilyPolicy = &requireDualStack 17626 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv6Protocol} 17627 }, 17628 numErrs: 1, 17629 }, { 17630 name: "valid, nil service ipFamilies", 17631 tweakSvc: func(s *core.Service) { 17632 s.Spec.IPFamilies = nil 17633 }, 17634 numErrs: 0, 17635 }, { 17636 name: "valid, service with valid ipFamilies (v4)", 17637 tweakSvc: func(s *core.Service) { 17638 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 17639 }, 17640 numErrs: 0, 17641 }, { 17642 name: "valid, service with valid ipFamilies (v6)", 17643 tweakSvc: func(s *core.Service) { 17644 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17645 }, 17646 numErrs: 0, 17647 }, { 17648 name: "valid, service with valid ipFamilies(v4,v6)", 17649 tweakSvc: func(s *core.Service) { 17650 s.Spec.IPFamilyPolicy = &requireDualStack 17651 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17652 }, 17653 numErrs: 0, 17654 }, { 17655 name: "valid, service with valid ipFamilies(v6,v4)", 17656 tweakSvc: func(s *core.Service) { 17657 s.Spec.IPFamilyPolicy = &requireDualStack 17658 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 17659 }, 17660 numErrs: 0, 17661 }, { 17662 name: "valid, service preferred dual stack with single family", 17663 tweakSvc: func(s *core.Service) { 17664 s.Spec.IPFamilyPolicy = &preferDualStack 17665 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17666 }, 17667 numErrs: 0, 17668 }, 17669 /* cluster IPs. some tests are redundant */ 17670 { 17671 name: "invalid, garbage single ip", 17672 tweakSvc: func(s *core.Service) { 17673 s.Spec.ClusterIP = "garbage-ip" 17674 s.Spec.ClusterIPs = []string{"garbage-ip"} 17675 }, 17676 numErrs: 1, 17677 }, { 17678 name: "invalid, garbage ips", 17679 tweakSvc: func(s *core.Service) { 17680 s.Spec.IPFamilyPolicy = &requireDualStack 17681 s.Spec.ClusterIP = "garbage-ip" 17682 s.Spec.ClusterIPs = []string{"garbage-ip", "garbage-second-ip"} 17683 }, 17684 numErrs: 2, 17685 }, { 17686 name: "invalid, garbage first ip", 17687 tweakSvc: func(s *core.Service) { 17688 s.Spec.IPFamilyPolicy = &requireDualStack 17689 s.Spec.ClusterIP = "garbage-ip" 17690 s.Spec.ClusterIPs = []string{"garbage-ip", "2001::1"} 17691 }, 17692 numErrs: 1, 17693 }, { 17694 name: "invalid, garbage second ip", 17695 tweakSvc: func(s *core.Service) { 17696 s.Spec.IPFamilyPolicy = &requireDualStack 17697 s.Spec.ClusterIP = "2001::1" 17698 s.Spec.ClusterIPs = []string{"2001::1", "garbage-ip"} 17699 }, 17700 numErrs: 1, 17701 }, { 17702 name: "invalid, NONE + IP", 17703 tweakSvc: func(s *core.Service) { 17704 s.Spec.IPFamilyPolicy = &requireDualStack 17705 s.Spec.ClusterIP = "None" 17706 s.Spec.ClusterIPs = []string{"None", "2001::1"} 17707 }, 17708 numErrs: 1, 17709 }, { 17710 name: "invalid, IP + NONE", 17711 tweakSvc: func(s *core.Service) { 17712 s.Spec.IPFamilyPolicy = &requireDualStack 17713 s.Spec.ClusterIP = "2001::1" 17714 s.Spec.ClusterIPs = []string{"2001::1", "None"} 17715 }, 17716 numErrs: 1, 17717 }, { 17718 name: "invalid, EMPTY STRING + IP", 17719 tweakSvc: func(s *core.Service) { 17720 s.Spec.IPFamilyPolicy = &requireDualStack 17721 s.Spec.ClusterIP = "" 17722 s.Spec.ClusterIPs = []string{"", "2001::1"} 17723 }, 17724 numErrs: 2, 17725 }, { 17726 name: "invalid, IP + EMPTY STRING", 17727 tweakSvc: func(s *core.Service) { 17728 s.Spec.IPFamilyPolicy = &requireDualStack 17729 s.Spec.ClusterIP = "2001::1" 17730 s.Spec.ClusterIPs = []string{"2001::1", ""} 17731 }, 17732 numErrs: 1, 17733 }, { 17734 name: "invalid, same ip family (v6)", 17735 tweakSvc: func(s *core.Service) { 17736 s.Spec.IPFamilyPolicy = &requireDualStack 17737 s.Spec.ClusterIP = "2001::1" 17738 s.Spec.ClusterIPs = []string{"2001::1", "2001::4"} 17739 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17740 }, 17741 numErrs: 2, 17742 }, { 17743 name: "invalid, same ip family (v4)", 17744 tweakSvc: func(s *core.Service) { 17745 s.Spec.IPFamilyPolicy = &requireDualStack 17746 s.Spec.ClusterIP = "10.0.0.1" 17747 s.Spec.ClusterIPs = []string{"10.0.0.1", "10.0.0.10"} 17748 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17749 17750 }, 17751 numErrs: 2, 17752 }, { 17753 name: "invalid, more than two ips", 17754 tweakSvc: func(s *core.Service) { 17755 s.Spec.IPFamilyPolicy = &requireDualStack 17756 s.Spec.ClusterIP = "10.0.0.1" 17757 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1", "10.0.0.10"} 17758 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17759 }, 17760 numErrs: 1, 17761 }, { 17762 name: " multi ip, dualstack not set (request for downgrade)", 17763 tweakSvc: func(s *core.Service) { 17764 s.Spec.IPFamilyPolicy = &singleStack 17765 s.Spec.ClusterIP = "10.0.0.1" 17766 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17767 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17768 }, 17769 numErrs: 0, 17770 }, { 17771 name: "valid, headless-no-selector + multi family + gate off", 17772 tweakSvc: func(s *core.Service) { 17773 s.Spec.IPFamilyPolicy = &requireDualStack 17774 s.Spec.ClusterIP = "None" 17775 s.Spec.ClusterIPs = []string{"None"} 17776 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17777 s.Spec.Selector = nil 17778 }, 17779 numErrs: 0, 17780 }, { 17781 name: "valid, multi ip, single ipfamilies preferDualStack", 17782 tweakSvc: func(s *core.Service) { 17783 s.Spec.IPFamilyPolicy = &preferDualStack 17784 s.Spec.ClusterIP = "10.0.0.1" 17785 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17786 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 17787 }, 17788 numErrs: 0, 17789 }, 17790 17791 { 17792 name: "valid, multi ip, single ipfamilies (must match when provided) + requireDualStack", 17793 tweakSvc: func(s *core.Service) { 17794 s.Spec.IPFamilyPolicy = &requireDualStack 17795 s.Spec.ClusterIP = "10.0.0.1" 17796 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17797 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 17798 }, 17799 numErrs: 0, 17800 }, { 17801 name: "invalid, families don't match (v4=>v6)", 17802 tweakSvc: func(s *core.Service) { 17803 s.Spec.ClusterIP = "10.0.0.1" 17804 s.Spec.ClusterIPs = []string{"10.0.0.1"} 17805 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17806 }, 17807 numErrs: 1, 17808 }, { 17809 name: "invalid, families don't match (v6=>v4)", 17810 tweakSvc: func(s *core.Service) { 17811 s.Spec.ClusterIP = "2001::1" 17812 s.Spec.ClusterIPs = []string{"2001::1"} 17813 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 17814 }, 17815 numErrs: 1, 17816 }, { 17817 name: "valid. no field set", 17818 tweakSvc: func(s *core.Service) { 17819 }, 17820 numErrs: 0, 17821 }, 17822 17823 { 17824 name: "valid, single ip", 17825 tweakSvc: func(s *core.Service) { 17826 s.Spec.IPFamilyPolicy = &singleStack 17827 s.Spec.ClusterIP = "10.0.0.1" 17828 s.Spec.ClusterIPs = []string{"10.0.0.1"} 17829 }, 17830 numErrs: 0, 17831 }, { 17832 name: "valid, single family", 17833 tweakSvc: func(s *core.Service) { 17834 s.Spec.IPFamilyPolicy = &singleStack 17835 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17836 17837 }, 17838 numErrs: 0, 17839 }, { 17840 name: "valid, single ip + single family", 17841 tweakSvc: func(s *core.Service) { 17842 s.Spec.IPFamilyPolicy = &singleStack 17843 s.Spec.ClusterIP = "2001::1" 17844 s.Spec.ClusterIPs = []string{"2001::1"} 17845 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17846 17847 }, 17848 numErrs: 0, 17849 }, { 17850 name: "valid, single ip + single family (dual stack requested)", 17851 tweakSvc: func(s *core.Service) { 17852 s.Spec.IPFamilyPolicy = &preferDualStack 17853 s.Spec.ClusterIP = "2001::1" 17854 s.Spec.ClusterIPs = []string{"2001::1"} 17855 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 17856 17857 }, 17858 numErrs: 0, 17859 }, { 17860 name: "valid, single ip, multi ipfamilies", 17861 tweakSvc: func(s *core.Service) { 17862 s.Spec.IPFamilyPolicy = &requireDualStack 17863 s.Spec.ClusterIP = "10.0.0.1" 17864 s.Spec.ClusterIPs = []string{"10.0.0.1"} 17865 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17866 }, 17867 numErrs: 0, 17868 }, { 17869 name: "valid, multi ips, multi ipfamilies (4,6)", 17870 tweakSvc: func(s *core.Service) { 17871 s.Spec.IPFamilyPolicy = &requireDualStack 17872 s.Spec.ClusterIP = "10.0.0.1" 17873 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17874 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17875 }, 17876 numErrs: 0, 17877 }, { 17878 name: "valid, ips, multi ipfamilies (6,4)", 17879 tweakSvc: func(s *core.Service) { 17880 s.Spec.IPFamilyPolicy = &requireDualStack 17881 s.Spec.ClusterIP = "2001::1" 17882 s.Spec.ClusterIPs = []string{"2001::1", "10.0.0.1"} 17883 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 17884 }, 17885 numErrs: 0, 17886 }, { 17887 name: "valid, multi ips (6,4)", 17888 tweakSvc: func(s *core.Service) { 17889 s.Spec.IPFamilyPolicy = &requireDualStack 17890 s.Spec.ClusterIP = "2001::1" 17891 s.Spec.ClusterIPs = []string{"2001::1", "10.0.0.1"} 17892 }, 17893 numErrs: 0, 17894 }, { 17895 name: "valid, multi ipfamilies (6,4)", 17896 tweakSvc: func(s *core.Service) { 17897 s.Spec.IPFamilyPolicy = &requireDualStack 17898 s.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 17899 }, 17900 numErrs: 0, 17901 }, { 17902 name: "valid, multi ips (4,6)", 17903 tweakSvc: func(s *core.Service) { 17904 s.Spec.IPFamilyPolicy = &requireDualStack 17905 s.Spec.ClusterIP = "10.0.0.1" 17906 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17907 }, 17908 numErrs: 0, 17909 }, { 17910 name: "valid, multi ipfamilies (4,6)", 17911 tweakSvc: func(s *core.Service) { 17912 s.Spec.IPFamilyPolicy = &requireDualStack 17913 s.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 17914 }, 17915 numErrs: 0, 17916 }, { 17917 name: "valid, dual stack", 17918 tweakSvc: func(s *core.Service) { 17919 s.Spec.IPFamilyPolicy = &requireDualStack 17920 }, 17921 numErrs: 0, 17922 }, 17923 17924 { 17925 name: `valid appProtocol`, 17926 tweakSvc: func(s *core.Service) { 17927 s.Spec.Ports = []core.ServicePort{{ 17928 Port: 12345, 17929 TargetPort: intstr.FromInt32(12345), 17930 Protocol: "TCP", 17931 AppProtocol: utilpointer.String("HTTP"), 17932 }} 17933 }, 17934 numErrs: 0, 17935 }, { 17936 name: `valid custom appProtocol`, 17937 tweakSvc: func(s *core.Service) { 17938 s.Spec.Ports = []core.ServicePort{{ 17939 Port: 12345, 17940 TargetPort: intstr.FromInt32(12345), 17941 Protocol: "TCP", 17942 AppProtocol: utilpointer.String("example.com/protocol"), 17943 }} 17944 }, 17945 numErrs: 0, 17946 }, { 17947 name: `invalid appProtocol`, 17948 tweakSvc: func(s *core.Service) { 17949 s.Spec.Ports = []core.ServicePort{{ 17950 Port: 12345, 17951 TargetPort: intstr.FromInt32(12345), 17952 Protocol: "TCP", 17953 AppProtocol: utilpointer.String("example.com/protocol_with{invalid}[characters]"), 17954 }} 17955 }, 17956 numErrs: 1, 17957 }, 17958 17959 { 17960 name: "invalid cluster ip != clusterIP in multi ip service", 17961 tweakSvc: func(s *core.Service) { 17962 s.Spec.IPFamilyPolicy = &requireDualStack 17963 s.Spec.ClusterIP = "10.0.0.10" 17964 s.Spec.ClusterIPs = []string{"10.0.0.1", "2001::1"} 17965 }, 17966 numErrs: 1, 17967 }, { 17968 name: "invalid cluster ip != clusterIP in single ip service", 17969 tweakSvc: func(s *core.Service) { 17970 s.Spec.ClusterIP = "10.0.0.10" 17971 s.Spec.ClusterIPs = []string{"10.0.0.1"} 17972 }, 17973 numErrs: 1, 17974 }, { 17975 name: "Use AllocateLoadBalancerNodePorts when type is not LoadBalancer", 17976 tweakSvc: func(s *core.Service) { 17977 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17978 }, 17979 numErrs: 1, 17980 }, { 17981 name: "valid LoadBalancerClass when type is LoadBalancer", 17982 tweakSvc: func(s *core.Service) { 17983 s.Spec.Type = core.ServiceTypeLoadBalancer 17984 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17985 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17986 s.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 17987 }, 17988 numErrs: 0, 17989 }, { 17990 name: "invalid LoadBalancerClass", 17991 tweakSvc: func(s *core.Service) { 17992 s.Spec.Type = core.ServiceTypeLoadBalancer 17993 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 17994 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 17995 s.Spec.LoadBalancerClass = utilpointer.String("Bad/LoadBalancerClass") 17996 }, 17997 numErrs: 1, 17998 }, { 17999 name: "invalid: set LoadBalancerClass when type is not LoadBalancer", 18000 tweakSvc: func(s *core.Service) { 18001 s.Spec.Type = core.ServiceTypeClusterIP 18002 s.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 18003 }, 18004 numErrs: 1, 18005 }, { 18006 name: "topology annotations are mismatched", 18007 tweakSvc: func(s *core.Service) { 18008 s.Annotations[core.DeprecatedAnnotationTopologyAwareHints] = "original" 18009 s.Annotations[core.AnnotationTopologyMode] = "different" 18010 }, 18011 numErrs: 1, 18012 }, { 18013 name: "valid: trafficDistribution field set to PreferClose", 18014 tweakSvc: func(s *core.Service) { 18015 s.Spec.TrafficDistribution = utilpointer.String("PreferClose") 18016 }, 18017 numErrs: 0, 18018 }, { 18019 name: "invalid: trafficDistribution field set to Random", 18020 tweakSvc: func(s *core.Service) { 18021 s.Spec.TrafficDistribution = utilpointer.String("Random") 18022 }, 18023 numErrs: 1, 18024 }, 18025 } 18026 18027 for _, tc := range testCases { 18028 t.Run(tc.name, func(t *testing.T) { 18029 for i := range tc.featureGates { 18030 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, tc.featureGates[i], true) 18031 } 18032 svc := makeValidService() 18033 tc.tweakSvc(&svc) 18034 errs := ValidateServiceCreate(&svc) 18035 if len(errs) != tc.numErrs { 18036 t.Errorf("Unexpected error list for case %q(expected:%v got %v) - Errors:\n %v", tc.name, tc.numErrs, len(errs), errs.ToAggregate()) 18037 } 18038 }) 18039 } 18040 } 18041 18042 func TestValidateServiceExternalTrafficPolicy(t *testing.T) { 18043 testCases := []struct { 18044 name string 18045 tweakSvc func(svc *core.Service) // Given a basic valid service, each test case can customize it. 18046 numErrs int 18047 }{{ 18048 name: "valid loadBalancer service with externalTrafficPolicy and healthCheckNodePort set", 18049 tweakSvc: func(s *core.Service) { 18050 s.Spec.Type = core.ServiceTypeLoadBalancer 18051 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 18052 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 18053 s.Spec.HealthCheckNodePort = 34567 18054 }, 18055 numErrs: 0, 18056 }, { 18057 name: "valid nodePort service with externalTrafficPolicy set", 18058 tweakSvc: func(s *core.Service) { 18059 s.Spec.Type = core.ServiceTypeNodePort 18060 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 18061 }, 18062 numErrs: 0, 18063 }, { 18064 name: "valid clusterIP service with none of externalTrafficPolicy and healthCheckNodePort set", 18065 tweakSvc: func(s *core.Service) { 18066 s.Spec.Type = core.ServiceTypeClusterIP 18067 }, 18068 numErrs: 0, 18069 }, { 18070 name: "cannot set healthCheckNodePort field on loadBalancer service with externalTrafficPolicy!=Local", 18071 tweakSvc: func(s *core.Service) { 18072 s.Spec.Type = core.ServiceTypeLoadBalancer 18073 s.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 18074 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 18075 s.Spec.HealthCheckNodePort = 34567 18076 }, 18077 numErrs: 1, 18078 }, { 18079 name: "cannot set healthCheckNodePort field on nodePort service", 18080 tweakSvc: func(s *core.Service) { 18081 s.Spec.Type = core.ServiceTypeNodePort 18082 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 18083 s.Spec.HealthCheckNodePort = 34567 18084 }, 18085 numErrs: 1, 18086 }, { 18087 name: "cannot set externalTrafficPolicy or healthCheckNodePort fields on clusterIP service", 18088 tweakSvc: func(s *core.Service) { 18089 s.Spec.Type = core.ServiceTypeClusterIP 18090 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 18091 s.Spec.HealthCheckNodePort = 34567 18092 }, 18093 numErrs: 2, 18094 }, { 18095 name: "cannot set externalTrafficPolicy field on ExternalName service", 18096 tweakSvc: func(s *core.Service) { 18097 s.Spec.Type = core.ServiceTypeExternalName 18098 s.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyLocal 18099 }, 18100 numErrs: 1, 18101 }, { 18102 name: "externalTrafficPolicy is required on NodePort service", 18103 tweakSvc: func(s *core.Service) { 18104 s.Spec.Type = core.ServiceTypeNodePort 18105 }, 18106 numErrs: 1, 18107 }, { 18108 name: "externalTrafficPolicy is required on LoadBalancer service", 18109 tweakSvc: func(s *core.Service) { 18110 s.Spec.Type = core.ServiceTypeLoadBalancer 18111 }, 18112 numErrs: 1, 18113 }, { 18114 name: "externalTrafficPolicy is required on ClusterIP service with externalIPs", 18115 tweakSvc: func(s *core.Service) { 18116 s.Spec.Type = core.ServiceTypeClusterIP 18117 s.Spec.ExternalIPs = []string{"1.2.3,4"} 18118 }, 18119 numErrs: 1, 18120 }, 18121 } 18122 18123 for _, tc := range testCases { 18124 svc := makeValidService() 18125 tc.tweakSvc(&svc) 18126 errs := validateServiceExternalTrafficPolicy(&svc) 18127 if len(errs) != tc.numErrs { 18128 t.Errorf("Unexpected error list for case %q: %v", tc.name, errs.ToAggregate()) 18129 } 18130 } 18131 } 18132 18133 func TestValidateReplicationControllerStatus(t *testing.T) { 18134 tests := []struct { 18135 name string 18136 18137 replicas int32 18138 fullyLabeledReplicas int32 18139 readyReplicas int32 18140 availableReplicas int32 18141 observedGeneration int64 18142 18143 expectedErr bool 18144 }{{ 18145 name: "valid status", 18146 replicas: 3, 18147 fullyLabeledReplicas: 3, 18148 readyReplicas: 2, 18149 availableReplicas: 1, 18150 observedGeneration: 2, 18151 expectedErr: false, 18152 }, { 18153 name: "invalid replicas", 18154 replicas: -1, 18155 fullyLabeledReplicas: 3, 18156 readyReplicas: 2, 18157 availableReplicas: 1, 18158 observedGeneration: 2, 18159 expectedErr: true, 18160 }, { 18161 name: "invalid fullyLabeledReplicas", 18162 replicas: 3, 18163 fullyLabeledReplicas: -1, 18164 readyReplicas: 2, 18165 availableReplicas: 1, 18166 observedGeneration: 2, 18167 expectedErr: true, 18168 }, { 18169 name: "invalid readyReplicas", 18170 replicas: 3, 18171 fullyLabeledReplicas: 3, 18172 readyReplicas: -1, 18173 availableReplicas: 1, 18174 observedGeneration: 2, 18175 expectedErr: true, 18176 }, { 18177 name: "invalid availableReplicas", 18178 replicas: 3, 18179 fullyLabeledReplicas: 3, 18180 readyReplicas: 3, 18181 availableReplicas: -1, 18182 observedGeneration: 2, 18183 expectedErr: true, 18184 }, { 18185 name: "invalid observedGeneration", 18186 replicas: 3, 18187 fullyLabeledReplicas: 3, 18188 readyReplicas: 3, 18189 availableReplicas: 3, 18190 observedGeneration: -1, 18191 expectedErr: true, 18192 }, { 18193 name: "fullyLabeledReplicas greater than replicas", 18194 replicas: 3, 18195 fullyLabeledReplicas: 4, 18196 readyReplicas: 3, 18197 availableReplicas: 3, 18198 observedGeneration: 1, 18199 expectedErr: true, 18200 }, { 18201 name: "readyReplicas greater than replicas", 18202 replicas: 3, 18203 fullyLabeledReplicas: 3, 18204 readyReplicas: 4, 18205 availableReplicas: 3, 18206 observedGeneration: 1, 18207 expectedErr: true, 18208 }, { 18209 name: "availableReplicas greater than replicas", 18210 replicas: 3, 18211 fullyLabeledReplicas: 3, 18212 readyReplicas: 3, 18213 availableReplicas: 4, 18214 observedGeneration: 1, 18215 expectedErr: true, 18216 }, { 18217 name: "availableReplicas greater than readyReplicas", 18218 replicas: 3, 18219 fullyLabeledReplicas: 3, 18220 readyReplicas: 2, 18221 availableReplicas: 3, 18222 observedGeneration: 1, 18223 expectedErr: true, 18224 }, 18225 } 18226 18227 for _, test := range tests { 18228 status := core.ReplicationControllerStatus{ 18229 Replicas: test.replicas, 18230 FullyLabeledReplicas: test.fullyLabeledReplicas, 18231 ReadyReplicas: test.readyReplicas, 18232 AvailableReplicas: test.availableReplicas, 18233 ObservedGeneration: test.observedGeneration, 18234 } 18235 18236 if hasErr := len(ValidateReplicationControllerStatus(status, field.NewPath("status"))) > 0; hasErr != test.expectedErr { 18237 t.Errorf("%s: expected error: %t, got error: %t", test.name, test.expectedErr, hasErr) 18238 } 18239 } 18240 } 18241 18242 func TestValidateReplicationControllerStatusUpdate(t *testing.T) { 18243 validSelector := map[string]string{"a": "b"} 18244 validPodTemplate := core.PodTemplate{ 18245 Template: core.PodTemplateSpec{ 18246 ObjectMeta: metav1.ObjectMeta{ 18247 Labels: validSelector, 18248 }, 18249 Spec: core.PodSpec{ 18250 RestartPolicy: core.RestartPolicyAlways, 18251 DNSPolicy: core.DNSClusterFirst, 18252 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18253 }, 18254 }, 18255 } 18256 type rcUpdateTest struct { 18257 old core.ReplicationController 18258 update core.ReplicationController 18259 } 18260 successCases := []rcUpdateTest{{ 18261 old: core.ReplicationController{ 18262 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18263 Spec: core.ReplicationControllerSpec{ 18264 Selector: validSelector, 18265 Template: &validPodTemplate.Template, 18266 }, 18267 Status: core.ReplicationControllerStatus{ 18268 Replicas: 2, 18269 }, 18270 }, 18271 update: core.ReplicationController{ 18272 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18273 Spec: core.ReplicationControllerSpec{ 18274 Replicas: 3, 18275 Selector: validSelector, 18276 Template: &validPodTemplate.Template, 18277 }, 18278 Status: core.ReplicationControllerStatus{ 18279 Replicas: 4, 18280 }, 18281 }, 18282 }, 18283 } 18284 for _, successCase := range successCases { 18285 successCase.old.ObjectMeta.ResourceVersion = "1" 18286 successCase.update.ObjectMeta.ResourceVersion = "1" 18287 if errs := ValidateReplicationControllerStatusUpdate(&successCase.update, &successCase.old); len(errs) != 0 { 18288 t.Errorf("expected success: %v", errs) 18289 } 18290 } 18291 errorCases := map[string]rcUpdateTest{ 18292 "negative replicas": { 18293 old: core.ReplicationController{ 18294 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, 18295 Spec: core.ReplicationControllerSpec{ 18296 Selector: validSelector, 18297 Template: &validPodTemplate.Template, 18298 }, 18299 Status: core.ReplicationControllerStatus{ 18300 Replicas: 3, 18301 }, 18302 }, 18303 update: core.ReplicationController{ 18304 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18305 Spec: core.ReplicationControllerSpec{ 18306 Replicas: 2, 18307 Selector: validSelector, 18308 Template: &validPodTemplate.Template, 18309 }, 18310 Status: core.ReplicationControllerStatus{ 18311 Replicas: -3, 18312 }, 18313 }, 18314 }, 18315 } 18316 for testName, errorCase := range errorCases { 18317 if errs := ValidateReplicationControllerStatusUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { 18318 t.Errorf("expected failure: %s", testName) 18319 } 18320 } 18321 18322 } 18323 18324 func TestValidateReplicationControllerUpdate(t *testing.T) { 18325 validSelector := map[string]string{"a": "b"} 18326 validPodTemplate := core.PodTemplate{ 18327 Template: core.PodTemplateSpec{ 18328 ObjectMeta: metav1.ObjectMeta{ 18329 Labels: validSelector, 18330 }, 18331 Spec: core.PodSpec{ 18332 RestartPolicy: core.RestartPolicyAlways, 18333 DNSPolicy: core.DNSClusterFirst, 18334 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18335 }, 18336 }, 18337 } 18338 readWriteVolumePodTemplate := core.PodTemplate{ 18339 Template: core.PodTemplateSpec{ 18340 ObjectMeta: metav1.ObjectMeta{ 18341 Labels: validSelector, 18342 }, 18343 Spec: core.PodSpec{ 18344 RestartPolicy: core.RestartPolicyAlways, 18345 DNSPolicy: core.DNSClusterFirst, 18346 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18347 Volumes: []core.Volume{{Name: "gcepd", VolumeSource: core.VolumeSource{GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, 18348 }, 18349 }, 18350 } 18351 invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} 18352 invalidPodTemplate := core.PodTemplate{ 18353 Template: core.PodTemplateSpec{ 18354 Spec: core.PodSpec{ 18355 RestartPolicy: core.RestartPolicyAlways, 18356 DNSPolicy: core.DNSClusterFirst, 18357 }, 18358 ObjectMeta: metav1.ObjectMeta{ 18359 Labels: invalidSelector, 18360 }, 18361 }, 18362 } 18363 type rcUpdateTest struct { 18364 old core.ReplicationController 18365 update core.ReplicationController 18366 } 18367 successCases := []rcUpdateTest{{ 18368 old: core.ReplicationController{ 18369 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18370 Spec: core.ReplicationControllerSpec{ 18371 Selector: validSelector, 18372 Template: &validPodTemplate.Template, 18373 }, 18374 }, 18375 update: core.ReplicationController{ 18376 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18377 Spec: core.ReplicationControllerSpec{ 18378 Replicas: 3, 18379 Selector: validSelector, 18380 Template: &validPodTemplate.Template, 18381 }, 18382 }, 18383 }, { 18384 old: core.ReplicationController{ 18385 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18386 Spec: core.ReplicationControllerSpec{ 18387 Selector: validSelector, 18388 Template: &validPodTemplate.Template, 18389 }, 18390 }, 18391 update: core.ReplicationController{ 18392 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18393 Spec: core.ReplicationControllerSpec{ 18394 Replicas: 1, 18395 Selector: validSelector, 18396 Template: &readWriteVolumePodTemplate.Template, 18397 }, 18398 }, 18399 }, 18400 } 18401 for _, successCase := range successCases { 18402 successCase.old.ObjectMeta.ResourceVersion = "1" 18403 successCase.update.ObjectMeta.ResourceVersion = "1" 18404 if errs := ValidateReplicationControllerUpdate(&successCase.update, &successCase.old, PodValidationOptions{}); len(errs) != 0 { 18405 t.Errorf("expected success: %v", errs) 18406 } 18407 } 18408 errorCases := map[string]rcUpdateTest{ 18409 "more than one read/write": { 18410 old: core.ReplicationController{ 18411 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, 18412 Spec: core.ReplicationControllerSpec{ 18413 Selector: validSelector, 18414 Template: &validPodTemplate.Template, 18415 }, 18416 }, 18417 update: core.ReplicationController{ 18418 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18419 Spec: core.ReplicationControllerSpec{ 18420 Replicas: 2, 18421 Selector: validSelector, 18422 Template: &readWriteVolumePodTemplate.Template, 18423 }, 18424 }, 18425 }, 18426 "invalid selector": { 18427 old: core.ReplicationController{ 18428 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, 18429 Spec: core.ReplicationControllerSpec{ 18430 Selector: validSelector, 18431 Template: &validPodTemplate.Template, 18432 }, 18433 }, 18434 update: core.ReplicationController{ 18435 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18436 Spec: core.ReplicationControllerSpec{ 18437 Replicas: 2, 18438 Selector: invalidSelector, 18439 Template: &validPodTemplate.Template, 18440 }, 18441 }, 18442 }, 18443 "invalid pod": { 18444 old: core.ReplicationController{ 18445 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, 18446 Spec: core.ReplicationControllerSpec{ 18447 Selector: validSelector, 18448 Template: &validPodTemplate.Template, 18449 }, 18450 }, 18451 update: core.ReplicationController{ 18452 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18453 Spec: core.ReplicationControllerSpec{ 18454 Replicas: 2, 18455 Selector: validSelector, 18456 Template: &invalidPodTemplate.Template, 18457 }, 18458 }, 18459 }, 18460 "negative replicas": { 18461 old: core.ReplicationController{ 18462 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18463 Spec: core.ReplicationControllerSpec{ 18464 Selector: validSelector, 18465 Template: &validPodTemplate.Template, 18466 }, 18467 }, 18468 update: core.ReplicationController{ 18469 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18470 Spec: core.ReplicationControllerSpec{ 18471 Replicas: -1, 18472 Selector: validSelector, 18473 Template: &validPodTemplate.Template, 18474 }, 18475 }, 18476 }, 18477 } 18478 for testName, errorCase := range errorCases { 18479 if errs := ValidateReplicationControllerUpdate(&errorCase.update, &errorCase.old, PodValidationOptions{}); len(errs) == 0 { 18480 t.Errorf("expected failure: %s", testName) 18481 } 18482 } 18483 } 18484 18485 func TestValidateReplicationController(t *testing.T) { 18486 validSelector := map[string]string{"a": "b"} 18487 validPodTemplate := core.PodTemplate{ 18488 Template: core.PodTemplateSpec{ 18489 ObjectMeta: metav1.ObjectMeta{ 18490 Labels: validSelector, 18491 }, 18492 Spec: core.PodSpec{ 18493 RestartPolicy: core.RestartPolicyAlways, 18494 DNSPolicy: core.DNSClusterFirst, 18495 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18496 }, 18497 }, 18498 } 18499 readWriteVolumePodTemplate := core.PodTemplate{ 18500 Template: core.PodTemplateSpec{ 18501 ObjectMeta: metav1.ObjectMeta{ 18502 Labels: validSelector, 18503 }, 18504 Spec: core.PodSpec{ 18505 Volumes: []core.Volume{{Name: "gcepd", VolumeSource: core.VolumeSource{GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, 18506 RestartPolicy: core.RestartPolicyAlways, 18507 DNSPolicy: core.DNSClusterFirst, 18508 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18509 }, 18510 }, 18511 } 18512 hostnetPodTemplate := core.PodTemplate{ 18513 Template: core.PodTemplateSpec{ 18514 ObjectMeta: metav1.ObjectMeta{ 18515 Labels: validSelector, 18516 }, 18517 Spec: core.PodSpec{ 18518 SecurityContext: &core.PodSecurityContext{ 18519 HostNetwork: true, 18520 }, 18521 RestartPolicy: core.RestartPolicyAlways, 18522 DNSPolicy: core.DNSClusterFirst, 18523 Containers: []core.Container{{ 18524 Name: "abc", 18525 Image: "image", 18526 ImagePullPolicy: "IfNotPresent", 18527 TerminationMessagePolicy: "File", 18528 Ports: []core.ContainerPort{{ 18529 ContainerPort: 12345, 18530 Protocol: core.ProtocolTCP, 18531 }}, 18532 }}, 18533 }, 18534 }, 18535 } 18536 invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} 18537 invalidPodTemplate := core.PodTemplate{ 18538 Template: core.PodTemplateSpec{ 18539 Spec: core.PodSpec{ 18540 RestartPolicy: core.RestartPolicyAlways, 18541 DNSPolicy: core.DNSClusterFirst, 18542 }, 18543 ObjectMeta: metav1.ObjectMeta{ 18544 Labels: invalidSelector, 18545 }, 18546 }, 18547 } 18548 successCases := []core.ReplicationController{{ 18549 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18550 Spec: core.ReplicationControllerSpec{ 18551 Selector: validSelector, 18552 Template: &validPodTemplate.Template, 18553 }, 18554 }, { 18555 ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, 18556 Spec: core.ReplicationControllerSpec{ 18557 Selector: validSelector, 18558 Template: &validPodTemplate.Template, 18559 }, 18560 }, { 18561 ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, 18562 Spec: core.ReplicationControllerSpec{ 18563 Replicas: 1, 18564 Selector: validSelector, 18565 Template: &readWriteVolumePodTemplate.Template, 18566 }, 18567 }, { 18568 ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, 18569 Spec: core.ReplicationControllerSpec{ 18570 Replicas: 1, 18571 Selector: validSelector, 18572 Template: &hostnetPodTemplate.Template, 18573 }, 18574 }} 18575 for _, successCase := range successCases { 18576 if errs := ValidateReplicationController(&successCase, PodValidationOptions{}); len(errs) != 0 { 18577 t.Errorf("expected success: %v", errs) 18578 } 18579 } 18580 18581 errorCases := map[string]core.ReplicationController{ 18582 "zero-length ID": { 18583 ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, 18584 Spec: core.ReplicationControllerSpec{ 18585 Selector: validSelector, 18586 Template: &validPodTemplate.Template, 18587 }, 18588 }, 18589 "missing-namespace": { 18590 ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, 18591 Spec: core.ReplicationControllerSpec{ 18592 Selector: validSelector, 18593 Template: &validPodTemplate.Template, 18594 }, 18595 }, 18596 "empty selector": { 18597 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18598 Spec: core.ReplicationControllerSpec{ 18599 Template: &validPodTemplate.Template, 18600 }, 18601 }, 18602 "selector_doesnt_match": { 18603 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18604 Spec: core.ReplicationControllerSpec{ 18605 Selector: map[string]string{"foo": "bar"}, 18606 Template: &validPodTemplate.Template, 18607 }, 18608 }, 18609 "invalid manifest": { 18610 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18611 Spec: core.ReplicationControllerSpec{ 18612 Selector: validSelector, 18613 }, 18614 }, 18615 "read-write persistent disk with > 1 pod": { 18616 ObjectMeta: metav1.ObjectMeta{Name: "abc"}, 18617 Spec: core.ReplicationControllerSpec{ 18618 Replicas: 2, 18619 Selector: validSelector, 18620 Template: &readWriteVolumePodTemplate.Template, 18621 }, 18622 }, 18623 "negative_replicas": { 18624 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, 18625 Spec: core.ReplicationControllerSpec{ 18626 Replicas: -1, 18627 Selector: validSelector, 18628 }, 18629 }, 18630 "invalid_label": { 18631 ObjectMeta: metav1.ObjectMeta{ 18632 Name: "abc-123", 18633 Namespace: metav1.NamespaceDefault, 18634 Labels: map[string]string{ 18635 "NoUppercaseOrSpecialCharsLike=Equals": "bar", 18636 }, 18637 }, 18638 Spec: core.ReplicationControllerSpec{ 18639 Selector: validSelector, 18640 Template: &validPodTemplate.Template, 18641 }, 18642 }, 18643 "invalid_label 2": { 18644 ObjectMeta: metav1.ObjectMeta{ 18645 Name: "abc-123", 18646 Namespace: metav1.NamespaceDefault, 18647 Labels: map[string]string{ 18648 "NoUppercaseOrSpecialCharsLike=Equals": "bar", 18649 }, 18650 }, 18651 Spec: core.ReplicationControllerSpec{ 18652 Template: &invalidPodTemplate.Template, 18653 }, 18654 }, 18655 "invalid_annotation": { 18656 ObjectMeta: metav1.ObjectMeta{ 18657 Name: "abc-123", 18658 Namespace: metav1.NamespaceDefault, 18659 Annotations: map[string]string{ 18660 "NoUppercaseOrSpecialCharsLike=Equals": "bar", 18661 }, 18662 }, 18663 Spec: core.ReplicationControllerSpec{ 18664 Selector: validSelector, 18665 Template: &validPodTemplate.Template, 18666 }, 18667 }, 18668 "invalid restart policy 1": { 18669 ObjectMeta: metav1.ObjectMeta{ 18670 Name: "abc-123", 18671 Namespace: metav1.NamespaceDefault, 18672 }, 18673 Spec: core.ReplicationControllerSpec{ 18674 Selector: validSelector, 18675 Template: &core.PodTemplateSpec{ 18676 Spec: core.PodSpec{ 18677 RestartPolicy: core.RestartPolicyOnFailure, 18678 DNSPolicy: core.DNSClusterFirst, 18679 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18680 }, 18681 ObjectMeta: metav1.ObjectMeta{ 18682 Labels: validSelector, 18683 }, 18684 }, 18685 }, 18686 }, 18687 "invalid restart policy 2": { 18688 ObjectMeta: metav1.ObjectMeta{ 18689 Name: "abc-123", 18690 Namespace: metav1.NamespaceDefault, 18691 }, 18692 Spec: core.ReplicationControllerSpec{ 18693 Selector: validSelector, 18694 Template: &core.PodTemplateSpec{ 18695 Spec: core.PodSpec{ 18696 RestartPolicy: core.RestartPolicyNever, 18697 DNSPolicy: core.DNSClusterFirst, 18698 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18699 }, 18700 ObjectMeta: metav1.ObjectMeta{ 18701 Labels: validSelector, 18702 }, 18703 }, 18704 }, 18705 }, 18706 "template may not contain ephemeral containers": { 18707 ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, 18708 Spec: core.ReplicationControllerSpec{ 18709 Replicas: 1, 18710 Selector: validSelector, 18711 Template: &core.PodTemplateSpec{ 18712 ObjectMeta: metav1.ObjectMeta{ 18713 Labels: validSelector, 18714 }, 18715 Spec: core.PodSpec{ 18716 RestartPolicy: core.RestartPolicyAlways, 18717 DNSPolicy: core.DNSClusterFirst, 18718 Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 18719 EphemeralContainers: []core.EphemeralContainer{{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}}, 18720 }, 18721 }, 18722 }, 18723 }, 18724 } 18725 for k, v := range errorCases { 18726 errs := ValidateReplicationController(&v, PodValidationOptions{}) 18727 if len(errs) == 0 { 18728 t.Errorf("expected failure for %s", k) 18729 } 18730 for i := range errs { 18731 field := errs[i].Field 18732 if !strings.HasPrefix(field, "spec.template.") && 18733 field != "metadata.name" && 18734 field != "metadata.namespace" && 18735 field != "spec.selector" && 18736 field != "spec.template" && 18737 field != "GCEPersistentDisk.ReadOnly" && 18738 field != "spec.replicas" && 18739 field != "spec.template.labels" && 18740 field != "metadata.annotations" && 18741 field != "metadata.labels" && 18742 field != "status.replicas" { 18743 t.Errorf("%s: missing prefix for: %v", k, errs[i]) 18744 } 18745 } 18746 } 18747 } 18748 18749 func TestValidateNode(t *testing.T) { 18750 validSelector := map[string]string{"a": "b"} 18751 invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} 18752 successCases := []core.Node{{ 18753 ObjectMeta: metav1.ObjectMeta{ 18754 Name: "abc", 18755 Labels: validSelector, 18756 }, 18757 Status: core.NodeStatus{ 18758 Addresses: []core.NodeAddress{ 18759 {Type: core.NodeExternalIP, Address: "something"}, 18760 }, 18761 Capacity: core.ResourceList{ 18762 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18763 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 18764 core.ResourceName("my.org/gpu"): resource.MustParse("10"), 18765 core.ResourceName("hugepages-2Mi"): resource.MustParse("10Gi"), 18766 core.ResourceName("hugepages-1Gi"): resource.MustParse("0"), 18767 }, 18768 }, 18769 }, { 18770 ObjectMeta: metav1.ObjectMeta{ 18771 Name: "abc", 18772 }, 18773 Status: core.NodeStatus{ 18774 Addresses: []core.NodeAddress{ 18775 {Type: core.NodeExternalIP, Address: "something"}, 18776 }, 18777 Capacity: core.ResourceList{ 18778 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18779 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18780 }, 18781 }, 18782 }, { 18783 ObjectMeta: metav1.ObjectMeta{ 18784 Name: "abc", 18785 Labels: validSelector, 18786 }, 18787 Status: core.NodeStatus{ 18788 Addresses: []core.NodeAddress{ 18789 {Type: core.NodeExternalIP, Address: "something"}, 18790 }, 18791 Capacity: core.ResourceList{ 18792 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18793 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 18794 core.ResourceName("my.org/gpu"): resource.MustParse("10"), 18795 core.ResourceName("hugepages-2Mi"): resource.MustParse("10Gi"), 18796 core.ResourceName("hugepages-1Gi"): resource.MustParse("10Gi"), 18797 }, 18798 }, 18799 }, { 18800 ObjectMeta: metav1.ObjectMeta{ 18801 Name: "dedicated-node1", 18802 }, 18803 Status: core.NodeStatus{ 18804 Addresses: []core.NodeAddress{ 18805 {Type: core.NodeExternalIP, Address: "something"}, 18806 }, 18807 Capacity: core.ResourceList{ 18808 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18809 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18810 }, 18811 }, 18812 Spec: core.NodeSpec{ 18813 // Add a valid taint to a node 18814 Taints: []core.Taint{{Key: "GPU", Value: "true", Effect: "NoSchedule"}}, 18815 }, 18816 }, { 18817 ObjectMeta: metav1.ObjectMeta{ 18818 Name: "abc", 18819 Annotations: map[string]string{ 18820 core.PreferAvoidPodsAnnotationKey: ` 18821 { 18822 "preferAvoidPods": [ 18823 { 18824 "podSignature": { 18825 "podController": { 18826 "apiVersion": "v1", 18827 "kind": "ReplicationController", 18828 "name": "foo", 18829 "uid": "abcdef123456", 18830 "controller": true 18831 } 18832 }, 18833 "reason": "some reason", 18834 "message": "some message" 18835 } 18836 ] 18837 }`, 18838 }, 18839 }, 18840 Status: core.NodeStatus{ 18841 Addresses: []core.NodeAddress{ 18842 {Type: core.NodeExternalIP, Address: "something"}, 18843 }, 18844 Capacity: core.ResourceList{ 18845 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18846 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18847 }, 18848 }, 18849 }, { 18850 ObjectMeta: metav1.ObjectMeta{ 18851 Name: "abc", 18852 }, 18853 Status: core.NodeStatus{ 18854 Addresses: []core.NodeAddress{ 18855 {Type: core.NodeExternalIP, Address: "something"}, 18856 }, 18857 Capacity: core.ResourceList{ 18858 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18859 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18860 }, 18861 }, 18862 Spec: core.NodeSpec{ 18863 PodCIDRs: []string{"192.168.0.0/16"}, 18864 }, 18865 }, 18866 } 18867 for _, successCase := range successCases { 18868 if errs := ValidateNode(&successCase); len(errs) != 0 { 18869 t.Errorf("expected success: %v", errs) 18870 } 18871 } 18872 18873 errorCases := map[string]core.Node{ 18874 "zero-length Name": { 18875 ObjectMeta: metav1.ObjectMeta{ 18876 Name: "", 18877 Labels: validSelector, 18878 }, 18879 Status: core.NodeStatus{ 18880 Addresses: []core.NodeAddress{}, 18881 Capacity: core.ResourceList{ 18882 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18883 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 18884 }, 18885 }, 18886 }, 18887 "invalid-labels": { 18888 ObjectMeta: metav1.ObjectMeta{ 18889 Name: "abc-123", 18890 Labels: invalidSelector, 18891 }, 18892 Status: core.NodeStatus{ 18893 Capacity: core.ResourceList{ 18894 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18895 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 18896 }, 18897 }, 18898 }, 18899 "missing-taint-key": { 18900 ObjectMeta: metav1.ObjectMeta{ 18901 Name: "dedicated-node1", 18902 }, 18903 Spec: core.NodeSpec{ 18904 // Add a taint with an empty key to a node 18905 Taints: []core.Taint{{Key: "", Value: "special-user-1", Effect: "NoSchedule"}}, 18906 }, 18907 }, 18908 "bad-taint-key": { 18909 ObjectMeta: metav1.ObjectMeta{ 18910 Name: "dedicated-node1", 18911 }, 18912 Spec: core.NodeSpec{ 18913 // Add a taint with an invalid key to a node 18914 Taints: []core.Taint{{Key: "NoUppercaseOrSpecialCharsLike=Equals", Value: "special-user-1", Effect: "NoSchedule"}}, 18915 }, 18916 }, 18917 "bad-taint-value": { 18918 ObjectMeta: metav1.ObjectMeta{ 18919 Name: "dedicated-node2", 18920 }, 18921 Status: core.NodeStatus{ 18922 Addresses: []core.NodeAddress{ 18923 {Type: core.NodeExternalIP, Address: "something"}, 18924 }, 18925 Capacity: core.ResourceList{ 18926 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18927 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18928 }, 18929 }, 18930 Spec: core.NodeSpec{ 18931 // Add a taint with a bad value to a node 18932 Taints: []core.Taint{{Key: "dedicated", Value: "some\\bad\\value", Effect: "NoSchedule"}}, 18933 }, 18934 }, 18935 "missing-taint-effect": { 18936 ObjectMeta: metav1.ObjectMeta{ 18937 Name: "dedicated-node3", 18938 }, 18939 Status: core.NodeStatus{ 18940 Addresses: []core.NodeAddress{ 18941 {Type: core.NodeExternalIP, Address: "something"}, 18942 }, 18943 Capacity: core.ResourceList{ 18944 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18945 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18946 }, 18947 }, 18948 Spec: core.NodeSpec{ 18949 // Add a taint with an empty effect to a node 18950 Taints: []core.Taint{{Key: "dedicated", Value: "special-user-3", Effect: ""}}, 18951 }, 18952 }, 18953 "invalid-taint-effect": { 18954 ObjectMeta: metav1.ObjectMeta{ 18955 Name: "dedicated-node3", 18956 }, 18957 Status: core.NodeStatus{ 18958 Addresses: []core.NodeAddress{ 18959 {Type: core.NodeExternalIP, Address: "something"}, 18960 }, 18961 Capacity: core.ResourceList{ 18962 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 18963 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 18964 }, 18965 }, 18966 Spec: core.NodeSpec{ 18967 // Add a taint with NoExecute effect to a node 18968 Taints: []core.Taint{{Key: "dedicated", Value: "special-user-3", Effect: "NoScheduleNoAdmit"}}, 18969 }, 18970 }, 18971 "duplicated-taints-with-same-key-effect": { 18972 ObjectMeta: metav1.ObjectMeta{ 18973 Name: "dedicated-node1", 18974 }, 18975 Spec: core.NodeSpec{ 18976 // Add two taints to the node with the same key and effect; should be rejected. 18977 Taints: []core.Taint{ 18978 {Key: "dedicated", Value: "special-user-1", Effect: "NoSchedule"}, 18979 {Key: "dedicated", Value: "special-user-2", Effect: "NoSchedule"}, 18980 }, 18981 }, 18982 }, 18983 "missing-podSignature": { 18984 ObjectMeta: metav1.ObjectMeta{ 18985 Name: "abc-123", 18986 Annotations: map[string]string{ 18987 core.PreferAvoidPodsAnnotationKey: ` 18988 { 18989 "preferAvoidPods": [ 18990 { 18991 "reason": "some reason", 18992 "message": "some message" 18993 } 18994 ] 18995 }`, 18996 }, 18997 }, 18998 Status: core.NodeStatus{ 18999 Addresses: []core.NodeAddress{}, 19000 Capacity: core.ResourceList{ 19001 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19002 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 19003 }, 19004 }, 19005 }, 19006 "invalid-podController": { 19007 ObjectMeta: metav1.ObjectMeta{ 19008 Name: "abc-123", 19009 Annotations: map[string]string{ 19010 core.PreferAvoidPodsAnnotationKey: ` 19011 { 19012 "preferAvoidPods": [ 19013 { 19014 "podSignature": { 19015 "podController": { 19016 "apiVersion": "v1", 19017 "kind": "ReplicationController", 19018 "name": "foo", 19019 "uid": "abcdef123456", 19020 "controller": false 19021 } 19022 }, 19023 "reason": "some reason", 19024 "message": "some message" 19025 } 19026 ] 19027 }`, 19028 }, 19029 }, 19030 Status: core.NodeStatus{ 19031 Addresses: []core.NodeAddress{}, 19032 Capacity: core.ResourceList{ 19033 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19034 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 19035 }, 19036 }, 19037 }, 19038 "invalid-pod-cidr": { 19039 ObjectMeta: metav1.ObjectMeta{ 19040 Name: "abc", 19041 }, 19042 Status: core.NodeStatus{ 19043 Addresses: []core.NodeAddress{ 19044 {Type: core.NodeExternalIP, Address: "something"}, 19045 }, 19046 Capacity: core.ResourceList{ 19047 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19048 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 19049 }, 19050 }, 19051 Spec: core.NodeSpec{ 19052 PodCIDRs: []string{"192.168.0.0"}, 19053 }, 19054 }, 19055 "duplicate-pod-cidr": { 19056 ObjectMeta: metav1.ObjectMeta{ 19057 Name: "abc", 19058 }, 19059 Status: core.NodeStatus{ 19060 Addresses: []core.NodeAddress{ 19061 {Type: core.NodeExternalIP, Address: "something"}, 19062 }, 19063 Capacity: core.ResourceList{ 19064 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19065 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 19066 }, 19067 }, 19068 Spec: core.NodeSpec{ 19069 PodCIDRs: []string{"10.0.0.1/16", "10.0.0.1/16"}, 19070 }, 19071 }, 19072 } 19073 for k, v := range errorCases { 19074 errs := ValidateNode(&v) 19075 if len(errs) == 0 { 19076 t.Errorf("expected failure for %s", k) 19077 } 19078 for i := range errs { 19079 field := errs[i].Field 19080 expectedFields := map[string]bool{ 19081 "metadata.name": true, 19082 "metadata.labels": true, 19083 "metadata.annotations": true, 19084 "metadata.namespace": true, 19085 "spec.externalID": true, 19086 "spec.taints[0].key": true, 19087 "spec.taints[0].value": true, 19088 "spec.taints[0].effect": true, 19089 "metadata.annotations.scheduler.alpha.kubernetes.io/preferAvoidPods[0].PodSignature": true, 19090 "metadata.annotations.scheduler.alpha.kubernetes.io/preferAvoidPods[0].PodSignature.PodController.Controller": true, 19091 } 19092 if val, ok := expectedFields[field]; ok { 19093 if !val { 19094 t.Errorf("%s: missing prefix for: %v", k, errs[i]) 19095 } 19096 } 19097 } 19098 } 19099 } 19100 19101 func TestValidateNodeUpdate(t *testing.T) { 19102 tests := []struct { 19103 oldNode core.Node 19104 node core.Node 19105 valid bool 19106 }{ 19107 {core.Node{}, core.Node{}, true}, 19108 {core.Node{ 19109 ObjectMeta: metav1.ObjectMeta{ 19110 Name: "foo"}}, 19111 core.Node{ 19112 ObjectMeta: metav1.ObjectMeta{ 19113 Name: "bar"}, 19114 }, false}, 19115 {core.Node{ 19116 ObjectMeta: metav1.ObjectMeta{ 19117 Name: "foo", 19118 Labels: map[string]string{"foo": "bar"}, 19119 }, 19120 }, core.Node{ 19121 ObjectMeta: metav1.ObjectMeta{ 19122 Name: "foo", 19123 Labels: map[string]string{"foo": "baz"}, 19124 }, 19125 }, true}, 19126 {core.Node{ 19127 ObjectMeta: metav1.ObjectMeta{ 19128 Name: "foo", 19129 }, 19130 }, core.Node{ 19131 ObjectMeta: metav1.ObjectMeta{ 19132 Name: "foo", 19133 Labels: map[string]string{"foo": "baz"}, 19134 }, 19135 }, true}, 19136 {core.Node{ 19137 ObjectMeta: metav1.ObjectMeta{ 19138 Name: "foo", 19139 Labels: map[string]string{"bar": "foo"}, 19140 }, 19141 }, core.Node{ 19142 ObjectMeta: metav1.ObjectMeta{ 19143 Name: "foo", 19144 Labels: map[string]string{"foo": "baz"}, 19145 }, 19146 }, true}, 19147 {core.Node{ 19148 ObjectMeta: metav1.ObjectMeta{ 19149 Name: "foo", 19150 }, 19151 Spec: core.NodeSpec{ 19152 PodCIDRs: []string{}, 19153 }, 19154 }, core.Node{ 19155 ObjectMeta: metav1.ObjectMeta{ 19156 Name: "foo", 19157 }, 19158 Spec: core.NodeSpec{ 19159 PodCIDRs: []string{"192.168.0.0/16"}, 19160 }, 19161 }, true}, 19162 {core.Node{ 19163 ObjectMeta: metav1.ObjectMeta{ 19164 Name: "foo", 19165 }, 19166 Spec: core.NodeSpec{ 19167 PodCIDRs: []string{"192.123.0.0/16"}, 19168 }, 19169 }, core.Node{ 19170 ObjectMeta: metav1.ObjectMeta{ 19171 Name: "foo", 19172 }, 19173 Spec: core.NodeSpec{ 19174 PodCIDRs: []string{"192.168.0.0/16"}, 19175 }, 19176 }, false}, 19177 {core.Node{ 19178 ObjectMeta: metav1.ObjectMeta{ 19179 Name: "foo", 19180 }, 19181 Status: core.NodeStatus{ 19182 Capacity: core.ResourceList{ 19183 core.ResourceCPU: resource.MustParse("10000"), 19184 core.ResourceMemory: resource.MustParse("100"), 19185 }, 19186 }, 19187 }, core.Node{ 19188 ObjectMeta: metav1.ObjectMeta{ 19189 Name: "foo", 19190 }, 19191 Status: core.NodeStatus{ 19192 Capacity: core.ResourceList{ 19193 core.ResourceCPU: resource.MustParse("100"), 19194 core.ResourceMemory: resource.MustParse("10000"), 19195 }, 19196 }, 19197 }, true}, 19198 {core.Node{ 19199 ObjectMeta: metav1.ObjectMeta{ 19200 Name: "foo", 19201 Labels: map[string]string{"bar": "foo"}, 19202 }, 19203 Status: core.NodeStatus{ 19204 Capacity: core.ResourceList{ 19205 core.ResourceCPU: resource.MustParse("10000"), 19206 core.ResourceMemory: resource.MustParse("100"), 19207 }, 19208 }, 19209 }, core.Node{ 19210 ObjectMeta: metav1.ObjectMeta{ 19211 Name: "foo", 19212 Labels: map[string]string{"bar": "fooobaz"}, 19213 }, 19214 Status: core.NodeStatus{ 19215 Capacity: core.ResourceList{ 19216 core.ResourceCPU: resource.MustParse("100"), 19217 core.ResourceMemory: resource.MustParse("10000"), 19218 }, 19219 }, 19220 }, true}, 19221 {core.Node{ 19222 ObjectMeta: metav1.ObjectMeta{ 19223 Name: "foo", 19224 Labels: map[string]string{"bar": "foo"}, 19225 }, 19226 Status: core.NodeStatus{ 19227 Addresses: []core.NodeAddress{ 19228 {Type: core.NodeExternalIP, Address: "1.2.3.4"}, 19229 }, 19230 }, 19231 }, core.Node{ 19232 ObjectMeta: metav1.ObjectMeta{ 19233 Name: "foo", 19234 Labels: map[string]string{"bar": "fooobaz"}, 19235 }, 19236 }, true}, 19237 {core.Node{ 19238 ObjectMeta: metav1.ObjectMeta{ 19239 Name: "foo", 19240 Labels: map[string]string{"foo": "baz"}, 19241 }, 19242 }, core.Node{ 19243 ObjectMeta: metav1.ObjectMeta{ 19244 Name: "foo", 19245 Labels: map[string]string{"Foo": "baz"}, 19246 }, 19247 }, true}, 19248 {core.Node{ 19249 ObjectMeta: metav1.ObjectMeta{ 19250 Name: "foo", 19251 }, 19252 Spec: core.NodeSpec{ 19253 Unschedulable: false, 19254 }, 19255 }, core.Node{ 19256 ObjectMeta: metav1.ObjectMeta{ 19257 Name: "foo", 19258 }, 19259 Spec: core.NodeSpec{ 19260 Unschedulable: true, 19261 }, 19262 }, true}, 19263 {core.Node{ 19264 ObjectMeta: metav1.ObjectMeta{ 19265 Name: "foo", 19266 }, 19267 Spec: core.NodeSpec{ 19268 Unschedulable: false, 19269 }, 19270 }, core.Node{ 19271 ObjectMeta: metav1.ObjectMeta{ 19272 Name: "foo", 19273 }, 19274 Status: core.NodeStatus{ 19275 Addresses: []core.NodeAddress{ 19276 {Type: core.NodeExternalIP, Address: "1.1.1.1"}, 19277 {Type: core.NodeExternalIP, Address: "1.1.1.1"}, 19278 }, 19279 }, 19280 }, false}, 19281 {core.Node{ 19282 ObjectMeta: metav1.ObjectMeta{ 19283 Name: "foo", 19284 }, 19285 Spec: core.NodeSpec{ 19286 Unschedulable: false, 19287 }, 19288 }, core.Node{ 19289 ObjectMeta: metav1.ObjectMeta{ 19290 Name: "foo", 19291 }, 19292 Status: core.NodeStatus{ 19293 Addresses: []core.NodeAddress{ 19294 {Type: core.NodeExternalIP, Address: "1.1.1.1"}, 19295 {Type: core.NodeInternalIP, Address: "10.1.1.1"}, 19296 }, 19297 }, 19298 }, true}, 19299 {core.Node{ 19300 ObjectMeta: metav1.ObjectMeta{ 19301 Name: "foo", 19302 }, 19303 }, core.Node{ 19304 ObjectMeta: metav1.ObjectMeta{ 19305 Name: "foo", 19306 Annotations: map[string]string{ 19307 core.PreferAvoidPodsAnnotationKey: ` 19308 { 19309 "preferAvoidPods": [ 19310 { 19311 "podSignature": { 19312 "podController": { 19313 "apiVersion": "v1", 19314 "kind": "ReplicationController", 19315 "name": "foo", 19316 "uid": "abcdef123456", 19317 "controller": true 19318 } 19319 }, 19320 "reason": "some reason", 19321 "message": "some message" 19322 } 19323 ] 19324 }`, 19325 }, 19326 }, 19327 Spec: core.NodeSpec{ 19328 Unschedulable: false, 19329 }, 19330 }, true}, 19331 {core.Node{ 19332 ObjectMeta: metav1.ObjectMeta{ 19333 Name: "foo", 19334 }, 19335 }, core.Node{ 19336 ObjectMeta: metav1.ObjectMeta{ 19337 Name: "foo", 19338 Annotations: map[string]string{ 19339 core.PreferAvoidPodsAnnotationKey: ` 19340 { 19341 "preferAvoidPods": [ 19342 { 19343 "reason": "some reason", 19344 "message": "some message" 19345 } 19346 ] 19347 }`, 19348 }, 19349 }, 19350 }, false}, 19351 {core.Node{ 19352 ObjectMeta: metav1.ObjectMeta{ 19353 Name: "foo", 19354 }, 19355 }, core.Node{ 19356 ObjectMeta: metav1.ObjectMeta{ 19357 Name: "foo", 19358 Annotations: map[string]string{ 19359 core.PreferAvoidPodsAnnotationKey: ` 19360 { 19361 "preferAvoidPods": [ 19362 { 19363 "podSignature": { 19364 "podController": { 19365 "apiVersion": "v1", 19366 "kind": "ReplicationController", 19367 "name": "foo", 19368 "uid": "abcdef123456", 19369 "controller": false 19370 } 19371 }, 19372 "reason": "some reason", 19373 "message": "some message" 19374 } 19375 ] 19376 }`, 19377 }, 19378 }, 19379 }, false}, 19380 {core.Node{ 19381 ObjectMeta: metav1.ObjectMeta{ 19382 Name: "valid-extended-resources", 19383 }, 19384 }, core.Node{ 19385 ObjectMeta: metav1.ObjectMeta{ 19386 Name: "valid-extended-resources", 19387 }, 19388 Status: core.NodeStatus{ 19389 Capacity: core.ResourceList{ 19390 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19391 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 19392 core.ResourceName("example.com/a"): resource.MustParse("5"), 19393 core.ResourceName("example.com/b"): resource.MustParse("10"), 19394 }, 19395 }, 19396 }, true}, 19397 {core.Node{ 19398 ObjectMeta: metav1.ObjectMeta{ 19399 Name: "invalid-fractional-extended-capacity", 19400 }, 19401 }, core.Node{ 19402 ObjectMeta: metav1.ObjectMeta{ 19403 Name: "invalid-fractional-extended-capacity", 19404 }, 19405 Status: core.NodeStatus{ 19406 Capacity: core.ResourceList{ 19407 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19408 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 19409 core.ResourceName("example.com/a"): resource.MustParse("500m"), 19410 }, 19411 }, 19412 }, false}, 19413 {core.Node{ 19414 ObjectMeta: metav1.ObjectMeta{ 19415 Name: "invalid-fractional-extended-allocatable", 19416 }, 19417 }, core.Node{ 19418 ObjectMeta: metav1.ObjectMeta{ 19419 Name: "invalid-fractional-extended-allocatable", 19420 }, 19421 Status: core.NodeStatus{ 19422 Capacity: core.ResourceList{ 19423 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19424 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 19425 core.ResourceName("example.com/a"): resource.MustParse("5"), 19426 }, 19427 Allocatable: core.ResourceList{ 19428 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 19429 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 19430 core.ResourceName("example.com/a"): resource.MustParse("4.5"), 19431 }, 19432 }, 19433 }, false}, 19434 {core.Node{ 19435 ObjectMeta: metav1.ObjectMeta{ 19436 Name: "update-provider-id-when-not-set", 19437 }, 19438 }, core.Node{ 19439 ObjectMeta: metav1.ObjectMeta{ 19440 Name: "update-provider-id-when-not-set", 19441 }, 19442 Spec: core.NodeSpec{ 19443 ProviderID: "provider:///new", 19444 }, 19445 }, true}, 19446 {core.Node{ 19447 ObjectMeta: metav1.ObjectMeta{ 19448 Name: "update-provider-id-when-set", 19449 }, 19450 Spec: core.NodeSpec{ 19451 ProviderID: "provider:///old", 19452 }, 19453 }, core.Node{ 19454 ObjectMeta: metav1.ObjectMeta{ 19455 Name: "update-provider-id-when-set", 19456 }, 19457 Spec: core.NodeSpec{ 19458 ProviderID: "provider:///new", 19459 }, 19460 }, false}, 19461 {core.Node{ 19462 ObjectMeta: metav1.ObjectMeta{ 19463 Name: "pod-cidrs-as-is", 19464 }, 19465 Spec: core.NodeSpec{ 19466 PodCIDRs: []string{"192.168.0.0/16"}, 19467 }, 19468 }, core.Node{ 19469 ObjectMeta: metav1.ObjectMeta{ 19470 Name: "pod-cidrs-as-is", 19471 }, 19472 Spec: core.NodeSpec{ 19473 PodCIDRs: []string{"192.168.0.0/16"}, 19474 }, 19475 }, true}, 19476 {core.Node{ 19477 ObjectMeta: metav1.ObjectMeta{ 19478 Name: "pod-cidrs-as-is-2", 19479 }, 19480 Spec: core.NodeSpec{ 19481 PodCIDRs: []string{"192.168.0.0/16", "2000::/10"}, 19482 }, 19483 }, core.Node{ 19484 ObjectMeta: metav1.ObjectMeta{ 19485 Name: "pod-cidrs-as-is-2", 19486 }, 19487 Spec: core.NodeSpec{ 19488 PodCIDRs: []string{"192.168.0.0/16", "2000::/10"}, 19489 }, 19490 }, true}, 19491 {core.Node{ 19492 ObjectMeta: metav1.ObjectMeta{ 19493 Name: "pod-cidrs-not-same-length", 19494 }, 19495 Spec: core.NodeSpec{ 19496 PodCIDRs: []string{"192.168.0.0/16", "192.167.0.0/16", "2000::/10"}, 19497 }, 19498 }, core.Node{ 19499 ObjectMeta: metav1.ObjectMeta{ 19500 Name: "pod-cidrs-not-same-length", 19501 }, 19502 Spec: core.NodeSpec{ 19503 PodCIDRs: []string{"192.168.0.0/16", "2000::/10"}, 19504 }, 19505 }, false}, 19506 {core.Node{ 19507 ObjectMeta: metav1.ObjectMeta{ 19508 Name: "pod-cidrs-not-same", 19509 }, 19510 Spec: core.NodeSpec{ 19511 PodCIDRs: []string{"192.168.0.0/16", "2000::/10"}, 19512 }, 19513 }, core.Node{ 19514 ObjectMeta: metav1.ObjectMeta{ 19515 Name: "pod-cidrs-not-same", 19516 }, 19517 Spec: core.NodeSpec{ 19518 PodCIDRs: []string{"2000::/10", "192.168.0.0/16"}, 19519 }, 19520 }, false}, 19521 } 19522 for i, test := range tests { 19523 test.oldNode.ObjectMeta.ResourceVersion = "1" 19524 test.node.ObjectMeta.ResourceVersion = "1" 19525 errs := ValidateNodeUpdate(&test.node, &test.oldNode) 19526 if test.valid && len(errs) > 0 { 19527 t.Errorf("%d: Unexpected error: %v", i, errs) 19528 t.Logf("%#v vs %#v", test.oldNode.ObjectMeta, test.node.ObjectMeta) 19529 } 19530 if !test.valid && len(errs) == 0 { 19531 t.Errorf("%d: Unexpected non-error", i) 19532 } 19533 } 19534 } 19535 19536 func TestValidateServiceUpdate(t *testing.T) { 19537 requireDualStack := core.IPFamilyPolicyRequireDualStack 19538 preferDualStack := core.IPFamilyPolicyPreferDualStack 19539 singleStack := core.IPFamilyPolicySingleStack 19540 testCases := []struct { 19541 name string 19542 tweakSvc func(oldSvc, newSvc *core.Service) // given basic valid services, each test case can customize them 19543 numErrs int 19544 }{{ 19545 name: "no change", 19546 tweakSvc: func(oldSvc, newSvc *core.Service) { 19547 // do nothing 19548 }, 19549 numErrs: 0, 19550 }, { 19551 name: "change name", 19552 tweakSvc: func(oldSvc, newSvc *core.Service) { 19553 newSvc.Name += "2" 19554 }, 19555 numErrs: 1, 19556 }, { 19557 name: "change namespace", 19558 tweakSvc: func(oldSvc, newSvc *core.Service) { 19559 newSvc.Namespace += "2" 19560 }, 19561 numErrs: 1, 19562 }, { 19563 name: "change label valid", 19564 tweakSvc: func(oldSvc, newSvc *core.Service) { 19565 newSvc.Labels["key"] = "other-value" 19566 }, 19567 numErrs: 0, 19568 }, { 19569 name: "add label", 19570 tweakSvc: func(oldSvc, newSvc *core.Service) { 19571 newSvc.Labels["key2"] = "value2" 19572 }, 19573 numErrs: 0, 19574 }, { 19575 name: "change cluster IP", 19576 tweakSvc: func(oldSvc, newSvc *core.Service) { 19577 oldSvc.Spec.ClusterIP = "1.2.3.4" 19578 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19579 19580 newSvc.Spec.ClusterIP = "8.6.7.5" 19581 newSvc.Spec.ClusterIPs = []string{"8.6.7.5"} 19582 }, 19583 numErrs: 1, 19584 }, { 19585 name: "remove cluster IP", 19586 tweakSvc: func(oldSvc, newSvc *core.Service) { 19587 oldSvc.Spec.ClusterIP = "1.2.3.4" 19588 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19589 19590 newSvc.Spec.ClusterIP = "" 19591 newSvc.Spec.ClusterIPs = nil 19592 }, 19593 numErrs: 1, 19594 }, { 19595 name: "change affinity", 19596 tweakSvc: func(oldSvc, newSvc *core.Service) { 19597 newSvc.Spec.SessionAffinity = "ClientIP" 19598 newSvc.Spec.SessionAffinityConfig = &core.SessionAffinityConfig{ 19599 ClientIP: &core.ClientIPConfig{ 19600 TimeoutSeconds: utilpointer.Int32(90), 19601 }, 19602 } 19603 }, 19604 numErrs: 0, 19605 }, { 19606 name: "remove affinity", 19607 tweakSvc: func(oldSvc, newSvc *core.Service) { 19608 newSvc.Spec.SessionAffinity = "" 19609 }, 19610 numErrs: 1, 19611 }, { 19612 name: "change type", 19613 tweakSvc: func(oldSvc, newSvc *core.Service) { 19614 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19615 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19616 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19617 }, 19618 numErrs: 0, 19619 }, { 19620 name: "remove type", 19621 tweakSvc: func(oldSvc, newSvc *core.Service) { 19622 newSvc.Spec.Type = "" 19623 }, 19624 numErrs: 1, 19625 }, { 19626 name: "change type -> nodeport", 19627 tweakSvc: func(oldSvc, newSvc *core.Service) { 19628 newSvc.Spec.Type = core.ServiceTypeNodePort 19629 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19630 }, 19631 numErrs: 0, 19632 }, { 19633 name: "add loadBalancerSourceRanges", 19634 tweakSvc: func(oldSvc, newSvc *core.Service) { 19635 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19636 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19637 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19638 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19639 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19640 newSvc.Spec.LoadBalancerSourceRanges = []string{"10.0.0.0/8"} 19641 }, 19642 numErrs: 0, 19643 }, { 19644 name: "update loadBalancerSourceRanges", 19645 tweakSvc: func(oldSvc, newSvc *core.Service) { 19646 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19647 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19648 oldSvc.Spec.LoadBalancerSourceRanges = []string{"10.0.0.0/8"} 19649 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19650 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19651 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19652 newSvc.Spec.LoadBalancerSourceRanges = []string{"10.100.0.0/16"} 19653 }, 19654 numErrs: 0, 19655 }, { 19656 name: "LoadBalancer type cannot have None ClusterIP", 19657 tweakSvc: func(oldSvc, newSvc *core.Service) { 19658 newSvc.Spec.ClusterIP = "None" 19659 newSvc.Spec.ClusterIPs = []string{"None"} 19660 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19661 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19662 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19663 }, 19664 numErrs: 1, 19665 }, { 19666 name: "`None` ClusterIP can NOT be changed", 19667 tweakSvc: func(oldSvc, newSvc *core.Service) { 19668 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19669 newSvc.Spec.Type = core.ServiceTypeClusterIP 19670 19671 oldSvc.Spec.ClusterIP = "None" 19672 oldSvc.Spec.ClusterIPs = []string{"None"} 19673 19674 newSvc.Spec.ClusterIP = "1.2.3.4" 19675 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19676 }, 19677 numErrs: 1, 19678 }, { 19679 name: "`None` ClusterIP can NOT be removed", 19680 tweakSvc: func(oldSvc, newSvc *core.Service) { 19681 oldSvc.Spec.ClusterIP = "None" 19682 oldSvc.Spec.ClusterIPs = []string{"None"} 19683 19684 newSvc.Spec.ClusterIP = "" 19685 newSvc.Spec.ClusterIPs = nil 19686 }, 19687 numErrs: 1, 19688 }, { 19689 name: "ClusterIP can NOT be changed to None", 19690 tweakSvc: func(oldSvc, newSvc *core.Service) { 19691 oldSvc.Spec.ClusterIP = "1.2.3.4" 19692 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19693 19694 newSvc.Spec.ClusterIP = "None" 19695 newSvc.Spec.ClusterIPs = []string{"None"} 19696 }, 19697 numErrs: 1, 19698 }, 19699 19700 { 19701 name: "Service with ClusterIP type cannot change its set ClusterIP", 19702 tweakSvc: func(oldSvc, newSvc *core.Service) { 19703 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19704 newSvc.Spec.Type = core.ServiceTypeClusterIP 19705 19706 oldSvc.Spec.ClusterIP = "1.2.3.4" 19707 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19708 19709 newSvc.Spec.ClusterIP = "1.2.3.5" 19710 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19711 }, 19712 numErrs: 1, 19713 }, { 19714 name: "Service with ClusterIP type can change its empty ClusterIP", 19715 tweakSvc: func(oldSvc, newSvc *core.Service) { 19716 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19717 newSvc.Spec.Type = core.ServiceTypeClusterIP 19718 19719 oldSvc.Spec.ClusterIP = "" 19720 oldSvc.Spec.ClusterIPs = nil 19721 newSvc.Spec.ClusterIP = "1.2.3.5" 19722 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19723 }, 19724 numErrs: 0, 19725 }, { 19726 name: "Service with ClusterIP type cannot change its set ClusterIP when changing type to NodePort", 19727 tweakSvc: func(oldSvc, newSvc *core.Service) { 19728 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19729 newSvc.Spec.Type = core.ServiceTypeNodePort 19730 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19731 19732 oldSvc.Spec.ClusterIP = "1.2.3.4" 19733 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19734 19735 newSvc.Spec.ClusterIP = "1.2.3.5" 19736 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19737 }, 19738 numErrs: 1, 19739 }, { 19740 name: "Service with ClusterIP type can change its empty ClusterIP when changing type to NodePort", 19741 tweakSvc: func(oldSvc, newSvc *core.Service) { 19742 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19743 newSvc.Spec.Type = core.ServiceTypeNodePort 19744 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19745 19746 oldSvc.Spec.ClusterIP = "" 19747 oldSvc.Spec.ClusterIPs = nil 19748 19749 newSvc.Spec.ClusterIP = "1.2.3.5" 19750 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19751 }, 19752 numErrs: 0, 19753 }, { 19754 name: "Service with ClusterIP type cannot change its ClusterIP when changing type to LoadBalancer", 19755 tweakSvc: func(oldSvc, newSvc *core.Service) { 19756 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19757 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19758 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19759 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19760 19761 oldSvc.Spec.ClusterIP = "1.2.3.4" 19762 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19763 19764 newSvc.Spec.ClusterIP = "1.2.3.5" 19765 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19766 }, 19767 numErrs: 1, 19768 }, { 19769 name: "Service with ClusterIP type can change its empty ClusterIP when changing type to LoadBalancer", 19770 tweakSvc: func(oldSvc, newSvc *core.Service) { 19771 oldSvc.Spec.Type = core.ServiceTypeClusterIP 19772 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19773 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19774 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19775 19776 oldSvc.Spec.ClusterIP = "" 19777 oldSvc.Spec.ClusterIPs = nil 19778 19779 newSvc.Spec.ClusterIP = "1.2.3.5" 19780 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19781 }, 19782 numErrs: 0, 19783 }, { 19784 name: "Service with LoadBalancer type can change its AllocateLoadBalancerNodePorts from true to false", 19785 tweakSvc: func(oldSvc, newSvc *core.Service) { 19786 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19787 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19788 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19789 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19790 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(false) 19791 }, 19792 numErrs: 0, 19793 }, { 19794 name: "Service with LoadBalancer type can change its AllocateLoadBalancerNodePorts from false to true", 19795 tweakSvc: func(oldSvc, newSvc *core.Service) { 19796 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19797 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(false) 19798 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19799 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19800 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19801 }, 19802 numErrs: 0, 19803 }, { 19804 name: "Service with NodePort type cannot change its set ClusterIP", 19805 tweakSvc: func(oldSvc, newSvc *core.Service) { 19806 oldSvc.Spec.Type = core.ServiceTypeNodePort 19807 newSvc.Spec.Type = core.ServiceTypeNodePort 19808 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19809 19810 oldSvc.Spec.ClusterIP = "1.2.3.4" 19811 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19812 19813 newSvc.Spec.ClusterIP = "1.2.3.5" 19814 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19815 }, 19816 numErrs: 1, 19817 }, { 19818 name: "Service with NodePort type can change its empty ClusterIP", 19819 tweakSvc: func(oldSvc, newSvc *core.Service) { 19820 oldSvc.Spec.Type = core.ServiceTypeNodePort 19821 newSvc.Spec.Type = core.ServiceTypeNodePort 19822 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19823 19824 oldSvc.Spec.ClusterIP = "" 19825 oldSvc.Spec.ClusterIPs = nil 19826 19827 newSvc.Spec.ClusterIP = "1.2.3.5" 19828 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19829 }, 19830 numErrs: 0, 19831 }, { 19832 name: "Service with NodePort type cannot change its set ClusterIP when changing type to ClusterIP", 19833 tweakSvc: func(oldSvc, newSvc *core.Service) { 19834 oldSvc.Spec.Type = core.ServiceTypeNodePort 19835 newSvc.Spec.Type = core.ServiceTypeClusterIP 19836 19837 oldSvc.Spec.ClusterIP = "1.2.3.4" 19838 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19839 19840 newSvc.Spec.ClusterIP = "1.2.3.5" 19841 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19842 }, 19843 numErrs: 1, 19844 }, { 19845 name: "Service with NodePort type can change its empty ClusterIP when changing type to ClusterIP", 19846 tweakSvc: func(oldSvc, newSvc *core.Service) { 19847 oldSvc.Spec.Type = core.ServiceTypeNodePort 19848 newSvc.Spec.Type = core.ServiceTypeClusterIP 19849 19850 oldSvc.Spec.ClusterIP = "" 19851 oldSvc.Spec.ClusterIPs = nil 19852 19853 newSvc.Spec.ClusterIP = "1.2.3.5" 19854 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19855 }, 19856 numErrs: 0, 19857 }, { 19858 name: "Service with NodePort type cannot change its set ClusterIP when changing type to LoadBalancer", 19859 tweakSvc: func(oldSvc, newSvc *core.Service) { 19860 oldSvc.Spec.Type = core.ServiceTypeNodePort 19861 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19862 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19863 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19864 19865 oldSvc.Spec.ClusterIP = "1.2.3.4" 19866 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19867 19868 newSvc.Spec.ClusterIP = "1.2.3.5" 19869 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19870 }, 19871 numErrs: 1, 19872 }, { 19873 name: "Service with NodePort type can change its empty ClusterIP when changing type to LoadBalancer", 19874 tweakSvc: func(oldSvc, newSvc *core.Service) { 19875 oldSvc.Spec.Type = core.ServiceTypeNodePort 19876 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19877 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19878 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19879 19880 oldSvc.Spec.ClusterIP = "" 19881 oldSvc.Spec.ClusterIPs = nil 19882 19883 newSvc.Spec.ClusterIP = "1.2.3.5" 19884 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19885 }, 19886 numErrs: 0, 19887 }, { 19888 name: "Service with LoadBalancer type cannot change its set ClusterIP", 19889 tweakSvc: func(oldSvc, newSvc *core.Service) { 19890 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19891 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19892 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19893 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19894 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19895 19896 oldSvc.Spec.ClusterIP = "1.2.3.4" 19897 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19898 19899 newSvc.Spec.ClusterIP = "1.2.3.5" 19900 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19901 }, 19902 numErrs: 1, 19903 }, { 19904 name: "Service with LoadBalancer type can change its empty ClusterIP", 19905 tweakSvc: func(oldSvc, newSvc *core.Service) { 19906 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19907 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19908 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 19909 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19910 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19911 19912 oldSvc.Spec.ClusterIP = "" 19913 oldSvc.Spec.ClusterIPs = nil 19914 19915 newSvc.Spec.ClusterIP = "1.2.3.5" 19916 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19917 }, 19918 numErrs: 0, 19919 }, { 19920 name: "Service with LoadBalancer type cannot change its set ClusterIP when changing type to ClusterIP", 19921 tweakSvc: func(oldSvc, newSvc *core.Service) { 19922 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19923 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19924 newSvc.Spec.Type = core.ServiceTypeClusterIP 19925 19926 oldSvc.Spec.ClusterIP = "1.2.3.4" 19927 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19928 19929 newSvc.Spec.ClusterIP = "1.2.3.5" 19930 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19931 }, 19932 numErrs: 1, 19933 }, { 19934 name: "Service with LoadBalancer type can change its empty ClusterIP when changing type to ClusterIP", 19935 tweakSvc: func(oldSvc, newSvc *core.Service) { 19936 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19937 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19938 newSvc.Spec.Type = core.ServiceTypeClusterIP 19939 19940 oldSvc.Spec.ClusterIP = "" 19941 oldSvc.Spec.ClusterIPs = nil 19942 19943 newSvc.Spec.ClusterIP = "1.2.3.5" 19944 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19945 }, 19946 numErrs: 0, 19947 }, { 19948 name: "Service with LoadBalancer type cannot change its set ClusterIP when changing type to NodePort", 19949 tweakSvc: func(oldSvc, newSvc *core.Service) { 19950 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19951 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19952 newSvc.Spec.Type = core.ServiceTypeNodePort 19953 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19954 19955 oldSvc.Spec.ClusterIP = "1.2.3.4" 19956 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19957 19958 newSvc.Spec.ClusterIP = "1.2.3.5" 19959 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19960 }, 19961 numErrs: 1, 19962 }, { 19963 name: "Service with LoadBalancer type can change its empty ClusterIP when changing type to NodePort", 19964 tweakSvc: func(oldSvc, newSvc *core.Service) { 19965 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 19966 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 19967 newSvc.Spec.Type = core.ServiceTypeNodePort 19968 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 19969 19970 oldSvc.Spec.ClusterIP = "" 19971 oldSvc.Spec.ClusterIPs = nil 19972 19973 newSvc.Spec.ClusterIP = "1.2.3.5" 19974 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19975 }, 19976 numErrs: 0, 19977 }, { 19978 name: "Service with ExternalName type can change its empty ClusterIP when changing type to ClusterIP", 19979 tweakSvc: func(oldSvc, newSvc *core.Service) { 19980 oldSvc.Spec.Type = core.ServiceTypeExternalName 19981 newSvc.Spec.Type = core.ServiceTypeClusterIP 19982 19983 oldSvc.Spec.ClusterIP = "" 19984 oldSvc.Spec.ClusterIPs = nil 19985 19986 newSvc.Spec.ClusterIP = "1.2.3.5" 19987 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 19988 }, 19989 numErrs: 0, 19990 }, { 19991 name: "Service with ExternalName type can change its set ClusterIP when changing type to ClusterIP", 19992 tweakSvc: func(oldSvc, newSvc *core.Service) { 19993 oldSvc.Spec.Type = core.ServiceTypeExternalName 19994 newSvc.Spec.Type = core.ServiceTypeClusterIP 19995 19996 oldSvc.Spec.ClusterIP = "1.2.3.4" 19997 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 19998 19999 newSvc.Spec.ClusterIP = "1.2.3.5" 20000 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 20001 }, 20002 numErrs: 0, 20003 }, { 20004 name: "invalid node port with clusterIP None", 20005 tweakSvc: func(oldSvc, newSvc *core.Service) { 20006 oldSvc.Spec.Type = core.ServiceTypeNodePort 20007 newSvc.Spec.Type = core.ServiceTypeNodePort 20008 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20009 20010 oldSvc.Spec.Ports = append(oldSvc.Spec.Ports, core.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 20011 newSvc.Spec.Ports = append(newSvc.Spec.Ports, core.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1, TargetPort: intstr.FromInt32(1)}) 20012 20013 oldSvc.Spec.ClusterIP = "" 20014 oldSvc.Spec.ClusterIPs = nil 20015 20016 newSvc.Spec.ClusterIP = "None" 20017 newSvc.Spec.ClusterIPs = []string{"None"} 20018 }, 20019 numErrs: 1, 20020 }, 20021 /* Service IP Family */ 20022 { 20023 name: "convert from ExternalName", 20024 tweakSvc: func(oldSvc, newSvc *core.Service) { 20025 oldSvc.Spec.Type = core.ServiceTypeExternalName 20026 newSvc.Spec.Type = core.ServiceTypeClusterIP 20027 }, 20028 numErrs: 0, 20029 }, { 20030 name: "invalid: convert to ExternalName", 20031 tweakSvc: func(oldSvc, newSvc *core.Service) { 20032 singleStack := core.IPFamilyPolicySingleStack 20033 20034 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20035 oldSvc.Spec.ClusterIP = "10.0.0.10" 20036 oldSvc.Spec.ClusterIPs = []string{"10.0.0.10"} 20037 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20038 oldSvc.Spec.IPFamilyPolicy = &singleStack 20039 20040 newSvc.Spec.Type = core.ServiceTypeExternalName 20041 newSvc.Spec.ExternalName = "foo" 20042 /* 20043 not removing these fields is a validation error 20044 strategy takes care of resetting Families & Policy if ClusterIPs 20045 were reset. But it does not get called in validation testing. 20046 */ 20047 newSvc.Spec.ClusterIP = "10.0.0.10" 20048 newSvc.Spec.ClusterIPs = []string{"10.0.0.10"} 20049 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20050 newSvc.Spec.IPFamilyPolicy = &singleStack 20051 20052 }, 20053 numErrs: 3, 20054 }, { 20055 name: "valid: convert to ExternalName", 20056 tweakSvc: func(oldSvc, newSvc *core.Service) { 20057 singleStack := core.IPFamilyPolicySingleStack 20058 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20059 oldSvc.Spec.ClusterIP = "10.0.0.10" 20060 oldSvc.Spec.ClusterIPs = []string{"10.0.0.10"} 20061 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20062 oldSvc.Spec.IPFamilyPolicy = &singleStack 20063 20064 newSvc.Spec.Type = core.ServiceTypeExternalName 20065 newSvc.Spec.ExternalName = "foo" 20066 }, 20067 numErrs: 0, 20068 }, 20069 20070 { 20071 name: "same ServiceIPFamily", 20072 tweakSvc: func(oldSvc, newSvc *core.Service) { 20073 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20074 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20075 20076 newSvc.Spec.Type = core.ServiceTypeClusterIP 20077 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20078 }, 20079 numErrs: 0, 20080 }, { 20081 name: "same ServiceIPFamily, change IPFamilyPolicy to singleStack", 20082 tweakSvc: func(oldSvc, newSvc *core.Service) { 20083 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20084 oldSvc.Spec.IPFamilyPolicy = nil 20085 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20086 20087 newSvc.Spec.IPFamilyPolicy = &singleStack 20088 newSvc.Spec.Type = core.ServiceTypeClusterIP 20089 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20090 }, 20091 numErrs: 0, 20092 }, { 20093 name: "same ServiceIPFamily, change IPFamilyPolicy singleStack => requireDualStack", 20094 tweakSvc: func(oldSvc, newSvc *core.Service) { 20095 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20096 oldSvc.Spec.IPFamilyPolicy = &singleStack 20097 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20098 20099 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20100 newSvc.Spec.Type = core.ServiceTypeClusterIP 20101 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20102 }, 20103 numErrs: 0, 20104 }, 20105 20106 { 20107 name: "add a new ServiceIPFamily", 20108 tweakSvc: func(oldSvc, newSvc *core.Service) { 20109 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20110 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20111 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20112 20113 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20114 newSvc.Spec.Type = core.ServiceTypeClusterIP 20115 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20116 }, 20117 numErrs: 0, 20118 }, 20119 20120 { 20121 name: "ExternalName while changing Service IPFamily", 20122 tweakSvc: func(oldSvc, newSvc *core.Service) { 20123 oldSvc.Spec.ExternalName = "somename" 20124 oldSvc.Spec.Type = core.ServiceTypeExternalName 20125 20126 newSvc.Spec.ExternalName = "somename" 20127 newSvc.Spec.Type = core.ServiceTypeExternalName 20128 }, 20129 numErrs: 0, 20130 }, { 20131 name: "setting ipfamily from nil to v4", 20132 tweakSvc: func(oldSvc, newSvc *core.Service) { 20133 oldSvc.Spec.IPFamilies = nil 20134 20135 newSvc.Spec.ExternalName = "somename" 20136 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20137 }, 20138 numErrs: 0, 20139 }, { 20140 name: "setting ipfamily from nil to v6", 20141 tweakSvc: func(oldSvc, newSvc *core.Service) { 20142 oldSvc.Spec.IPFamilies = nil 20143 20144 newSvc.Spec.ExternalName = "somename" 20145 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 20146 }, 20147 numErrs: 0, 20148 }, { 20149 name: "change primary ServiceIPFamily", 20150 tweakSvc: func(oldSvc, newSvc *core.Service) { 20151 oldSvc.Spec.ClusterIP = "1.2.3.4" 20152 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20153 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20154 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20155 20156 newSvc.Spec.Type = core.ServiceTypeClusterIP 20157 newSvc.Spec.ClusterIP = "1.2.3.4" 20158 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20159 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 20160 }, 20161 numErrs: 2, 20162 }, 20163 /* upgrade + downgrade from/to dualstack tests */ 20164 { 20165 name: "valid: upgrade to dual stack with requiredDualStack", 20166 tweakSvc: func(oldSvc, newSvc *core.Service) { 20167 oldSvc.Spec.ClusterIP = "1.2.3.4" 20168 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20169 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20170 oldSvc.Spec.IPFamilyPolicy = &singleStack 20171 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20172 20173 newSvc.Spec.Type = core.ServiceTypeClusterIP 20174 newSvc.Spec.ClusterIP = "1.2.3.4" 20175 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20176 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20177 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20178 }, 20179 numErrs: 0, 20180 }, { 20181 name: "valid: upgrade to dual stack with preferDualStack", 20182 tweakSvc: func(oldSvc, newSvc *core.Service) { 20183 oldSvc.Spec.ClusterIP = "1.2.3.4" 20184 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20185 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20186 oldSvc.Spec.IPFamilyPolicy = &singleStack 20187 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20188 20189 newSvc.Spec.Type = core.ServiceTypeClusterIP 20190 newSvc.Spec.ClusterIP = "1.2.3.4" 20191 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20192 newSvc.Spec.IPFamilyPolicy = &preferDualStack 20193 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20194 }, 20195 numErrs: 0, 20196 }, 20197 20198 { 20199 name: "valid: upgrade to dual stack, no specific secondary ip", 20200 tweakSvc: func(oldSvc, newSvc *core.Service) { 20201 oldSvc.Spec.ClusterIP = "1.2.3.4" 20202 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20203 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20204 oldSvc.Spec.IPFamilyPolicy = &singleStack 20205 20206 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20207 20208 newSvc.Spec.Type = core.ServiceTypeClusterIP 20209 newSvc.Spec.ClusterIP = "1.2.3.4" 20210 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20211 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20212 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20213 }, 20214 numErrs: 0, 20215 }, { 20216 name: "valid: upgrade to dual stack, with specific secondary ip", 20217 tweakSvc: func(oldSvc, newSvc *core.Service) { 20218 oldSvc.Spec.ClusterIP = "1.2.3.4" 20219 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20220 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20221 oldSvc.Spec.IPFamilyPolicy = &singleStack 20222 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20223 20224 newSvc.Spec.Type = core.ServiceTypeClusterIP 20225 newSvc.Spec.ClusterIP = "1.2.3.4" 20226 newSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20227 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20228 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20229 }, 20230 numErrs: 0, 20231 }, { 20232 name: "valid: downgrade from dual to single", 20233 tweakSvc: func(oldSvc, newSvc *core.Service) { 20234 oldSvc.Spec.ClusterIP = "1.2.3.4" 20235 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20236 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20237 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20238 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20239 20240 newSvc.Spec.Type = core.ServiceTypeClusterIP 20241 newSvc.Spec.ClusterIP = "1.2.3.4" 20242 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20243 newSvc.Spec.IPFamilyPolicy = &singleStack 20244 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20245 }, 20246 numErrs: 0, 20247 }, { 20248 name: "valid: change families for a headless service", 20249 tweakSvc: func(oldSvc, newSvc *core.Service) { 20250 oldSvc.Spec.ClusterIP = "None" 20251 oldSvc.Spec.ClusterIPs = []string{"None"} 20252 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20253 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20254 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20255 20256 newSvc.Spec.Type = core.ServiceTypeClusterIP 20257 newSvc.Spec.ClusterIP = "None" 20258 newSvc.Spec.ClusterIPs = []string{"None"} 20259 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20260 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 20261 }, 20262 numErrs: 0, 20263 }, { 20264 name: "valid: upgrade a headless service", 20265 tweakSvc: func(oldSvc, newSvc *core.Service) { 20266 oldSvc.Spec.ClusterIP = "None" 20267 oldSvc.Spec.ClusterIPs = []string{"None"} 20268 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20269 oldSvc.Spec.IPFamilyPolicy = &singleStack 20270 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20271 20272 newSvc.Spec.Type = core.ServiceTypeClusterIP 20273 newSvc.Spec.ClusterIP = "None" 20274 newSvc.Spec.ClusterIPs = []string{"None"} 20275 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20276 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 20277 }, 20278 numErrs: 0, 20279 }, { 20280 name: "valid: downgrade a headless service", 20281 tweakSvc: func(oldSvc, newSvc *core.Service) { 20282 oldSvc.Spec.ClusterIP = "None" 20283 oldSvc.Spec.ClusterIPs = []string{"None"} 20284 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20285 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20286 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20287 20288 newSvc.Spec.Type = core.ServiceTypeClusterIP 20289 newSvc.Spec.ClusterIP = "None" 20290 newSvc.Spec.ClusterIPs = []string{"None"} 20291 newSvc.Spec.IPFamilyPolicy = &singleStack 20292 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol} 20293 }, 20294 numErrs: 0, 20295 }, 20296 20297 { 20298 name: "invalid flip families", 20299 tweakSvc: func(oldSvc, newSvc *core.Service) { 20300 oldSvc.Spec.ClusterIP = "1.2.3.40" 20301 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20302 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20303 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20304 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20305 20306 newSvc.Spec.Type = core.ServiceTypeClusterIP 20307 newSvc.Spec.ClusterIP = "2001::1" 20308 newSvc.Spec.ClusterIPs = []string{"2001::1", "1.2.3.5"} 20309 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20310 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv6Protocol, core.IPv4Protocol} 20311 }, 20312 numErrs: 4, 20313 }, { 20314 name: "invalid change first ip, in dualstack service", 20315 tweakSvc: func(oldSvc, newSvc *core.Service) { 20316 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20317 oldSvc.Spec.ClusterIP = "1.2.3.4" 20318 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20319 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20320 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20321 20322 newSvc.Spec.Type = core.ServiceTypeClusterIP 20323 newSvc.Spec.ClusterIP = "1.2.3.5" 20324 newSvc.Spec.ClusterIPs = []string{"1.2.3.5", "2001::1"} 20325 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20326 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20327 }, 20328 numErrs: 1, 20329 }, { 20330 name: "invalid, change second ip in dualstack service", 20331 tweakSvc: func(oldSvc, newSvc *core.Service) { 20332 oldSvc.Spec.ClusterIP = "1.2.3.4" 20333 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20334 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20335 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20336 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20337 20338 newSvc.Spec.Type = core.ServiceTypeClusterIP 20339 newSvc.Spec.ClusterIP = "1.2.3.4" 20340 newSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2002::1"} 20341 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20342 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20343 }, 20344 numErrs: 1, 20345 }, { 20346 name: "downgrade keeping the families", 20347 tweakSvc: func(oldSvc, newSvc *core.Service) { 20348 oldSvc.Spec.ClusterIP = "1.2.3.4" 20349 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20350 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20351 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20352 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20353 20354 newSvc.Spec.Type = core.ServiceTypeClusterIP 20355 newSvc.Spec.ClusterIP = "1.2.3.4" 20356 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20357 newSvc.Spec.IPFamilyPolicy = &singleStack 20358 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20359 }, 20360 numErrs: 0, // families and ips are trimmed in strategy 20361 }, { 20362 name: "invalid, downgrade without changing to singleStack", 20363 tweakSvc: func(oldSvc, newSvc *core.Service) { 20364 oldSvc.Spec.ClusterIP = "1.2.3.4" 20365 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20366 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20367 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20368 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20369 20370 newSvc.Spec.Type = core.ServiceTypeClusterIP 20371 newSvc.Spec.ClusterIP = "1.2.3.4" 20372 newSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20373 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20374 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20375 }, 20376 numErrs: 2, 20377 }, { 20378 name: "invalid, downgrade and change primary ip", 20379 tweakSvc: func(oldSvc, newSvc *core.Service) { 20380 oldSvc.Spec.ClusterIP = "1.2.3.4" 20381 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4", "2001::1"} 20382 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20383 oldSvc.Spec.IPFamilyPolicy = &requireDualStack 20384 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20385 20386 newSvc.Spec.Type = core.ServiceTypeClusterIP 20387 newSvc.Spec.ClusterIP = "1.2.3.5" 20388 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 20389 newSvc.Spec.IPFamilyPolicy = &singleStack 20390 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20391 }, 20392 numErrs: 1, 20393 }, { 20394 name: "invalid: upgrade to dual stack and change primary", 20395 tweakSvc: func(oldSvc, newSvc *core.Service) { 20396 oldSvc.Spec.ClusterIP = "1.2.3.4" 20397 oldSvc.Spec.ClusterIPs = []string{"1.2.3.4"} 20398 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20399 oldSvc.Spec.IPFamilyPolicy = &singleStack 20400 20401 oldSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol} 20402 20403 newSvc.Spec.Type = core.ServiceTypeClusterIP 20404 newSvc.Spec.ClusterIP = "1.2.3.5" 20405 newSvc.Spec.ClusterIPs = []string{"1.2.3.5"} 20406 newSvc.Spec.IPFamilyPolicy = &requireDualStack 20407 newSvc.Spec.IPFamilies = []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol} 20408 }, 20409 numErrs: 1, 20410 }, { 20411 name: "update to valid app protocol", 20412 tweakSvc: func(oldSvc, newSvc *core.Service) { 20413 oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt32(3000), Protocol: "TCP"}} 20414 newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt32(3000), Protocol: "TCP", AppProtocol: utilpointer.String("https")}} 20415 }, 20416 numErrs: 0, 20417 }, { 20418 name: "update to invalid app protocol", 20419 tweakSvc: func(oldSvc, newSvc *core.Service) { 20420 oldSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt32(3000), Protocol: "TCP"}} 20421 newSvc.Spec.Ports = []core.ServicePort{{Name: "a", Port: 443, TargetPort: intstr.FromInt32(3000), Protocol: "TCP", AppProtocol: utilpointer.String("~https")}} 20422 }, 20423 numErrs: 1, 20424 }, { 20425 name: "Set AllocateLoadBalancerNodePorts when type is not LoadBalancer", 20426 tweakSvc: func(oldSvc, newSvc *core.Service) { 20427 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20428 }, 20429 numErrs: 1, 20430 }, { 20431 name: "update LoadBalancer type of service without change LoadBalancerClass", 20432 tweakSvc: func(oldSvc, newSvc *core.Service) { 20433 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20434 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20435 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-old") 20436 20437 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20438 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20439 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20440 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-old") 20441 }, 20442 numErrs: 0, 20443 }, { 20444 name: "invalid: change LoadBalancerClass when update service", 20445 tweakSvc: func(oldSvc, newSvc *core.Service) { 20446 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20447 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20448 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-old") 20449 20450 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20451 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20452 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20453 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-new") 20454 }, 20455 numErrs: 1, 20456 }, { 20457 name: "invalid: unset LoadBalancerClass when update service", 20458 tweakSvc: func(oldSvc, newSvc *core.Service) { 20459 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20460 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20461 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-old") 20462 20463 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20464 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20465 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20466 newSvc.Spec.LoadBalancerClass = nil 20467 }, 20468 numErrs: 1, 20469 }, { 20470 name: "invalid: set LoadBalancerClass when update service", 20471 tweakSvc: func(oldSvc, newSvc *core.Service) { 20472 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20473 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20474 oldSvc.Spec.LoadBalancerClass = nil 20475 20476 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20477 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20478 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20479 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-new") 20480 }, 20481 numErrs: 1, 20482 }, { 20483 name: "update to LoadBalancer type of service with valid LoadBalancerClass", 20484 tweakSvc: func(oldSvc, newSvc *core.Service) { 20485 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20486 20487 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20488 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20489 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20490 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20491 }, 20492 numErrs: 0, 20493 }, { 20494 name: "update to LoadBalancer type of service without LoadBalancerClass", 20495 tweakSvc: func(oldSvc, newSvc *core.Service) { 20496 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20497 20498 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20499 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20500 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20501 newSvc.Spec.LoadBalancerClass = nil 20502 }, 20503 numErrs: 0, 20504 }, { 20505 name: "invalid: set invalid LoadBalancerClass when update service to LoadBalancer", 20506 tweakSvc: func(oldSvc, newSvc *core.Service) { 20507 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20508 20509 newSvc.Spec.Type = core.ServiceTypeLoadBalancer 20510 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20511 newSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20512 newSvc.Spec.LoadBalancerClass = utilpointer.String("Bad/LoadBalancerclass") 20513 }, 20514 numErrs: 2, 20515 }, { 20516 name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service", 20517 tweakSvc: func(oldSvc, newSvc *core.Service) { 20518 oldSvc.Spec.Type = core.ServiceTypeClusterIP 20519 20520 newSvc.Spec.Type = core.ServiceTypeClusterIP 20521 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20522 }, 20523 numErrs: 2, 20524 }, { 20525 name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service", 20526 tweakSvc: func(oldSvc, newSvc *core.Service) { 20527 oldSvc.Spec.Type = core.ServiceTypeExternalName 20528 20529 newSvc.Spec.Type = core.ServiceTypeExternalName 20530 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20531 }, 20532 numErrs: 3, 20533 }, { 20534 name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service", 20535 tweakSvc: func(oldSvc, newSvc *core.Service) { 20536 oldSvc.Spec.Type = core.ServiceTypeNodePort 20537 20538 newSvc.Spec.Type = core.ServiceTypeNodePort 20539 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20540 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20541 }, 20542 numErrs: 2, 20543 }, { 20544 name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service", 20545 tweakSvc: func(oldSvc, newSvc *core.Service) { 20546 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20547 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20548 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20549 20550 newSvc.Spec.Type = core.ServiceTypeClusterIP 20551 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20552 }, 20553 numErrs: 2, 20554 }, { 20555 name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service", 20556 tweakSvc: func(oldSvc, newSvc *core.Service) { 20557 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20558 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20559 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20560 20561 newSvc.Spec.Type = core.ServiceTypeExternalName 20562 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20563 }, 20564 numErrs: 3, 20565 }, { 20566 name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service", 20567 tweakSvc: func(oldSvc, newSvc *core.Service) { 20568 oldSvc.Spec.Type = core.ServiceTypeLoadBalancer 20569 oldSvc.Spec.AllocateLoadBalancerNodePorts = utilpointer.Bool(true) 20570 oldSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20571 20572 newSvc.Spec.Type = core.ServiceTypeNodePort 20573 newSvc.Spec.ExternalTrafficPolicy = core.ServiceExternalTrafficPolicyCluster 20574 newSvc.Spec.LoadBalancerClass = utilpointer.String("test.com/test-load-balancer-class") 20575 }, 20576 numErrs: 2, 20577 }, { 20578 name: "update internalTrafficPolicy from Cluster to Local", 20579 tweakSvc: func(oldSvc, newSvc *core.Service) { 20580 cluster := core.ServiceInternalTrafficPolicyCluster 20581 oldSvc.Spec.InternalTrafficPolicy = &cluster 20582 20583 local := core.ServiceInternalTrafficPolicyLocal 20584 newSvc.Spec.InternalTrafficPolicy = &local 20585 }, 20586 numErrs: 0, 20587 }, { 20588 name: "update internalTrafficPolicy from Local to Cluster", 20589 tweakSvc: func(oldSvc, newSvc *core.Service) { 20590 local := core.ServiceInternalTrafficPolicyLocal 20591 oldSvc.Spec.InternalTrafficPolicy = &local 20592 20593 cluster := core.ServiceInternalTrafficPolicyCluster 20594 newSvc.Spec.InternalTrafficPolicy = &cluster 20595 }, 20596 numErrs: 0, 20597 }, { 20598 name: "topology annotations are mismatched", 20599 tweakSvc: func(oldSvc, newSvc *core.Service) { 20600 newSvc.Annotations[core.DeprecatedAnnotationTopologyAwareHints] = "original" 20601 newSvc.Annotations[core.AnnotationTopologyMode] = "different" 20602 }, 20603 numErrs: 1, 20604 }, 20605 } 20606 20607 for _, tc := range testCases { 20608 t.Run(tc.name, func(t *testing.T) { 20609 oldSvc := makeValidService() 20610 newSvc := makeValidService() 20611 tc.tweakSvc(&oldSvc, &newSvc) 20612 errs := ValidateServiceUpdate(&newSvc, &oldSvc) 20613 if len(errs) != tc.numErrs { 20614 t.Errorf("Expected %d errors, got %d: %v", tc.numErrs, len(errs), errs.ToAggregate()) 20615 } 20616 }) 20617 } 20618 } 20619 20620 func TestValidateResourceNames(t *testing.T) { 20621 table := []struct { 20622 input core.ResourceName 20623 success bool 20624 expect string 20625 }{ 20626 {"memory", true, ""}, 20627 {"cpu", true, ""}, 20628 {"storage", true, ""}, 20629 {"requests.cpu", true, ""}, 20630 {"requests.memory", true, ""}, 20631 {"requests.storage", true, ""}, 20632 {"limits.cpu", true, ""}, 20633 {"limits.memory", true, ""}, 20634 {"network", false, ""}, 20635 {"disk", false, ""}, 20636 {"", false, ""}, 20637 {".", false, ""}, 20638 {"..", false, ""}, 20639 {"my.favorite.app.co/12345", true, ""}, 20640 {"my.favorite.app.co/_12345", false, ""}, 20641 {"my.favorite.app.co/12345_", false, ""}, 20642 {"kubernetes.io/..", false, ""}, 20643 {core.ResourceName("kubernetes.io/" + strings.Repeat("a", 63)), true, ""}, 20644 {core.ResourceName("kubernetes.io/" + strings.Repeat("a", 64)), false, ""}, 20645 {"kubernetes.io//", false, ""}, 20646 {"kubernetes.io", false, ""}, 20647 {"kubernetes.io/will/not/work/", false, ""}, 20648 } 20649 for k, item := range table { 20650 err := validateResourceName(item.input, field.NewPath("field")) 20651 if len(err) != 0 && item.success { 20652 t.Errorf("expected no failure for input %q", item.input) 20653 } else if len(err) == 0 && !item.success { 20654 t.Errorf("expected failure for input %q", item.input) 20655 for i := range err { 20656 detail := err[i].Detail 20657 if detail != "" && !strings.Contains(detail, item.expect) { 20658 t.Errorf("%d: expected error detail either empty or %s, got %s", k, item.expect, detail) 20659 } 20660 } 20661 } 20662 } 20663 } 20664 20665 func getResourceList(cpu, memory string) core.ResourceList { 20666 res := core.ResourceList{} 20667 if cpu != "" { 20668 res[core.ResourceCPU] = resource.MustParse(cpu) 20669 } 20670 if memory != "" { 20671 res[core.ResourceMemory] = resource.MustParse(memory) 20672 } 20673 return res 20674 } 20675 20676 func getStorageResourceList(storage string) core.ResourceList { 20677 res := core.ResourceList{} 20678 if storage != "" { 20679 res[core.ResourceStorage] = resource.MustParse(storage) 20680 } 20681 return res 20682 } 20683 20684 func getLocalStorageResourceList(ephemeralStorage string) core.ResourceList { 20685 res := core.ResourceList{} 20686 if ephemeralStorage != "" { 20687 res[core.ResourceEphemeralStorage] = resource.MustParse(ephemeralStorage) 20688 } 20689 return res 20690 } 20691 20692 func TestValidateLimitRangeForLocalStorage(t *testing.T) { 20693 testCases := []struct { 20694 name string 20695 spec core.LimitRangeSpec 20696 }{{ 20697 name: "all-fields-valid", 20698 spec: core.LimitRangeSpec{ 20699 Limits: []core.LimitRangeItem{{ 20700 Type: core.LimitTypePod, 20701 Max: getLocalStorageResourceList("10000Mi"), 20702 Min: getLocalStorageResourceList("100Mi"), 20703 MaxLimitRequestRatio: getLocalStorageResourceList(""), 20704 }, { 20705 Type: core.LimitTypeContainer, 20706 Max: getLocalStorageResourceList("10000Mi"), 20707 Min: getLocalStorageResourceList("100Mi"), 20708 Default: getLocalStorageResourceList("500Mi"), 20709 DefaultRequest: getLocalStorageResourceList("200Mi"), 20710 MaxLimitRequestRatio: getLocalStorageResourceList(""), 20711 }}, 20712 }, 20713 }, 20714 } 20715 20716 for _, testCase := range testCases { 20717 limitRange := &core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: testCase.name, Namespace: "foo"}, Spec: testCase.spec} 20718 if errs := ValidateLimitRange(limitRange); len(errs) != 0 { 20719 t.Errorf("Case %v, unexpected error: %v", testCase.name, errs) 20720 } 20721 } 20722 } 20723 20724 func TestValidateLimitRange(t *testing.T) { 20725 successCases := []struct { 20726 name string 20727 spec core.LimitRangeSpec 20728 }{{ 20729 name: "all-fields-valid", 20730 spec: core.LimitRangeSpec{ 20731 Limits: []core.LimitRangeItem{{ 20732 Type: core.LimitTypePod, 20733 Max: getResourceList("100m", "10000Mi"), 20734 Min: getResourceList("5m", "100Mi"), 20735 MaxLimitRequestRatio: getResourceList("10", ""), 20736 }, { 20737 Type: core.LimitTypeContainer, 20738 Max: getResourceList("100m", "10000Mi"), 20739 Min: getResourceList("5m", "100Mi"), 20740 Default: getResourceList("50m", "500Mi"), 20741 DefaultRequest: getResourceList("10m", "200Mi"), 20742 MaxLimitRequestRatio: getResourceList("10", ""), 20743 }, { 20744 Type: core.LimitTypePersistentVolumeClaim, 20745 Max: getStorageResourceList("10Gi"), 20746 Min: getStorageResourceList("5Gi"), 20747 }}, 20748 }, 20749 }, { 20750 name: "pvc-min-only", 20751 spec: core.LimitRangeSpec{ 20752 Limits: []core.LimitRangeItem{{ 20753 Type: core.LimitTypePersistentVolumeClaim, 20754 Min: getStorageResourceList("5Gi"), 20755 }}, 20756 }, 20757 }, { 20758 name: "pvc-max-only", 20759 spec: core.LimitRangeSpec{ 20760 Limits: []core.LimitRangeItem{{ 20761 Type: core.LimitTypePersistentVolumeClaim, 20762 Max: getStorageResourceList("10Gi"), 20763 }}, 20764 }, 20765 }, { 20766 name: "all-fields-valid-big-numbers", 20767 spec: core.LimitRangeSpec{ 20768 Limits: []core.LimitRangeItem{{ 20769 Type: core.LimitTypeContainer, 20770 Max: getResourceList("100m", "10000T"), 20771 Min: getResourceList("5m", "100Mi"), 20772 Default: getResourceList("50m", "500Mi"), 20773 DefaultRequest: getResourceList("10m", "200Mi"), 20774 MaxLimitRequestRatio: getResourceList("10", ""), 20775 }}, 20776 }, 20777 }, { 20778 name: "thirdparty-fields-all-valid-standard-container-resources", 20779 spec: core.LimitRangeSpec{ 20780 Limits: []core.LimitRangeItem{{ 20781 Type: "thirdparty.com/foo", 20782 Max: getResourceList("100m", "10000T"), 20783 Min: getResourceList("5m", "100Mi"), 20784 Default: getResourceList("50m", "500Mi"), 20785 DefaultRequest: getResourceList("10m", "200Mi"), 20786 MaxLimitRequestRatio: getResourceList("10", ""), 20787 }}, 20788 }, 20789 }, { 20790 name: "thirdparty-fields-all-valid-storage-resources", 20791 spec: core.LimitRangeSpec{ 20792 Limits: []core.LimitRangeItem{{ 20793 Type: "thirdparty.com/foo", 20794 Max: getStorageResourceList("10000T"), 20795 Min: getStorageResourceList("100Mi"), 20796 Default: getStorageResourceList("500Mi"), 20797 DefaultRequest: getStorageResourceList("200Mi"), 20798 MaxLimitRequestRatio: getStorageResourceList(""), 20799 }}, 20800 }, 20801 }, 20802 } 20803 20804 for _, successCase := range successCases { 20805 limitRange := &core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: successCase.name, Namespace: "foo"}, Spec: successCase.spec} 20806 if errs := ValidateLimitRange(limitRange); len(errs) != 0 { 20807 t.Errorf("Case %v, unexpected error: %v", successCase.name, errs) 20808 } 20809 } 20810 20811 errorCases := map[string]struct { 20812 R core.LimitRange 20813 D string 20814 }{ 20815 "zero-length-name": { 20816 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: "foo"}, Spec: core.LimitRangeSpec{}}, 20817 "name or generateName is required", 20818 }, 20819 "zero-length-namespace": { 20820 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: ""}, Spec: core.LimitRangeSpec{}}, 20821 "", 20822 }, 20823 "invalid-name": { 20824 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "^Invalid", Namespace: "foo"}, Spec: core.LimitRangeSpec{}}, 20825 dnsSubdomainLabelErrMsg, 20826 }, 20827 "invalid-namespace": { 20828 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "^Invalid"}, Spec: core.LimitRangeSpec{}}, 20829 dnsLabelErrMsg, 20830 }, 20831 "duplicate-limit-type": { 20832 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20833 Limits: []core.LimitRangeItem{{ 20834 Type: core.LimitTypePod, 20835 Max: getResourceList("100m", "10000m"), 20836 Min: getResourceList("0m", "100m"), 20837 }, { 20838 Type: core.LimitTypePod, 20839 Min: getResourceList("0m", "100m"), 20840 }}, 20841 }}, 20842 "", 20843 }, 20844 "default-limit-type-pod": { 20845 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20846 Limits: []core.LimitRangeItem{{ 20847 Type: core.LimitTypePod, 20848 Max: getResourceList("100m", "10000m"), 20849 Min: getResourceList("0m", "100m"), 20850 Default: getResourceList("10m", "100m"), 20851 }}, 20852 }}, 20853 "may not be specified when `type` is 'Pod'", 20854 }, 20855 "default-request-limit-type-pod": { 20856 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20857 Limits: []core.LimitRangeItem{{ 20858 Type: core.LimitTypePod, 20859 Max: getResourceList("100m", "10000m"), 20860 Min: getResourceList("0m", "100m"), 20861 DefaultRequest: getResourceList("10m", "100m"), 20862 }}, 20863 }}, 20864 "may not be specified when `type` is 'Pod'", 20865 }, 20866 "min value 100m is greater than max value 10m": { 20867 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20868 Limits: []core.LimitRangeItem{{ 20869 Type: core.LimitTypePod, 20870 Max: getResourceList("10m", ""), 20871 Min: getResourceList("100m", ""), 20872 }}, 20873 }}, 20874 "min value 100m is greater than max value 10m", 20875 }, 20876 "invalid spec default outside range": { 20877 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20878 Limits: []core.LimitRangeItem{{ 20879 Type: core.LimitTypeContainer, 20880 Max: getResourceList("1", ""), 20881 Min: getResourceList("100m", ""), 20882 Default: getResourceList("2000m", ""), 20883 }}, 20884 }}, 20885 "default value 2 is greater than max value 1", 20886 }, 20887 "invalid spec default request outside range": { 20888 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20889 Limits: []core.LimitRangeItem{{ 20890 Type: core.LimitTypeContainer, 20891 Max: getResourceList("1", ""), 20892 Min: getResourceList("100m", ""), 20893 DefaultRequest: getResourceList("2000m", ""), 20894 }}, 20895 }}, 20896 "default request value 2 is greater than max value 1", 20897 }, 20898 "invalid spec default request more than default": { 20899 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20900 Limits: []core.LimitRangeItem{{ 20901 Type: core.LimitTypeContainer, 20902 Max: getResourceList("2", ""), 20903 Min: getResourceList("100m", ""), 20904 Default: getResourceList("500m", ""), 20905 DefaultRequest: getResourceList("800m", ""), 20906 }}, 20907 }}, 20908 "default request value 800m is greater than default limit value 500m", 20909 }, 20910 "invalid spec maxLimitRequestRatio less than 1": { 20911 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20912 Limits: []core.LimitRangeItem{{ 20913 Type: core.LimitTypePod, 20914 MaxLimitRequestRatio: getResourceList("800m", ""), 20915 }}, 20916 }}, 20917 "ratio 800m is less than 1", 20918 }, 20919 "invalid spec maxLimitRequestRatio greater than max/min": { 20920 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20921 Limits: []core.LimitRangeItem{{ 20922 Type: core.LimitTypeContainer, 20923 Max: getResourceList("", "2Gi"), 20924 Min: getResourceList("", "512Mi"), 20925 MaxLimitRequestRatio: getResourceList("", "10"), 20926 }}, 20927 }}, 20928 "ratio 10 is greater than max/min = 4.000000", 20929 }, 20930 "invalid non standard limit type": { 20931 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20932 Limits: []core.LimitRangeItem{{ 20933 Type: "foo", 20934 Max: getStorageResourceList("10000T"), 20935 Min: getStorageResourceList("100Mi"), 20936 Default: getStorageResourceList("500Mi"), 20937 DefaultRequest: getStorageResourceList("200Mi"), 20938 MaxLimitRequestRatio: getStorageResourceList(""), 20939 }}, 20940 }}, 20941 "must be a standard limit type or fully qualified", 20942 }, 20943 "min and max values missing, one required": { 20944 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20945 Limits: []core.LimitRangeItem{{ 20946 Type: core.LimitTypePersistentVolumeClaim, 20947 }}, 20948 }}, 20949 "either minimum or maximum storage value is required, but neither was provided", 20950 }, 20951 "invalid min greater than max": { 20952 core.LimitRange{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: core.LimitRangeSpec{ 20953 Limits: []core.LimitRangeItem{{ 20954 Type: core.LimitTypePersistentVolumeClaim, 20955 Min: getStorageResourceList("10Gi"), 20956 Max: getStorageResourceList("1Gi"), 20957 }}, 20958 }}, 20959 "min value 10Gi is greater than max value 1Gi", 20960 }, 20961 } 20962 20963 for k, v := range errorCases { 20964 errs := ValidateLimitRange(&v.R) 20965 if len(errs) == 0 { 20966 t.Errorf("expected failure for %s", k) 20967 } 20968 for i := range errs { 20969 detail := errs[i].Detail 20970 if !strings.Contains(detail, v.D) { 20971 t.Errorf("[%s]: expected error detail either empty or %q, got %q", k, v.D, detail) 20972 } 20973 } 20974 } 20975 20976 } 20977 20978 func TestValidatePersistentVolumeClaimStatusUpdate(t *testing.T) { 20979 validClaim := testVolumeClaim("foo", "ns", core.PersistentVolumeClaimSpec{ 20980 AccessModes: []core.PersistentVolumeAccessMode{ 20981 core.ReadWriteOnce, 20982 core.ReadOnlyMany, 20983 }, 20984 Resources: core.VolumeResourceRequirements{ 20985 Requests: core.ResourceList{ 20986 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 20987 }, 20988 }, 20989 }) 20990 validConditionUpdate := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 20991 AccessModes: []core.PersistentVolumeAccessMode{ 20992 core.ReadWriteOnce, 20993 core.ReadOnlyMany, 20994 }, 20995 Resources: core.VolumeResourceRequirements{ 20996 Requests: core.ResourceList{ 20997 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 20998 }, 20999 }, 21000 }, core.PersistentVolumeClaimStatus{ 21001 Phase: core.ClaimPending, 21002 Conditions: []core.PersistentVolumeClaimCondition{ 21003 {Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue}, 21004 }, 21005 }) 21006 validAllocatedResources := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21007 AccessModes: []core.PersistentVolumeAccessMode{ 21008 core.ReadWriteOnce, 21009 core.ReadOnlyMany, 21010 }, 21011 Resources: core.VolumeResourceRequirements{ 21012 Requests: core.ResourceList{ 21013 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21014 }, 21015 }, 21016 }, core.PersistentVolumeClaimStatus{ 21017 Phase: core.ClaimPending, 21018 Conditions: []core.PersistentVolumeClaimCondition{ 21019 {Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue}, 21020 }, 21021 AllocatedResources: core.ResourceList{ 21022 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21023 }, 21024 }) 21025 21026 invalidAllocatedResources := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21027 AccessModes: []core.PersistentVolumeAccessMode{ 21028 core.ReadWriteOnce, 21029 core.ReadOnlyMany, 21030 }, 21031 Resources: core.VolumeResourceRequirements{ 21032 Requests: core.ResourceList{ 21033 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21034 }, 21035 }, 21036 }, core.PersistentVolumeClaimStatus{ 21037 Phase: core.ClaimPending, 21038 Conditions: []core.PersistentVolumeClaimCondition{ 21039 {Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue}, 21040 }, 21041 AllocatedResources: core.ResourceList{ 21042 core.ResourceName(core.ResourceStorage): resource.MustParse("-10G"), 21043 }, 21044 }) 21045 21046 noStoraegeClaimStatus := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21047 AccessModes: []core.PersistentVolumeAccessMode{ 21048 core.ReadWriteOnce, 21049 }, 21050 Resources: core.VolumeResourceRequirements{ 21051 Requests: core.ResourceList{ 21052 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21053 }, 21054 }, 21055 }, core.PersistentVolumeClaimStatus{ 21056 Phase: core.ClaimPending, 21057 AllocatedResources: core.ResourceList{ 21058 core.ResourceName(core.ResourceCPU): resource.MustParse("10G"), 21059 }, 21060 }) 21061 progressResizeStatus := core.PersistentVolumeClaimControllerResizeInProgress 21062 21063 invalidResizeStatus := core.ClaimResourceStatus("foo") 21064 validResizeKeyCustom := core.ResourceName("example.com/foo") 21065 invalidNativeResizeKey := core.ResourceName("kubernetes.io/foo") 21066 21067 validResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21068 AccessModes: []core.PersistentVolumeAccessMode{ 21069 core.ReadWriteOnce, 21070 }, 21071 }, core.PersistentVolumeClaimStatus{ 21072 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21073 core.ResourceStorage: progressResizeStatus, 21074 }, 21075 }) 21076 21077 validResizeStatusControllerResizeFailed := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21078 AccessModes: []core.PersistentVolumeAccessMode{ 21079 core.ReadWriteOnce, 21080 }, 21081 }, core.PersistentVolumeClaimStatus{ 21082 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21083 core.ResourceStorage: core.PersistentVolumeClaimControllerResizeFailed, 21084 }, 21085 }) 21086 21087 validNodeResizePending := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21088 AccessModes: []core.PersistentVolumeAccessMode{ 21089 core.ReadWriteOnce, 21090 }, 21091 }, core.PersistentVolumeClaimStatus{ 21092 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21093 core.ResourceStorage: core.PersistentVolumeClaimNodeResizePending, 21094 }, 21095 }) 21096 21097 validNodeResizeInProgress := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21098 AccessModes: []core.PersistentVolumeAccessMode{ 21099 core.ReadWriteOnce, 21100 }, 21101 }, core.PersistentVolumeClaimStatus{ 21102 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21103 core.ResourceStorage: core.PersistentVolumeClaimNodeResizeInProgress, 21104 }, 21105 }) 21106 21107 validNodeResizeFailed := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21108 AccessModes: []core.PersistentVolumeAccessMode{ 21109 core.ReadWriteOnce, 21110 }, 21111 }, core.PersistentVolumeClaimStatus{ 21112 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21113 core.ResourceStorage: core.PersistentVolumeClaimNodeResizeFailed, 21114 }, 21115 }) 21116 21117 invalidResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21118 AccessModes: []core.PersistentVolumeAccessMode{ 21119 core.ReadWriteOnce, 21120 }, 21121 }, core.PersistentVolumeClaimStatus{ 21122 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21123 core.ResourceStorage: invalidResizeStatus, 21124 }, 21125 }) 21126 21127 invalidNativeResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21128 AccessModes: []core.PersistentVolumeAccessMode{ 21129 core.ReadWriteOnce, 21130 }, 21131 }, core.PersistentVolumeClaimStatus{ 21132 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21133 invalidNativeResizeKey: core.PersistentVolumeClaimNodeResizePending, 21134 }, 21135 }) 21136 21137 validExternalResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21138 AccessModes: []core.PersistentVolumeAccessMode{ 21139 core.ReadWriteOnce, 21140 }, 21141 }, core.PersistentVolumeClaimStatus{ 21142 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21143 validResizeKeyCustom: core.PersistentVolumeClaimNodeResizePending, 21144 }, 21145 }) 21146 21147 multipleResourceStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21148 AccessModes: []core.PersistentVolumeAccessMode{ 21149 core.ReadWriteOnce, 21150 }, 21151 }, core.PersistentVolumeClaimStatus{ 21152 AllocatedResources: core.ResourceList{ 21153 core.ResourceStorage: resource.MustParse("5Gi"), 21154 validResizeKeyCustom: resource.MustParse("10Gi"), 21155 }, 21156 AllocatedResourceStatuses: map[core.ResourceName]core.ClaimResourceStatus{ 21157 core.ResourceStorage: core.PersistentVolumeClaimControllerResizeFailed, 21158 validResizeKeyCustom: core.PersistentVolumeClaimControllerResizeInProgress, 21159 }, 21160 }) 21161 21162 invalidNativeResourceAllocatedKey := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21163 AccessModes: []core.PersistentVolumeAccessMode{ 21164 core.ReadWriteOnce, 21165 core.ReadOnlyMany, 21166 }, 21167 Resources: core.VolumeResourceRequirements{ 21168 Requests: core.ResourceList{ 21169 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21170 }, 21171 }, 21172 }, core.PersistentVolumeClaimStatus{ 21173 Phase: core.ClaimPending, 21174 Conditions: []core.PersistentVolumeClaimCondition{ 21175 {Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue}, 21176 }, 21177 AllocatedResources: core.ResourceList{ 21178 invalidNativeResizeKey: resource.MustParse("14G"), 21179 }, 21180 }) 21181 21182 validExternalAllocatedResource := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{ 21183 AccessModes: []core.PersistentVolumeAccessMode{ 21184 core.ReadWriteOnce, 21185 core.ReadOnlyMany, 21186 }, 21187 Resources: core.VolumeResourceRequirements{ 21188 Requests: core.ResourceList{ 21189 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 21190 }, 21191 }, 21192 }, core.PersistentVolumeClaimStatus{ 21193 Phase: core.ClaimPending, 21194 Conditions: []core.PersistentVolumeClaimCondition{ 21195 {Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue}, 21196 }, 21197 AllocatedResources: core.ResourceList{ 21198 validResizeKeyCustom: resource.MustParse("14G"), 21199 }, 21200 }) 21201 21202 scenarios := map[string]struct { 21203 isExpectedFailure bool 21204 oldClaim *core.PersistentVolumeClaim 21205 newClaim *core.PersistentVolumeClaim 21206 enableRecoverFromExpansion bool 21207 }{ 21208 "condition-update-with-enabled-feature-gate": { 21209 isExpectedFailure: false, 21210 oldClaim: validClaim, 21211 newClaim: validConditionUpdate, 21212 }, 21213 "status-update-with-valid-allocatedResources-feature-enabled": { 21214 isExpectedFailure: false, 21215 oldClaim: validClaim, 21216 newClaim: validAllocatedResources, 21217 enableRecoverFromExpansion: true, 21218 }, 21219 "status-update-with-invalid-allocatedResources-native-key-feature-enabled": { 21220 isExpectedFailure: true, 21221 oldClaim: validClaim, 21222 newClaim: invalidNativeResourceAllocatedKey, 21223 enableRecoverFromExpansion: true, 21224 }, 21225 "status-update-with-valid-allocatedResources-external-key-feature-enabled": { 21226 isExpectedFailure: false, 21227 oldClaim: validClaim, 21228 newClaim: validExternalAllocatedResource, 21229 enableRecoverFromExpansion: true, 21230 }, 21231 21232 "status-update-with-invalid-allocatedResources-feature-enabled": { 21233 isExpectedFailure: true, 21234 oldClaim: validClaim, 21235 newClaim: invalidAllocatedResources, 21236 enableRecoverFromExpansion: true, 21237 }, 21238 "status-update-with-no-storage-update": { 21239 isExpectedFailure: true, 21240 oldClaim: validClaim, 21241 newClaim: noStoraegeClaimStatus, 21242 enableRecoverFromExpansion: true, 21243 }, 21244 "staus-update-with-controller-resize-failed": { 21245 isExpectedFailure: false, 21246 oldClaim: validClaim, 21247 newClaim: validResizeStatusControllerResizeFailed, 21248 enableRecoverFromExpansion: true, 21249 }, 21250 "staus-update-with-node-resize-pending": { 21251 isExpectedFailure: false, 21252 oldClaim: validClaim, 21253 newClaim: validNodeResizePending, 21254 enableRecoverFromExpansion: true, 21255 }, 21256 "staus-update-with-node-resize-inprogress": { 21257 isExpectedFailure: false, 21258 oldClaim: validClaim, 21259 newClaim: validNodeResizeInProgress, 21260 enableRecoverFromExpansion: true, 21261 }, 21262 "staus-update-with-node-resize-failed": { 21263 isExpectedFailure: false, 21264 oldClaim: validClaim, 21265 newClaim: validNodeResizeFailed, 21266 enableRecoverFromExpansion: true, 21267 }, 21268 "staus-update-with-invalid-native-resource-status-key": { 21269 isExpectedFailure: true, 21270 oldClaim: validClaim, 21271 newClaim: invalidNativeResizeStatusPVC, 21272 enableRecoverFromExpansion: true, 21273 }, 21274 "staus-update-with-valid-external-resource-status-key": { 21275 isExpectedFailure: false, 21276 oldClaim: validClaim, 21277 newClaim: validExternalResizeStatusPVC, 21278 enableRecoverFromExpansion: true, 21279 }, 21280 "status-update-with-multiple-resources-key": { 21281 isExpectedFailure: false, 21282 oldClaim: validClaim, 21283 newClaim: multipleResourceStatusPVC, 21284 enableRecoverFromExpansion: true, 21285 }, 21286 "status-update-with-valid-pvc-resize-status": { 21287 isExpectedFailure: false, 21288 oldClaim: validClaim, 21289 newClaim: validResizeStatusPVC, 21290 enableRecoverFromExpansion: true, 21291 }, 21292 "status-update-with-invalid-pvc-resize-status": { 21293 isExpectedFailure: true, 21294 oldClaim: validClaim, 21295 newClaim: invalidResizeStatusPVC, 21296 enableRecoverFromExpansion: true, 21297 }, 21298 "status-update-with-old-pvc-valid-resourcestatus-newpvc-invalid-recovery-disabled": { 21299 isExpectedFailure: true, 21300 oldClaim: validResizeStatusPVC, 21301 newClaim: invalidResizeStatusPVC, 21302 enableRecoverFromExpansion: false, 21303 }, 21304 "status-update-with-old-pvc-valid-allocatedResource-newpvc-invalid-recovery-disabled": { 21305 isExpectedFailure: true, 21306 oldClaim: validExternalAllocatedResource, 21307 newClaim: invalidNativeResourceAllocatedKey, 21308 enableRecoverFromExpansion: false, 21309 }, 21310 } 21311 for name, scenario := range scenarios { 21312 t.Run(name, func(t *testing.T) { 21313 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, scenario.enableRecoverFromExpansion) 21314 21315 validateOpts := ValidationOptionsForPersistentVolumeClaim(scenario.newClaim, scenario.oldClaim) 21316 21317 // ensure we have a resource version specified for updates 21318 scenario.oldClaim.ResourceVersion = "1" 21319 scenario.newClaim.ResourceVersion = "1" 21320 errs := ValidatePersistentVolumeClaimStatusUpdate(scenario.newClaim, scenario.oldClaim, validateOpts) 21321 if len(errs) == 0 && scenario.isExpectedFailure { 21322 t.Errorf("Unexpected success for scenario: %s", name) 21323 } 21324 if len(errs) > 0 && !scenario.isExpectedFailure { 21325 t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) 21326 } 21327 }) 21328 } 21329 } 21330 21331 func TestValidateResourceQuota(t *testing.T) { 21332 spec := core.ResourceQuotaSpec{ 21333 Hard: core.ResourceList{ 21334 core.ResourceCPU: resource.MustParse("100"), 21335 core.ResourceMemory: resource.MustParse("10000"), 21336 core.ResourceRequestsCPU: resource.MustParse("100"), 21337 core.ResourceRequestsMemory: resource.MustParse("10000"), 21338 core.ResourceLimitsCPU: resource.MustParse("100"), 21339 core.ResourceLimitsMemory: resource.MustParse("10000"), 21340 core.ResourcePods: resource.MustParse("10"), 21341 core.ResourceServices: resource.MustParse("0"), 21342 core.ResourceReplicationControllers: resource.MustParse("10"), 21343 core.ResourceQuotas: resource.MustParse("10"), 21344 core.ResourceConfigMaps: resource.MustParse("10"), 21345 core.ResourceSecrets: resource.MustParse("10"), 21346 }, 21347 } 21348 21349 terminatingSpec := core.ResourceQuotaSpec{ 21350 Hard: core.ResourceList{ 21351 core.ResourceCPU: resource.MustParse("100"), 21352 core.ResourceLimitsCPU: resource.MustParse("200"), 21353 }, 21354 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeTerminating}, 21355 } 21356 21357 nonTerminatingSpec := core.ResourceQuotaSpec{ 21358 Hard: core.ResourceList{ 21359 core.ResourceCPU: resource.MustParse("100"), 21360 }, 21361 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeNotTerminating}, 21362 } 21363 21364 bestEffortSpec := core.ResourceQuotaSpec{ 21365 Hard: core.ResourceList{ 21366 core.ResourcePods: resource.MustParse("100"), 21367 }, 21368 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeBestEffort}, 21369 } 21370 21371 nonBestEffortSpec := core.ResourceQuotaSpec{ 21372 Hard: core.ResourceList{ 21373 core.ResourceCPU: resource.MustParse("100"), 21374 }, 21375 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeNotBestEffort}, 21376 } 21377 21378 crossNamespaceAffinitySpec := core.ResourceQuotaSpec{ 21379 Hard: core.ResourceList{ 21380 core.ResourceCPU: resource.MustParse("100"), 21381 core.ResourceLimitsCPU: resource.MustParse("200"), 21382 }, 21383 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeCrossNamespacePodAffinity}, 21384 } 21385 21386 scopeSelectorSpec := core.ResourceQuotaSpec{ 21387 ScopeSelector: &core.ScopeSelector{ 21388 MatchExpressions: []core.ScopedResourceSelectorRequirement{{ 21389 ScopeName: core.ResourceQuotaScopePriorityClass, 21390 Operator: core.ScopeSelectorOpIn, 21391 Values: []string{"cluster-services"}, 21392 }}, 21393 }, 21394 } 21395 21396 // storage is not yet supported as a quota tracked resource 21397 invalidQuotaResourceSpec := core.ResourceQuotaSpec{ 21398 Hard: core.ResourceList{ 21399 core.ResourceStorage: resource.MustParse("10"), 21400 }, 21401 } 21402 21403 negativeSpec := core.ResourceQuotaSpec{ 21404 Hard: core.ResourceList{ 21405 core.ResourceCPU: resource.MustParse("-100"), 21406 core.ResourceMemory: resource.MustParse("-10000"), 21407 core.ResourcePods: resource.MustParse("-10"), 21408 core.ResourceServices: resource.MustParse("-10"), 21409 core.ResourceReplicationControllers: resource.MustParse("-10"), 21410 core.ResourceQuotas: resource.MustParse("-10"), 21411 core.ResourceConfigMaps: resource.MustParse("-10"), 21412 core.ResourceSecrets: resource.MustParse("-10"), 21413 }, 21414 } 21415 21416 fractionalComputeSpec := core.ResourceQuotaSpec{ 21417 Hard: core.ResourceList{ 21418 core.ResourceCPU: resource.MustParse("100m"), 21419 }, 21420 } 21421 21422 fractionalPodSpec := core.ResourceQuotaSpec{ 21423 Hard: core.ResourceList{ 21424 core.ResourcePods: resource.MustParse(".1"), 21425 core.ResourceServices: resource.MustParse(".5"), 21426 core.ResourceReplicationControllers: resource.MustParse("1.25"), 21427 core.ResourceQuotas: resource.MustParse("2.5"), 21428 }, 21429 } 21430 21431 invalidTerminatingScopePairsSpec := core.ResourceQuotaSpec{ 21432 Hard: core.ResourceList{ 21433 core.ResourceCPU: resource.MustParse("100"), 21434 }, 21435 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeTerminating, core.ResourceQuotaScopeNotTerminating}, 21436 } 21437 21438 invalidBestEffortScopePairsSpec := core.ResourceQuotaSpec{ 21439 Hard: core.ResourceList{ 21440 core.ResourcePods: resource.MustParse("100"), 21441 }, 21442 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScopeBestEffort, core.ResourceQuotaScopeNotBestEffort}, 21443 } 21444 21445 invalidCrossNamespaceAffinitySpec := core.ResourceQuotaSpec{ 21446 ScopeSelector: &core.ScopeSelector{ 21447 MatchExpressions: []core.ScopedResourceSelectorRequirement{{ 21448 ScopeName: core.ResourceQuotaScopeCrossNamespacePodAffinity, 21449 Operator: core.ScopeSelectorOpIn, 21450 Values: []string{"cluster-services"}, 21451 }}, 21452 }, 21453 } 21454 21455 invalidScopeNameSpec := core.ResourceQuotaSpec{ 21456 Hard: core.ResourceList{ 21457 core.ResourceCPU: resource.MustParse("100"), 21458 }, 21459 Scopes: []core.ResourceQuotaScope{core.ResourceQuotaScope("foo")}, 21460 } 21461 21462 testCases := map[string]struct { 21463 rq core.ResourceQuota 21464 errDetail string 21465 errField string 21466 }{ 21467 "no-scope": { 21468 rq: core.ResourceQuota{ 21469 ObjectMeta: metav1.ObjectMeta{ 21470 Name: "abc", 21471 Namespace: "foo", 21472 }, 21473 Spec: spec, 21474 }, 21475 }, 21476 "fractional-compute-spec": { 21477 rq: core.ResourceQuota{ 21478 ObjectMeta: metav1.ObjectMeta{ 21479 Name: "abc", 21480 Namespace: "foo", 21481 }, 21482 Spec: fractionalComputeSpec, 21483 }, 21484 }, 21485 "terminating-spec": { 21486 rq: core.ResourceQuota{ 21487 ObjectMeta: metav1.ObjectMeta{ 21488 Name: "abc", 21489 Namespace: "foo", 21490 }, 21491 Spec: terminatingSpec, 21492 }, 21493 }, 21494 "non-terminating-spec": { 21495 rq: core.ResourceQuota{ 21496 ObjectMeta: metav1.ObjectMeta{ 21497 Name: "abc", 21498 Namespace: "foo", 21499 }, 21500 Spec: nonTerminatingSpec, 21501 }, 21502 }, 21503 "best-effort-spec": { 21504 rq: core.ResourceQuota{ 21505 ObjectMeta: metav1.ObjectMeta{ 21506 Name: "abc", 21507 Namespace: "foo", 21508 }, 21509 Spec: bestEffortSpec, 21510 }, 21511 }, 21512 "cross-namespace-affinity-spec": { 21513 rq: core.ResourceQuota{ 21514 ObjectMeta: metav1.ObjectMeta{ 21515 Name: "abc", 21516 Namespace: "foo", 21517 }, 21518 Spec: crossNamespaceAffinitySpec, 21519 }, 21520 }, 21521 "scope-selector-spec": { 21522 rq: core.ResourceQuota{ 21523 ObjectMeta: metav1.ObjectMeta{ 21524 Name: "abc", 21525 Namespace: "foo", 21526 }, 21527 Spec: scopeSelectorSpec, 21528 }, 21529 }, 21530 "non-best-effort-spec": { 21531 rq: core.ResourceQuota{ 21532 ObjectMeta: metav1.ObjectMeta{ 21533 Name: "abc", 21534 Namespace: "foo", 21535 }, 21536 Spec: nonBestEffortSpec, 21537 }, 21538 }, 21539 "zero-length Name": { 21540 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: "foo"}, Spec: spec}, 21541 errDetail: "name or generateName is required", 21542 }, 21543 "zero-length Namespace": { 21544 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: ""}, Spec: spec}, 21545 errField: "metadata.namespace", 21546 }, 21547 "invalid Name": { 21548 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "^Invalid", Namespace: "foo"}, Spec: spec}, 21549 errDetail: dnsSubdomainLabelErrMsg, 21550 }, 21551 "invalid Namespace": { 21552 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "^Invalid"}, Spec: spec}, 21553 errDetail: dnsLabelErrMsg, 21554 }, 21555 "negative-limits": { 21556 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: negativeSpec}, 21557 errDetail: isNegativeErrorMsg, 21558 }, 21559 "fractional-api-resource": { 21560 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: fractionalPodSpec}, 21561 errDetail: isNotIntegerErrorMsg, 21562 }, 21563 "invalid-quota-resource": { 21564 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidQuotaResourceSpec}, 21565 errDetail: isInvalidQuotaResource, 21566 }, 21567 "invalid-quota-terminating-pair": { 21568 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidTerminatingScopePairsSpec}, 21569 errDetail: "conflicting scopes", 21570 }, 21571 "invalid-quota-besteffort-pair": { 21572 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidBestEffortScopePairsSpec}, 21573 errDetail: "conflicting scopes", 21574 }, 21575 "invalid-quota-scope-name": { 21576 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidScopeNameSpec}, 21577 errDetail: "unsupported scope", 21578 }, 21579 "invalid-cross-namespace-affinity": { 21580 rq: core.ResourceQuota{ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidCrossNamespaceAffinitySpec}, 21581 errDetail: "must be 'Exist' when scope is any of ResourceQuotaScopeTerminating, ResourceQuotaScopeNotTerminating, ResourceQuotaScopeBestEffort, ResourceQuotaScopeNotBestEffort or ResourceQuotaScopeCrossNamespacePodAffinity", 21582 }, 21583 } 21584 for name, tc := range testCases { 21585 t.Run(name, func(t *testing.T) { 21586 errs := ValidateResourceQuota(&tc.rq) 21587 if len(tc.errDetail) == 0 && len(tc.errField) == 0 && len(errs) != 0 { 21588 t.Errorf("expected success: %v", errs) 21589 } else if (len(tc.errDetail) != 0 || len(tc.errField) != 0) && len(errs) == 0 { 21590 t.Errorf("expected failure") 21591 } else { 21592 for i := range errs { 21593 if !strings.Contains(errs[i].Detail, tc.errDetail) { 21594 t.Errorf("expected error detail either empty or %s, got %s", tc.errDetail, errs[i].Detail) 21595 } 21596 } 21597 } 21598 }) 21599 } 21600 } 21601 21602 func TestValidateNamespace(t *testing.T) { 21603 validLabels := map[string]string{"a": "b"} 21604 invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} 21605 successCases := []core.Namespace{{ 21606 ObjectMeta: metav1.ObjectMeta{Name: "abc", Labels: validLabels}, 21607 }, { 21608 ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, 21609 Spec: core.NamespaceSpec{ 21610 Finalizers: []core.FinalizerName{"example.com/something", "example.com/other"}, 21611 }, 21612 }, 21613 } 21614 for _, successCase := range successCases { 21615 if errs := ValidateNamespace(&successCase); len(errs) != 0 { 21616 t.Errorf("expected success: %v", errs) 21617 } 21618 } 21619 errorCases := map[string]struct { 21620 R core.Namespace 21621 D string 21622 }{ 21623 "zero-length name": { 21624 core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ""}}, 21625 "", 21626 }, 21627 "defined-namespace": { 21628 core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: "makesnosense"}}, 21629 "", 21630 }, 21631 "invalid-labels": { 21632 core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "abc", Labels: invalidLabels}}, 21633 "", 21634 }, 21635 } 21636 for k, v := range errorCases { 21637 errs := ValidateNamespace(&v.R) 21638 if len(errs) == 0 { 21639 t.Errorf("expected failure for %s", k) 21640 } 21641 } 21642 } 21643 21644 func TestValidateNamespaceFinalizeUpdate(t *testing.T) { 21645 tests := []struct { 21646 oldNamespace core.Namespace 21647 namespace core.Namespace 21648 valid bool 21649 }{ 21650 {core.Namespace{}, core.Namespace{}, true}, 21651 {core.Namespace{ 21652 ObjectMeta: metav1.ObjectMeta{ 21653 Name: "foo"}}, 21654 core.Namespace{ 21655 ObjectMeta: metav1.ObjectMeta{ 21656 Name: "foo"}, 21657 Spec: core.NamespaceSpec{ 21658 Finalizers: []core.FinalizerName{"Foo"}, 21659 }, 21660 }, false}, 21661 {core.Namespace{ 21662 ObjectMeta: metav1.ObjectMeta{ 21663 Name: "foo"}, 21664 Spec: core.NamespaceSpec{ 21665 Finalizers: []core.FinalizerName{"foo.com/bar"}, 21666 }, 21667 }, 21668 core.Namespace{ 21669 ObjectMeta: metav1.ObjectMeta{ 21670 Name: "foo"}, 21671 Spec: core.NamespaceSpec{ 21672 Finalizers: []core.FinalizerName{"foo.com/bar", "what.com/bar"}, 21673 }, 21674 }, true}, 21675 {core.Namespace{ 21676 ObjectMeta: metav1.ObjectMeta{ 21677 Name: "fooemptyfinalizer"}, 21678 Spec: core.NamespaceSpec{ 21679 Finalizers: []core.FinalizerName{"foo.com/bar"}, 21680 }, 21681 }, 21682 core.Namespace{ 21683 ObjectMeta: metav1.ObjectMeta{ 21684 Name: "fooemptyfinalizer"}, 21685 Spec: core.NamespaceSpec{ 21686 Finalizers: []core.FinalizerName{"", "foo.com/bar", "what.com/bar"}, 21687 }, 21688 }, false}, 21689 } 21690 for i, test := range tests { 21691 test.namespace.ObjectMeta.ResourceVersion = "1" 21692 test.oldNamespace.ObjectMeta.ResourceVersion = "1" 21693 errs := ValidateNamespaceFinalizeUpdate(&test.namespace, &test.oldNamespace) 21694 if test.valid && len(errs) > 0 { 21695 t.Errorf("%d: Unexpected error: %v", i, errs) 21696 t.Logf("%#v vs %#v", test.oldNamespace, test.namespace) 21697 } 21698 if !test.valid && len(errs) == 0 { 21699 t.Errorf("%d: Unexpected non-error", i) 21700 } 21701 } 21702 } 21703 21704 func TestValidateNamespaceStatusUpdate(t *testing.T) { 21705 now := metav1.Now() 21706 21707 tests := []struct { 21708 oldNamespace core.Namespace 21709 namespace core.Namespace 21710 valid bool 21711 }{ 21712 {core.Namespace{}, core.Namespace{ 21713 Status: core.NamespaceStatus{ 21714 Phase: core.NamespaceActive, 21715 }, 21716 }, true}, 21717 // Cannot set deletionTimestamp via status update 21718 {core.Namespace{ 21719 ObjectMeta: metav1.ObjectMeta{ 21720 Name: "foo"}}, 21721 core.Namespace{ 21722 ObjectMeta: metav1.ObjectMeta{ 21723 Name: "foo", 21724 DeletionTimestamp: &now}, 21725 Status: core.NamespaceStatus{ 21726 Phase: core.NamespaceTerminating, 21727 }, 21728 }, false}, 21729 // Can update phase via status update 21730 {core.Namespace{ 21731 ObjectMeta: metav1.ObjectMeta{ 21732 Name: "foo", 21733 DeletionTimestamp: &now}}, 21734 core.Namespace{ 21735 ObjectMeta: metav1.ObjectMeta{ 21736 Name: "foo", 21737 DeletionTimestamp: &now}, 21738 Status: core.NamespaceStatus{ 21739 Phase: core.NamespaceTerminating, 21740 }, 21741 }, true}, 21742 {core.Namespace{ 21743 ObjectMeta: metav1.ObjectMeta{ 21744 Name: "foo"}}, 21745 core.Namespace{ 21746 ObjectMeta: metav1.ObjectMeta{ 21747 Name: "foo"}, 21748 Status: core.NamespaceStatus{ 21749 Phase: core.NamespaceTerminating, 21750 }, 21751 }, false}, 21752 {core.Namespace{ 21753 ObjectMeta: metav1.ObjectMeta{ 21754 Name: "foo"}}, 21755 core.Namespace{ 21756 ObjectMeta: metav1.ObjectMeta{ 21757 Name: "bar"}, 21758 Status: core.NamespaceStatus{ 21759 Phase: core.NamespaceTerminating, 21760 }, 21761 }, false}, 21762 } 21763 for i, test := range tests { 21764 test.namespace.ObjectMeta.ResourceVersion = "1" 21765 test.oldNamespace.ObjectMeta.ResourceVersion = "1" 21766 errs := ValidateNamespaceStatusUpdate(&test.namespace, &test.oldNamespace) 21767 if test.valid && len(errs) > 0 { 21768 t.Errorf("%d: Unexpected error: %v", i, errs) 21769 t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta) 21770 } 21771 if !test.valid && len(errs) == 0 { 21772 t.Errorf("%d: Unexpected non-error", i) 21773 } 21774 } 21775 } 21776 21777 func TestValidateNamespaceUpdate(t *testing.T) { 21778 tests := []struct { 21779 oldNamespace core.Namespace 21780 namespace core.Namespace 21781 valid bool 21782 }{ 21783 {core.Namespace{}, core.Namespace{}, true}, 21784 {core.Namespace{ 21785 ObjectMeta: metav1.ObjectMeta{ 21786 Name: "foo1"}}, 21787 core.Namespace{ 21788 ObjectMeta: metav1.ObjectMeta{ 21789 Name: "bar1"}, 21790 }, false}, 21791 {core.Namespace{ 21792 ObjectMeta: metav1.ObjectMeta{ 21793 Name: "foo2", 21794 Labels: map[string]string{"foo": "bar"}, 21795 }, 21796 }, core.Namespace{ 21797 ObjectMeta: metav1.ObjectMeta{ 21798 Name: "foo2", 21799 Labels: map[string]string{"foo": "baz"}, 21800 }, 21801 }, true}, 21802 {core.Namespace{ 21803 ObjectMeta: metav1.ObjectMeta{ 21804 Name: "foo3", 21805 }, 21806 }, core.Namespace{ 21807 ObjectMeta: metav1.ObjectMeta{ 21808 Name: "foo3", 21809 Labels: map[string]string{"foo": "baz"}, 21810 }, 21811 }, true}, 21812 {core.Namespace{ 21813 ObjectMeta: metav1.ObjectMeta{ 21814 Name: "foo4", 21815 Labels: map[string]string{"bar": "foo"}, 21816 }, 21817 }, core.Namespace{ 21818 ObjectMeta: metav1.ObjectMeta{ 21819 Name: "foo4", 21820 Labels: map[string]string{"foo": "baz"}, 21821 }, 21822 }, true}, 21823 {core.Namespace{ 21824 ObjectMeta: metav1.ObjectMeta{ 21825 Name: "foo5", 21826 Labels: map[string]string{"foo": "baz"}, 21827 }, 21828 }, core.Namespace{ 21829 ObjectMeta: metav1.ObjectMeta{ 21830 Name: "foo5", 21831 Labels: map[string]string{"Foo": "baz"}, 21832 }, 21833 }, true}, 21834 {core.Namespace{ 21835 ObjectMeta: metav1.ObjectMeta{ 21836 Name: "foo6", 21837 Labels: map[string]string{"foo": "baz"}, 21838 }, 21839 }, core.Namespace{ 21840 ObjectMeta: metav1.ObjectMeta{ 21841 Name: "foo6", 21842 Labels: map[string]string{"Foo": "baz"}, 21843 }, 21844 Spec: core.NamespaceSpec{ 21845 Finalizers: []core.FinalizerName{"kubernetes"}, 21846 }, 21847 Status: core.NamespaceStatus{ 21848 Phase: core.NamespaceTerminating, 21849 }, 21850 }, true}, 21851 } 21852 for i, test := range tests { 21853 test.namespace.ObjectMeta.ResourceVersion = "1" 21854 test.oldNamespace.ObjectMeta.ResourceVersion = "1" 21855 errs := ValidateNamespaceUpdate(&test.namespace, &test.oldNamespace) 21856 if test.valid && len(errs) > 0 { 21857 t.Errorf("%d: Unexpected error: %v", i, errs) 21858 t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta) 21859 } 21860 if !test.valid && len(errs) == 0 { 21861 t.Errorf("%d: Unexpected non-error", i) 21862 } 21863 } 21864 } 21865 21866 func TestValidateSecret(t *testing.T) { 21867 // Opaque secret validation 21868 validSecret := func() core.Secret { 21869 return core.Secret{ 21870 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, 21871 Data: map[string][]byte{ 21872 "data-1": []byte("bar"), 21873 }, 21874 } 21875 } 21876 21877 var ( 21878 emptyName = validSecret() 21879 invalidName = validSecret() 21880 emptyNs = validSecret() 21881 invalidNs = validSecret() 21882 overMaxSize = validSecret() 21883 invalidKey = validSecret() 21884 leadingDotKey = validSecret() 21885 dotKey = validSecret() 21886 doubleDotKey = validSecret() 21887 ) 21888 21889 emptyName.Name = "" 21890 invalidName.Name = "NoUppercaseOrSpecialCharsLike=Equals" 21891 emptyNs.Namespace = "" 21892 invalidNs.Namespace = "NoUppercaseOrSpecialCharsLike=Equals" 21893 overMaxSize.Data = map[string][]byte{ 21894 "over": make([]byte, core.MaxSecretSize+1), 21895 } 21896 invalidKey.Data["a*b"] = []byte("whoops") 21897 leadingDotKey.Data[".key"] = []byte("bar") 21898 dotKey.Data["."] = []byte("bar") 21899 doubleDotKey.Data[".."] = []byte("bar") 21900 21901 // kubernetes.io/service-account-token secret validation 21902 validServiceAccountTokenSecret := func() core.Secret { 21903 return core.Secret{ 21904 ObjectMeta: metav1.ObjectMeta{ 21905 Name: "foo", 21906 Namespace: "bar", 21907 Annotations: map[string]string{ 21908 core.ServiceAccountNameKey: "foo", 21909 }, 21910 }, 21911 Type: core.SecretTypeServiceAccountToken, 21912 Data: map[string][]byte{ 21913 "data-1": []byte("bar"), 21914 }, 21915 } 21916 } 21917 21918 var ( 21919 emptyTokenAnnotation = validServiceAccountTokenSecret() 21920 missingTokenAnnotation = validServiceAccountTokenSecret() 21921 missingTokenAnnotations = validServiceAccountTokenSecret() 21922 ) 21923 emptyTokenAnnotation.Annotations[core.ServiceAccountNameKey] = "" 21924 delete(missingTokenAnnotation.Annotations, core.ServiceAccountNameKey) 21925 missingTokenAnnotations.Annotations = nil 21926 21927 tests := map[string]struct { 21928 secret core.Secret 21929 valid bool 21930 }{ 21931 "valid": {validSecret(), true}, 21932 "empty name": {emptyName, false}, 21933 "invalid name": {invalidName, false}, 21934 "empty namespace": {emptyNs, false}, 21935 "invalid namespace": {invalidNs, false}, 21936 "over max size": {overMaxSize, false}, 21937 "invalid key": {invalidKey, false}, 21938 "valid service-account-token secret": {validServiceAccountTokenSecret(), true}, 21939 "empty service-account-token annotation": {emptyTokenAnnotation, false}, 21940 "missing service-account-token annotation": {missingTokenAnnotation, false}, 21941 "missing service-account-token annotations": {missingTokenAnnotations, false}, 21942 "leading dot key": {leadingDotKey, true}, 21943 "dot key": {dotKey, false}, 21944 "double dot key": {doubleDotKey, false}, 21945 } 21946 21947 for name, tc := range tests { 21948 errs := ValidateSecret(&tc.secret) 21949 if tc.valid && len(errs) > 0 { 21950 t.Errorf("%v: Unexpected error: %v", name, errs) 21951 } 21952 if !tc.valid && len(errs) == 0 { 21953 t.Errorf("%v: Unexpected non-error", name) 21954 } 21955 } 21956 } 21957 21958 func TestValidateSecretUpdate(t *testing.T) { 21959 validSecret := func() core.Secret { 21960 return core.Secret{ 21961 ObjectMeta: metav1.ObjectMeta{ 21962 Name: "foo", 21963 Namespace: "bar", 21964 ResourceVersion: "20", 21965 }, 21966 Data: map[string][]byte{ 21967 "data-1": []byte("bar"), 21968 }, 21969 } 21970 } 21971 21972 falseVal := false 21973 trueVal := true 21974 21975 secret := validSecret() 21976 immutableSecret := validSecret() 21977 immutableSecret.Immutable = &trueVal 21978 mutableSecret := validSecret() 21979 mutableSecret.Immutable = &falseVal 21980 21981 secretWithData := validSecret() 21982 secretWithData.Data["data-2"] = []byte("baz") 21983 immutableSecretWithData := validSecret() 21984 immutableSecretWithData.Immutable = &trueVal 21985 immutableSecretWithData.Data["data-2"] = []byte("baz") 21986 21987 secretWithChangedData := validSecret() 21988 secretWithChangedData.Data["data-1"] = []byte("foo") 21989 immutableSecretWithChangedData := validSecret() 21990 immutableSecretWithChangedData.Immutable = &trueVal 21991 immutableSecretWithChangedData.Data["data-1"] = []byte("foo") 21992 21993 tests := []struct { 21994 name string 21995 oldSecret core.Secret 21996 newSecret core.Secret 21997 valid bool 21998 }{{ 21999 name: "mark secret immutable", 22000 oldSecret: secret, 22001 newSecret: immutableSecret, 22002 valid: true, 22003 }, { 22004 name: "revert immutable secret", 22005 oldSecret: immutableSecret, 22006 newSecret: secret, 22007 valid: false, 22008 }, { 22009 name: "makr immutable secret mutable", 22010 oldSecret: immutableSecret, 22011 newSecret: mutableSecret, 22012 valid: false, 22013 }, { 22014 name: "add data in secret", 22015 oldSecret: secret, 22016 newSecret: secretWithData, 22017 valid: true, 22018 }, { 22019 name: "add data in immutable secret", 22020 oldSecret: immutableSecret, 22021 newSecret: immutableSecretWithData, 22022 valid: false, 22023 }, { 22024 name: "change data in secret", 22025 oldSecret: secret, 22026 newSecret: secretWithChangedData, 22027 valid: true, 22028 }, { 22029 name: "change data in immutable secret", 22030 oldSecret: immutableSecret, 22031 newSecret: immutableSecretWithChangedData, 22032 valid: false, 22033 }, 22034 } 22035 22036 for _, tc := range tests { 22037 t.Run(tc.name, func(t *testing.T) { 22038 errs := ValidateSecretUpdate(&tc.newSecret, &tc.oldSecret) 22039 if tc.valid && len(errs) > 0 { 22040 t.Errorf("Unexpected error: %v", errs) 22041 } 22042 if !tc.valid && len(errs) == 0 { 22043 t.Errorf("Unexpected lack of error") 22044 } 22045 }) 22046 } 22047 } 22048 22049 func TestValidateDockerConfigSecret(t *testing.T) { 22050 validDockerSecret := func() core.Secret { 22051 return core.Secret{ 22052 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, 22053 Type: core.SecretTypeDockercfg, 22054 Data: map[string][]byte{ 22055 core.DockerConfigKey: []byte(`{"https://index.docker.io/v1/": {"auth": "Y2x1ZWRyb29sZXIwMDAxOnBhc3N3b3Jk","email": "fake@example.com"}}`), 22056 }, 22057 } 22058 } 22059 validDockerSecret2 := func() core.Secret { 22060 return core.Secret{ 22061 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, 22062 Type: core.SecretTypeDockerConfigJSON, 22063 Data: map[string][]byte{ 22064 core.DockerConfigJSONKey: []byte(`{"auths":{"https://index.docker.io/v1/": {"auth": "Y2x1ZWRyb29sZXIwMDAxOnBhc3N3b3Jk","email": "fake@example.com"}}}`), 22065 }, 22066 } 22067 } 22068 22069 var ( 22070 missingDockerConfigKey = validDockerSecret() 22071 emptyDockerConfigKey = validDockerSecret() 22072 invalidDockerConfigKey = validDockerSecret() 22073 missingDockerConfigKey2 = validDockerSecret2() 22074 emptyDockerConfigKey2 = validDockerSecret2() 22075 invalidDockerConfigKey2 = validDockerSecret2() 22076 ) 22077 22078 delete(missingDockerConfigKey.Data, core.DockerConfigKey) 22079 emptyDockerConfigKey.Data[core.DockerConfigKey] = []byte("") 22080 invalidDockerConfigKey.Data[core.DockerConfigKey] = []byte("bad") 22081 delete(missingDockerConfigKey2.Data, core.DockerConfigJSONKey) 22082 emptyDockerConfigKey2.Data[core.DockerConfigJSONKey] = []byte("") 22083 invalidDockerConfigKey2.Data[core.DockerConfigJSONKey] = []byte("bad") 22084 22085 tests := map[string]struct { 22086 secret core.Secret 22087 valid bool 22088 }{ 22089 "valid dockercfg": {validDockerSecret(), true}, 22090 "missing dockercfg": {missingDockerConfigKey, false}, 22091 "empty dockercfg": {emptyDockerConfigKey, false}, 22092 "invalid dockercfg": {invalidDockerConfigKey, false}, 22093 "valid config.json": {validDockerSecret2(), true}, 22094 "missing config.json": {missingDockerConfigKey2, false}, 22095 "empty config.json": {emptyDockerConfigKey2, false}, 22096 "invalid config.json": {invalidDockerConfigKey2, false}, 22097 } 22098 22099 for name, tc := range tests { 22100 errs := ValidateSecret(&tc.secret) 22101 if tc.valid && len(errs) > 0 { 22102 t.Errorf("%v: Unexpected error: %v", name, errs) 22103 } 22104 if !tc.valid && len(errs) == 0 { 22105 t.Errorf("%v: Unexpected non-error", name) 22106 } 22107 } 22108 } 22109 22110 func TestValidateBasicAuthSecret(t *testing.T) { 22111 validBasicAuthSecret := func() core.Secret { 22112 return core.Secret{ 22113 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, 22114 Type: core.SecretTypeBasicAuth, 22115 Data: map[string][]byte{ 22116 core.BasicAuthUsernameKey: []byte("username"), 22117 core.BasicAuthPasswordKey: []byte("password"), 22118 }, 22119 } 22120 } 22121 22122 var ( 22123 missingBasicAuthUsernamePasswordKeys = validBasicAuthSecret() 22124 ) 22125 22126 delete(missingBasicAuthUsernamePasswordKeys.Data, core.BasicAuthUsernameKey) 22127 delete(missingBasicAuthUsernamePasswordKeys.Data, core.BasicAuthPasswordKey) 22128 22129 tests := map[string]struct { 22130 secret core.Secret 22131 valid bool 22132 }{ 22133 "valid": {validBasicAuthSecret(), true}, 22134 "missing username and password": {missingBasicAuthUsernamePasswordKeys, false}, 22135 } 22136 22137 for name, tc := range tests { 22138 errs := ValidateSecret(&tc.secret) 22139 if tc.valid && len(errs) > 0 { 22140 t.Errorf("%v: Unexpected error: %v", name, errs) 22141 } 22142 if !tc.valid && len(errs) == 0 { 22143 t.Errorf("%v: Unexpected non-error", name) 22144 } 22145 } 22146 } 22147 22148 func TestValidateSSHAuthSecret(t *testing.T) { 22149 validSSHAuthSecret := func() core.Secret { 22150 return core.Secret{ 22151 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, 22152 Type: core.SecretTypeSSHAuth, 22153 Data: map[string][]byte{ 22154 core.SSHAuthPrivateKey: []byte("foo-bar-baz"), 22155 }, 22156 } 22157 } 22158 22159 missingSSHAuthPrivateKey := validSSHAuthSecret() 22160 22161 delete(missingSSHAuthPrivateKey.Data, core.SSHAuthPrivateKey) 22162 22163 tests := map[string]struct { 22164 secret core.Secret 22165 valid bool 22166 }{ 22167 "valid": {validSSHAuthSecret(), true}, 22168 "missing private key": {missingSSHAuthPrivateKey, false}, 22169 } 22170 22171 for name, tc := range tests { 22172 errs := ValidateSecret(&tc.secret) 22173 if tc.valid && len(errs) > 0 { 22174 t.Errorf("%v: Unexpected error: %v", name, errs) 22175 } 22176 if !tc.valid && len(errs) == 0 { 22177 t.Errorf("%v: Unexpected non-error", name) 22178 } 22179 } 22180 } 22181 22182 func TestValidateEndpointsCreate(t *testing.T) { 22183 successCases := map[string]struct { 22184 endpoints core.Endpoints 22185 }{ 22186 "simple endpoint": { 22187 endpoints: core.Endpoints{ 22188 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22189 Subsets: []core.EndpointSubset{{ 22190 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}, {IP: "10.10.2.2"}}, 22191 Ports: []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}}, 22192 }, { 22193 Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}}, 22194 Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}, {Name: "b", Port: 76, Protocol: "TCP"}}, 22195 }}, 22196 }, 22197 }, 22198 "empty subsets": { 22199 endpoints: core.Endpoints{ 22200 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22201 }, 22202 }, 22203 "no name required for singleton port": { 22204 endpoints: core.Endpoints{ 22205 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22206 Subsets: []core.EndpointSubset{{ 22207 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22208 Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP"}}, 22209 }}, 22210 }, 22211 }, 22212 "valid appProtocol": { 22213 endpoints: core.Endpoints{ 22214 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22215 Subsets: []core.EndpointSubset{{ 22216 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22217 Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.String("HTTP")}}, 22218 }}, 22219 }, 22220 }, 22221 "empty ports": { 22222 endpoints: core.Endpoints{ 22223 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22224 Subsets: []core.EndpointSubset{{ 22225 Addresses: []core.EndpointAddress{{IP: "10.10.3.3"}}, 22226 }}, 22227 }, 22228 }, 22229 } 22230 22231 for name, tc := range successCases { 22232 t.Run(name, func(t *testing.T) { 22233 errs := ValidateEndpointsCreate(&tc.endpoints) 22234 if len(errs) != 0 { 22235 t.Errorf("Expected no validation errors, got %v", errs) 22236 } 22237 22238 }) 22239 } 22240 22241 errorCases := map[string]struct { 22242 endpoints core.Endpoints 22243 errorType field.ErrorType 22244 errorDetail string 22245 }{ 22246 "missing namespace": { 22247 endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc"}}, 22248 errorType: "FieldValueRequired", 22249 }, 22250 "missing name": { 22251 endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Namespace: "namespace"}}, 22252 errorType: "FieldValueRequired", 22253 }, 22254 "invalid namespace": { 22255 endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "no@#invalid.;chars\"allowed"}}, 22256 errorType: "FieldValueInvalid", 22257 errorDetail: dnsLabelErrMsg, 22258 }, 22259 "invalid name": { 22260 endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "-_Invliad^&Characters", Namespace: "namespace"}}, 22261 errorType: "FieldValueInvalid", 22262 errorDetail: dnsSubdomainLabelErrMsg, 22263 }, 22264 "empty addresses": { 22265 endpoints: core.Endpoints{ 22266 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22267 Subsets: []core.EndpointSubset{{ 22268 Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}}, 22269 }}, 22270 }, 22271 errorType: "FieldValueRequired", 22272 }, 22273 "invalid IP": { 22274 endpoints: core.Endpoints{ 22275 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22276 Subsets: []core.EndpointSubset{{ 22277 Addresses: []core.EndpointAddress{{IP: "[2001:0db8:85a3:0042:1000:8a2e:0370:7334]"}}, 22278 Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}}, 22279 }}, 22280 }, 22281 errorType: "FieldValueInvalid", 22282 errorDetail: "must be a valid IP address", 22283 }, 22284 "Multiple ports, one without name": { 22285 endpoints: core.Endpoints{ 22286 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22287 Subsets: []core.EndpointSubset{{ 22288 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22289 Ports: []core.EndpointPort{{Port: 8675, Protocol: "TCP"}, {Name: "b", Port: 309, Protocol: "TCP"}}, 22290 }}, 22291 }, 22292 errorType: "FieldValueRequired", 22293 }, 22294 "Invalid port number": { 22295 endpoints: core.Endpoints{ 22296 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22297 Subsets: []core.EndpointSubset{{ 22298 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22299 Ports: []core.EndpointPort{{Name: "a", Port: 66000, Protocol: "TCP"}}, 22300 }}, 22301 }, 22302 errorType: "FieldValueInvalid", 22303 errorDetail: "between", 22304 }, 22305 "Invalid protocol": { 22306 endpoints: core.Endpoints{ 22307 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22308 Subsets: []core.EndpointSubset{{ 22309 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22310 Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "Protocol"}}, 22311 }}, 22312 }, 22313 errorType: "FieldValueNotSupported", 22314 }, 22315 "Address missing IP": { 22316 endpoints: core.Endpoints{ 22317 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22318 Subsets: []core.EndpointSubset{{ 22319 Addresses: []core.EndpointAddress{{}}, 22320 Ports: []core.EndpointPort{{Name: "a", Port: 93, Protocol: "TCP"}}, 22321 }}, 22322 }, 22323 errorType: "FieldValueInvalid", 22324 errorDetail: "must be a valid IP address", 22325 }, 22326 "Port missing number": { 22327 endpoints: core.Endpoints{ 22328 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22329 Subsets: []core.EndpointSubset{{ 22330 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22331 Ports: []core.EndpointPort{{Name: "a", Protocol: "TCP"}}, 22332 }}, 22333 }, 22334 errorType: "FieldValueInvalid", 22335 errorDetail: "between", 22336 }, 22337 "Port missing protocol": { 22338 endpoints: core.Endpoints{ 22339 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22340 Subsets: []core.EndpointSubset{{ 22341 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22342 Ports: []core.EndpointPort{{Name: "a", Port: 93}}, 22343 }}, 22344 }, 22345 errorType: "FieldValueRequired", 22346 }, 22347 "Address is loopback": { 22348 endpoints: core.Endpoints{ 22349 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22350 Subsets: []core.EndpointSubset{{ 22351 Addresses: []core.EndpointAddress{{IP: "127.0.0.1"}}, 22352 Ports: []core.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}}, 22353 }}, 22354 }, 22355 errorType: "FieldValueInvalid", 22356 errorDetail: "loopback", 22357 }, 22358 "Address is link-local": { 22359 endpoints: core.Endpoints{ 22360 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22361 Subsets: []core.EndpointSubset{{ 22362 Addresses: []core.EndpointAddress{{IP: "169.254.169.254"}}, 22363 Ports: []core.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}}, 22364 }}, 22365 }, 22366 errorType: "FieldValueInvalid", 22367 errorDetail: "link-local", 22368 }, 22369 "Address is link-local multicast": { 22370 endpoints: core.Endpoints{ 22371 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22372 Subsets: []core.EndpointSubset{{ 22373 Addresses: []core.EndpointAddress{{IP: "224.0.0.1"}}, 22374 Ports: []core.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP"}}, 22375 }}, 22376 }, 22377 errorType: "FieldValueInvalid", 22378 errorDetail: "link-local multicast", 22379 }, 22380 "Invalid AppProtocol": { 22381 endpoints: core.Endpoints{ 22382 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace"}, 22383 Subsets: []core.EndpointSubset{{ 22384 Addresses: []core.EndpointAddress{{IP: "10.10.1.1"}}, 22385 Ports: []core.EndpointPort{{Name: "p", Port: 93, Protocol: "TCP", AppProtocol: utilpointer.String("lots-of[invalid]-{chars}")}}, 22386 }}, 22387 }, 22388 errorType: "FieldValueInvalid", 22389 errorDetail: "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character", 22390 }, 22391 } 22392 22393 for k, v := range errorCases { 22394 t.Run(k, func(t *testing.T) { 22395 if errs := ValidateEndpointsCreate(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 22396 t.Errorf("Expected error type %s with detail %q, got %v", v.errorType, v.errorDetail, errs) 22397 } 22398 }) 22399 } 22400 } 22401 22402 func TestValidateEndpointsUpdate(t *testing.T) { 22403 baseEndpoints := core.Endpoints{ 22404 ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "namespace", ResourceVersion: "1234"}, 22405 Subsets: []core.EndpointSubset{{ 22406 Addresses: []core.EndpointAddress{{IP: "10.1.2.3"}}, 22407 }}, 22408 } 22409 22410 testCases := map[string]struct { 22411 tweakOldEndpoints func(ep *core.Endpoints) 22412 tweakNewEndpoints func(ep *core.Endpoints) 22413 numExpectedErrors int 22414 }{ 22415 "update to valid app protocol": { 22416 tweakOldEndpoints: func(ep *core.Endpoints) { 22417 ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}} 22418 }, 22419 tweakNewEndpoints: func(ep *core.Endpoints) { 22420 ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.String("https")}} 22421 }, 22422 numExpectedErrors: 0, 22423 }, 22424 "update to invalid app protocol": { 22425 tweakOldEndpoints: func(ep *core.Endpoints) { 22426 ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP"}} 22427 }, 22428 tweakNewEndpoints: func(ep *core.Endpoints) { 22429 ep.Subsets[0].Ports = []core.EndpointPort{{Name: "a", Port: 8675, Protocol: "TCP", AppProtocol: utilpointer.String("~https")}} 22430 }, 22431 numExpectedErrors: 1, 22432 }, 22433 } 22434 22435 for name, tc := range testCases { 22436 t.Run(name, func(t *testing.T) { 22437 oldEndpoints := baseEndpoints.DeepCopy() 22438 tc.tweakOldEndpoints(oldEndpoints) 22439 newEndpoints := baseEndpoints.DeepCopy() 22440 tc.tweakNewEndpoints(newEndpoints) 22441 22442 errs := ValidateEndpointsUpdate(newEndpoints, oldEndpoints) 22443 if len(errs) != tc.numExpectedErrors { 22444 t.Errorf("Expected %d validation errors, got %d: %v", tc.numExpectedErrors, len(errs), errs) 22445 } 22446 22447 }) 22448 } 22449 } 22450 22451 func TestValidateWindowsSecurityContext(t *testing.T) { 22452 tests := []struct { 22453 name string 22454 sc *core.PodSpec 22455 expectError bool 22456 errorMsg string 22457 errorType field.ErrorType 22458 }{{ 22459 name: "pod with SELinux Options", 22460 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{SELinuxOptions: &core.SELinuxOptions{Role: "dummy"}}}}}, 22461 expectError: true, 22462 errorMsg: "cannot be set for a windows pod", 22463 errorType: "FieldValueForbidden", 22464 }, { 22465 name: "pod with SeccompProfile", 22466 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{SeccompProfile: &core.SeccompProfile{LocalhostProfile: utilpointer.String("dummy")}}}}}, 22467 expectError: true, 22468 errorMsg: "cannot be set for a windows pod", 22469 errorType: "FieldValueForbidden", 22470 }, { 22471 name: "pod with AppArmorProfile", 22472 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{AppArmorProfile: &core.AppArmorProfile{Type: core.AppArmorProfileTypeRuntimeDefault}}}}}, 22473 expectError: true, 22474 errorMsg: "cannot be set for a windows pod", 22475 errorType: "FieldValueForbidden", 22476 }, { 22477 name: "pod with WindowsOptions, no error", 22478 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{WindowsOptions: &core.WindowsSecurityContextOptions{RunAsUserName: utilpointer.String("dummy")}}}}}, 22479 expectError: false, 22480 }, 22481 } 22482 for _, test := range tests { 22483 t.Run(test.name, func(t *testing.T) { 22484 errs := validateWindows(test.sc, field.NewPath("field")) 22485 if test.expectError && len(errs) > 0 { 22486 if errs[0].Type != test.errorType { 22487 t.Errorf("expected error type %q got %q", test.errorType, errs[0].Type) 22488 } 22489 if errs[0].Detail != test.errorMsg { 22490 t.Errorf("expected error detail %q, got %q", test.errorMsg, errs[0].Detail) 22491 } 22492 } else if test.expectError && len(errs) == 0 { 22493 t.Error("Unexpected success") 22494 } 22495 if !test.expectError && len(errs) != 0 { 22496 t.Errorf("Unexpected error(s): %v", errs) 22497 } 22498 }) 22499 } 22500 } 22501 22502 func TestValidateOSFields(t *testing.T) { 22503 // Contains the list of OS specific fields within pod spec. 22504 // All the fields in pod spec should be either osSpecific or osNeutral field 22505 // To make a field OS specific: 22506 // - Add documentation to the os specific field indicating which os it can/cannot be set for 22507 // - Add documentation to the os field in the api 22508 // - Add validation logic validateLinux, validateWindows functions to make sure the field is only set for eligible OSes 22509 osSpecificFields := sets.NewString( 22510 "Containers[*].SecurityContext.AppArmorProfile", 22511 "Containers[*].SecurityContext.AllowPrivilegeEscalation", 22512 "Containers[*].SecurityContext.Capabilities", 22513 "Containers[*].SecurityContext.Privileged", 22514 "Containers[*].SecurityContext.ProcMount", 22515 "Containers[*].SecurityContext.ReadOnlyRootFilesystem", 22516 "Containers[*].SecurityContext.RunAsGroup", 22517 "Containers[*].SecurityContext.RunAsUser", 22518 "Containers[*].SecurityContext.SELinuxOptions", 22519 "Containers[*].SecurityContext.SeccompProfile", 22520 "Containers[*].SecurityContext.WindowsOptions", 22521 "InitContainers[*].SecurityContext.AppArmorProfile", 22522 "InitContainers[*].SecurityContext.AllowPrivilegeEscalation", 22523 "InitContainers[*].SecurityContext.Capabilities", 22524 "InitContainers[*].SecurityContext.Privileged", 22525 "InitContainers[*].SecurityContext.ProcMount", 22526 "InitContainers[*].SecurityContext.ReadOnlyRootFilesystem", 22527 "InitContainers[*].SecurityContext.RunAsGroup", 22528 "InitContainers[*].SecurityContext.RunAsUser", 22529 "InitContainers[*].SecurityContext.SELinuxOptions", 22530 "InitContainers[*].SecurityContext.SeccompProfile", 22531 "InitContainers[*].SecurityContext.WindowsOptions", 22532 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.AppArmorProfile", 22533 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.AllowPrivilegeEscalation", 22534 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.Capabilities", 22535 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.Privileged", 22536 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.ProcMount", 22537 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.ReadOnlyRootFilesystem", 22538 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.RunAsGroup", 22539 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.RunAsUser", 22540 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.SELinuxOptions", 22541 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.SeccompProfile", 22542 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.WindowsOptions", 22543 "OS", 22544 "SecurityContext.AppArmorProfile", 22545 "SecurityContext.FSGroup", 22546 "SecurityContext.FSGroupChangePolicy", 22547 "SecurityContext.HostIPC", 22548 "SecurityContext.HostNetwork", 22549 "SecurityContext.HostPID", 22550 "SecurityContext.HostUsers", 22551 "SecurityContext.RunAsGroup", 22552 "SecurityContext.RunAsUser", 22553 "SecurityContext.SELinuxOptions", 22554 "SecurityContext.SeccompProfile", 22555 "SecurityContext.ShareProcessNamespace", 22556 "SecurityContext.SupplementalGroups", 22557 "SecurityContext.Sysctls", 22558 "SecurityContext.WindowsOptions", 22559 ) 22560 osNeutralFields := sets.NewString( 22561 "ActiveDeadlineSeconds", 22562 "Affinity", 22563 "AutomountServiceAccountToken", 22564 "Containers[*].Args", 22565 "Containers[*].Command", 22566 "Containers[*].Env", 22567 "Containers[*].EnvFrom", 22568 "Containers[*].Image", 22569 "Containers[*].ImagePullPolicy", 22570 "Containers[*].Lifecycle", 22571 "Containers[*].LivenessProbe", 22572 "Containers[*].Name", 22573 "Containers[*].Ports", 22574 "Containers[*].ReadinessProbe", 22575 "Containers[*].Resources", 22576 "Containers[*].ResizePolicy[*].RestartPolicy", 22577 "Containers[*].ResizePolicy[*].ResourceName", 22578 "Containers[*].RestartPolicy", 22579 "Containers[*].SecurityContext.RunAsNonRoot", 22580 "Containers[*].Stdin", 22581 "Containers[*].StdinOnce", 22582 "Containers[*].StartupProbe", 22583 "Containers[*].VolumeDevices[*]", 22584 "Containers[*].VolumeMounts[*]", 22585 "Containers[*].TTY", 22586 "Containers[*].TerminationMessagePath", 22587 "Containers[*].TerminationMessagePolicy", 22588 "Containers[*].WorkingDir", 22589 "DNSPolicy", 22590 "EnableServiceLinks", 22591 "EphemeralContainers[*].EphemeralContainerCommon.Args", 22592 "EphemeralContainers[*].EphemeralContainerCommon.Command", 22593 "EphemeralContainers[*].EphemeralContainerCommon.Env", 22594 "EphemeralContainers[*].EphemeralContainerCommon.EnvFrom", 22595 "EphemeralContainers[*].EphemeralContainerCommon.Image", 22596 "EphemeralContainers[*].EphemeralContainerCommon.ImagePullPolicy", 22597 "EphemeralContainers[*].EphemeralContainerCommon.Lifecycle", 22598 "EphemeralContainers[*].EphemeralContainerCommon.LivenessProbe", 22599 "EphemeralContainers[*].EphemeralContainerCommon.Name", 22600 "EphemeralContainers[*].EphemeralContainerCommon.Ports", 22601 "EphemeralContainers[*].EphemeralContainerCommon.ReadinessProbe", 22602 "EphemeralContainers[*].EphemeralContainerCommon.Resources", 22603 "EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].RestartPolicy", 22604 "EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].ResourceName", 22605 "EphemeralContainers[*].EphemeralContainerCommon.RestartPolicy", 22606 "EphemeralContainers[*].EphemeralContainerCommon.Stdin", 22607 "EphemeralContainers[*].EphemeralContainerCommon.StdinOnce", 22608 "EphemeralContainers[*].EphemeralContainerCommon.TTY", 22609 "EphemeralContainers[*].EphemeralContainerCommon.TerminationMessagePath", 22610 "EphemeralContainers[*].EphemeralContainerCommon.TerminationMessagePolicy", 22611 "EphemeralContainers[*].EphemeralContainerCommon.WorkingDir", 22612 "EphemeralContainers[*].TargetContainerName", 22613 "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.RunAsNonRoot", 22614 "EphemeralContainers[*].EphemeralContainerCommon.StartupProbe", 22615 "EphemeralContainers[*].EphemeralContainerCommon.VolumeDevices[*]", 22616 "EphemeralContainers[*].EphemeralContainerCommon.VolumeMounts[*]", 22617 "HostAliases", 22618 "Hostname", 22619 "ImagePullSecrets", 22620 "InitContainers[*].Args", 22621 "InitContainers[*].Command", 22622 "InitContainers[*].Env", 22623 "InitContainers[*].EnvFrom", 22624 "InitContainers[*].Image", 22625 "InitContainers[*].ImagePullPolicy", 22626 "InitContainers[*].Lifecycle", 22627 "InitContainers[*].LivenessProbe", 22628 "InitContainers[*].Name", 22629 "InitContainers[*].Ports", 22630 "InitContainers[*].ReadinessProbe", 22631 "InitContainers[*].Resources", 22632 "InitContainers[*].ResizePolicy[*].RestartPolicy", 22633 "InitContainers[*].ResizePolicy[*].ResourceName", 22634 "InitContainers[*].RestartPolicy", 22635 "InitContainers[*].Stdin", 22636 "InitContainers[*].StdinOnce", 22637 "InitContainers[*].TTY", 22638 "InitContainers[*].TerminationMessagePath", 22639 "InitContainers[*].TerminationMessagePolicy", 22640 "InitContainers[*].WorkingDir", 22641 "InitContainers[*].SecurityContext.RunAsNonRoot", 22642 "InitContainers[*].StartupProbe", 22643 "InitContainers[*].VolumeDevices[*]", 22644 "InitContainers[*].VolumeMounts[*]", 22645 "NodeName", 22646 "NodeSelector", 22647 "PreemptionPolicy", 22648 "Priority", 22649 "PriorityClassName", 22650 "ReadinessGates", 22651 "ResourceClaims[*].Name", 22652 "ResourceClaims[*].Source.ResourceClaimName", 22653 "ResourceClaims[*].Source.ResourceClaimTemplateName", 22654 "RestartPolicy", 22655 "RuntimeClassName", 22656 "SchedulerName", 22657 "SchedulingGates[*].Name", 22658 "SecurityContext.RunAsNonRoot", 22659 "ServiceAccountName", 22660 "SetHostnameAsFQDN", 22661 "Subdomain", 22662 "TerminationGracePeriodSeconds", 22663 "Volumes", 22664 "DNSConfig", 22665 "Overhead", 22666 "Tolerations", 22667 "TopologySpreadConstraints", 22668 ) 22669 22670 expect := sets.NewString().Union(osSpecificFields).Union(osNeutralFields) 22671 22672 result := collectResourcePaths(t, expect, reflect.TypeOf(&core.PodSpec{}), nil) 22673 22674 if !expect.Equal(result) { 22675 // expected fields missing from result 22676 missing := expect.Difference(result) 22677 // unexpected fields in result but not specified in expect 22678 unexpected := result.Difference(expect) 22679 if len(missing) > 0 { 22680 t.Errorf("the following fields were expected, but missing from the result. "+ 22681 "If the field has been removed, please remove it from the osNeutralFields set "+ 22682 "or remove it from the osSpecificFields set, as appropriate:\n%s", 22683 strings.Join(missing.List(), "\n")) 22684 } 22685 if len(unexpected) > 0 { 22686 t.Errorf("the following fields were in the result, but unexpected. "+ 22687 "If the field is new, please add it to the osNeutralFields set "+ 22688 "or add it to the osSpecificFields set, as appropriate:\n%s", 22689 strings.Join(unexpected.List(), "\n")) 22690 } 22691 } 22692 } 22693 22694 func TestValidateSchedulingGates(t *testing.T) { 22695 fieldPath := field.NewPath("field") 22696 22697 tests := []struct { 22698 name string 22699 schedulingGates []core.PodSchedulingGate 22700 wantFieldErrors field.ErrorList 22701 }{{ 22702 name: "nil gates", 22703 schedulingGates: nil, 22704 wantFieldErrors: field.ErrorList{}, 22705 }, { 22706 name: "empty string in gates", 22707 schedulingGates: []core.PodSchedulingGate{ 22708 {Name: "foo"}, 22709 {Name: ""}, 22710 }, 22711 wantFieldErrors: field.ErrorList{ 22712 field.Invalid(fieldPath.Index(1), "", "name part must be non-empty"), 22713 field.Invalid(fieldPath.Index(1), "", "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"), 22714 }, 22715 }, { 22716 name: "legal gates", 22717 schedulingGates: []core.PodSchedulingGate{ 22718 {Name: "foo"}, 22719 {Name: "bar"}, 22720 }, 22721 wantFieldErrors: field.ErrorList{}, 22722 }, { 22723 name: "illegal gates", 22724 schedulingGates: []core.PodSchedulingGate{ 22725 {Name: "foo"}, 22726 {Name: "\nbar"}, 22727 }, 22728 wantFieldErrors: []*field.Error{field.Invalid(fieldPath.Index(1), "\nbar", "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")}, 22729 }, { 22730 name: "duplicated gates (single duplication)", 22731 schedulingGates: []core.PodSchedulingGate{ 22732 {Name: "foo"}, 22733 {Name: "bar"}, 22734 {Name: "bar"}, 22735 }, 22736 wantFieldErrors: []*field.Error{field.Duplicate(fieldPath.Index(2), "bar")}, 22737 }, { 22738 name: "duplicated gates (multiple duplications)", 22739 schedulingGates: []core.PodSchedulingGate{ 22740 {Name: "foo"}, 22741 {Name: "bar"}, 22742 {Name: "foo"}, 22743 {Name: "baz"}, 22744 {Name: "foo"}, 22745 {Name: "bar"}, 22746 }, 22747 wantFieldErrors: field.ErrorList{ 22748 field.Duplicate(fieldPath.Index(2), "foo"), 22749 field.Duplicate(fieldPath.Index(4), "foo"), 22750 field.Duplicate(fieldPath.Index(5), "bar"), 22751 }, 22752 }, 22753 } 22754 for _, tt := range tests { 22755 t.Run(tt.name, func(t *testing.T) { 22756 errs := validateSchedulingGates(tt.schedulingGates, fieldPath) 22757 if diff := cmp.Diff(tt.wantFieldErrors, errs); diff != "" { 22758 t.Errorf("unexpected field errors (-want, +got):\n%s", diff) 22759 } 22760 }) 22761 } 22762 } 22763 22764 // collectResourcePaths traverses the object, computing all the struct paths. 22765 func collectResourcePaths(t *testing.T, skipRecurseList sets.String, tp reflect.Type, path *field.Path) sets.String { 22766 if pathStr := path.String(); len(pathStr) > 0 && skipRecurseList.Has(pathStr) { 22767 return sets.NewString(pathStr) 22768 } 22769 22770 paths := sets.NewString() 22771 switch tp.Kind() { 22772 case reflect.Pointer: 22773 paths.Insert(collectResourcePaths(t, skipRecurseList, tp.Elem(), path).List()...) 22774 case reflect.Struct: 22775 for i := 0; i < tp.NumField(); i++ { 22776 field := tp.Field(i) 22777 paths.Insert(collectResourcePaths(t, skipRecurseList, field.Type, path.Child(field.Name)).List()...) 22778 } 22779 case reflect.Map, reflect.Slice: 22780 paths.Insert(collectResourcePaths(t, skipRecurseList, tp.Elem(), path.Key("*")).List()...) 22781 case reflect.Interface: 22782 t.Fatalf("unexpected interface{} field %s", path.String()) 22783 default: 22784 // if we hit a primitive type, we're at a leaf 22785 paths.Insert(path.String()) 22786 } 22787 return paths 22788 } 22789 22790 func TestValidateTLSSecret(t *testing.T) { 22791 successCases := map[string]core.Secret{ 22792 "empty certificate chain": { 22793 ObjectMeta: metav1.ObjectMeta{Name: "tls-cert", Namespace: "namespace"}, 22794 Data: map[string][]byte{ 22795 core.TLSCertKey: []byte("public key"), 22796 core.TLSPrivateKeyKey: []byte("private key"), 22797 }, 22798 }, 22799 } 22800 for k, v := range successCases { 22801 if errs := ValidateSecret(&v); len(errs) != 0 { 22802 t.Errorf("Expected success for %s, got %v", k, errs) 22803 } 22804 } 22805 errorCases := map[string]struct { 22806 secrets core.Secret 22807 errorType field.ErrorType 22808 errorDetail string 22809 }{ 22810 "missing public key": { 22811 secrets: core.Secret{ 22812 ObjectMeta: metav1.ObjectMeta{Name: "tls-cert"}, 22813 Data: map[string][]byte{ 22814 core.TLSCertKey: []byte("public key"), 22815 }, 22816 }, 22817 errorType: "FieldValueRequired", 22818 }, 22819 "missing private key": { 22820 secrets: core.Secret{ 22821 ObjectMeta: metav1.ObjectMeta{Name: "tls-cert"}, 22822 Data: map[string][]byte{ 22823 core.TLSCertKey: []byte("public key"), 22824 }, 22825 }, 22826 errorType: "FieldValueRequired", 22827 }, 22828 } 22829 for k, v := range errorCases { 22830 if errs := ValidateSecret(&v.secrets); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 22831 t.Errorf("[%s] Expected error type %s with detail %q, got %v", k, v.errorType, v.errorDetail, errs) 22832 } 22833 } 22834 } 22835 22836 func TestValidateLinuxSecurityContext(t *testing.T) { 22837 runAsUser := int64(1) 22838 validLinuxSC := &core.SecurityContext{ 22839 Privileged: utilpointer.Bool(false), 22840 Capabilities: &core.Capabilities{ 22841 Add: []core.Capability{"foo"}, 22842 Drop: []core.Capability{"bar"}, 22843 }, 22844 SELinuxOptions: &core.SELinuxOptions{ 22845 User: "user", 22846 Role: "role", 22847 Type: "type", 22848 Level: "level", 22849 }, 22850 RunAsUser: &runAsUser, 22851 } 22852 invalidLinuxSC := &core.SecurityContext{ 22853 WindowsOptions: &core.WindowsSecurityContextOptions{RunAsUserName: utilpointer.String("myUser")}, 22854 } 22855 cases := map[string]struct { 22856 sc *core.PodSpec 22857 expectErr bool 22858 errorType field.ErrorType 22859 errorDetail string 22860 }{ 22861 "valid SC, linux, no error": { 22862 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: validLinuxSC}}}, 22863 expectErr: false, 22864 }, 22865 "invalid SC, linux, error": { 22866 sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: invalidLinuxSC}}}, 22867 errorType: "FieldValueForbidden", 22868 errorDetail: "windows options cannot be set for a linux pod", 22869 expectErr: true, 22870 }, 22871 } 22872 for k, v := range cases { 22873 t.Run(k, func(t *testing.T) { 22874 errs := validateLinux(v.sc, field.NewPath("field")) 22875 if v.expectErr && len(errs) > 0 { 22876 if errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 22877 t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs) 22878 } 22879 } else if v.expectErr && len(errs) == 0 { 22880 t.Errorf("Unexpected success") 22881 } 22882 if !v.expectErr && len(errs) != 0 { 22883 t.Errorf("Unexpected error(s): %v", errs) 22884 } 22885 }) 22886 } 22887 } 22888 22889 func TestValidateSecurityContext(t *testing.T) { 22890 runAsUser := int64(1) 22891 fullValidSC := func() *core.SecurityContext { 22892 return &core.SecurityContext{ 22893 Privileged: utilpointer.Bool(false), 22894 Capabilities: &core.Capabilities{ 22895 Add: []core.Capability{"foo"}, 22896 Drop: []core.Capability{"bar"}, 22897 }, 22898 SELinuxOptions: &core.SELinuxOptions{ 22899 User: "user", 22900 Role: "role", 22901 Type: "type", 22902 Level: "level", 22903 }, 22904 RunAsUser: &runAsUser, 22905 } 22906 } 22907 22908 // setup data 22909 allSettings := fullValidSC() 22910 noCaps := fullValidSC() 22911 noCaps.Capabilities = nil 22912 22913 noSELinux := fullValidSC() 22914 noSELinux.SELinuxOptions = nil 22915 22916 noPrivRequest := fullValidSC() 22917 noPrivRequest.Privileged = nil 22918 22919 noRunAsUser := fullValidSC() 22920 noRunAsUser.RunAsUser = nil 22921 22922 procMountSet := fullValidSC() 22923 defPmt := core.DefaultProcMount 22924 procMountSet.ProcMount = &defPmt 22925 22926 umPmt := core.UnmaskedProcMount 22927 procMountUnmasked := fullValidSC() 22928 procMountUnmasked.ProcMount = &umPmt 22929 22930 successCases := map[string]struct { 22931 sc *core.SecurityContext 22932 hostUsers bool 22933 }{ 22934 "all settings": {allSettings, false}, 22935 "no capabilities": {noCaps, false}, 22936 "no selinux": {noSELinux, false}, 22937 "no priv request": {noPrivRequest, false}, 22938 "no run as user": {noRunAsUser, false}, 22939 "proc mount set": {procMountSet, true}, 22940 "proc mount unmasked": {procMountUnmasked, false}, 22941 } 22942 for k, v := range successCases { 22943 if errs := ValidateSecurityContext(v.sc, field.NewPath("field"), v.hostUsers); len(errs) != 0 { 22944 t.Errorf("[%s] Expected success, got %v", k, errs) 22945 } 22946 } 22947 22948 privRequestWithGlobalDeny := fullValidSC() 22949 privRequestWithGlobalDeny.Privileged = utilpointer.Bool(true) 22950 22951 negativeRunAsUser := fullValidSC() 22952 negativeUser := int64(-1) 22953 negativeRunAsUser.RunAsUser = &negativeUser 22954 22955 privWithoutEscalation := fullValidSC() 22956 privWithoutEscalation.Privileged = utilpointer.Bool(true) 22957 privWithoutEscalation.AllowPrivilegeEscalation = utilpointer.Bool(false) 22958 22959 capSysAdminWithoutEscalation := fullValidSC() 22960 capSysAdminWithoutEscalation.Capabilities.Add = []core.Capability{"CAP_SYS_ADMIN"} 22961 capSysAdminWithoutEscalation.AllowPrivilegeEscalation = utilpointer.Bool(false) 22962 22963 errorCases := map[string]struct { 22964 sc *core.SecurityContext 22965 errorType field.ErrorType 22966 errorDetail string 22967 capAllowPriv bool 22968 }{ 22969 "request privileged when capabilities forbids": { 22970 sc: privRequestWithGlobalDeny, 22971 errorType: "FieldValueForbidden", 22972 errorDetail: "disallowed by cluster policy", 22973 }, 22974 "negative RunAsUser": { 22975 sc: negativeRunAsUser, 22976 errorType: "FieldValueInvalid", 22977 errorDetail: "must be between", 22978 }, 22979 "with CAP_SYS_ADMIN and allowPrivilegeEscalation false": { 22980 sc: capSysAdminWithoutEscalation, 22981 errorType: "FieldValueInvalid", 22982 errorDetail: "cannot set `allowPrivilegeEscalation` to false and `capabilities.Add` CAP_SYS_ADMIN", 22983 }, 22984 "with privileged and allowPrivilegeEscalation false": { 22985 sc: privWithoutEscalation, 22986 errorType: "FieldValueInvalid", 22987 errorDetail: "cannot set `allowPrivilegeEscalation` to false and `privileged` to true", 22988 capAllowPriv: true, 22989 }, 22990 "with unmasked proc mount type and no user namespace": { 22991 sc: procMountUnmasked, 22992 errorType: "FieldValueInvalid", 22993 errorDetail: "`hostUsers` must be false to use `Unmasked`", 22994 }, 22995 } 22996 for k, v := range errorCases { 22997 capabilities.SetForTests(capabilities.Capabilities{ 22998 AllowPrivileged: v.capAllowPriv, 22999 }) 23000 // note the unconditional `true` here for hostUsers. The failure case to test for ProcMount only includes it being true, 23001 // and the field is ignored if ProcMount isn't set. Thus, we can unconditionally set to `true` and simplify the test matrix setup. 23002 if errs := ValidateSecurityContext(v.sc, field.NewPath("field"), true); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) { 23003 t.Errorf("[%s] Expected error type %q with detail %q, got %v", k, v.errorType, v.errorDetail, errs) 23004 } 23005 } 23006 } 23007 23008 func fakeValidSecurityContext(priv bool) *core.SecurityContext { 23009 return &core.SecurityContext{ 23010 Privileged: &priv, 23011 } 23012 } 23013 23014 func TestValidPodLogOptions(t *testing.T) { 23015 now := metav1.Now() 23016 negative := int64(-1) 23017 zero := int64(0) 23018 positive := int64(1) 23019 tests := []struct { 23020 opt core.PodLogOptions 23021 errs int 23022 }{ 23023 {core.PodLogOptions{}, 0}, 23024 {core.PodLogOptions{Previous: true}, 0}, 23025 {core.PodLogOptions{Follow: true}, 0}, 23026 {core.PodLogOptions{TailLines: &zero}, 0}, 23027 {core.PodLogOptions{TailLines: &negative}, 1}, 23028 {core.PodLogOptions{TailLines: &positive}, 0}, 23029 {core.PodLogOptions{LimitBytes: &zero}, 1}, 23030 {core.PodLogOptions{LimitBytes: &negative}, 1}, 23031 {core.PodLogOptions{LimitBytes: &positive}, 0}, 23032 {core.PodLogOptions{SinceSeconds: &negative}, 1}, 23033 {core.PodLogOptions{SinceSeconds: &positive}, 0}, 23034 {core.PodLogOptions{SinceSeconds: &zero}, 1}, 23035 {core.PodLogOptions{SinceTime: &now}, 0}, 23036 } 23037 for i, test := range tests { 23038 errs := ValidatePodLogOptions(&test.opt) 23039 if test.errs != len(errs) { 23040 t.Errorf("%d: Unexpected errors: %v", i, errs) 23041 } 23042 } 23043 } 23044 23045 func TestValidateConfigMap(t *testing.T) { 23046 newConfigMap := func(name, namespace string, data map[string]string, binaryData map[string][]byte) core.ConfigMap { 23047 return core.ConfigMap{ 23048 ObjectMeta: metav1.ObjectMeta{ 23049 Name: name, 23050 Namespace: namespace, 23051 }, 23052 Data: data, 23053 BinaryData: binaryData, 23054 } 23055 } 23056 23057 var ( 23058 validConfigMap = newConfigMap("validname", "validns", map[string]string{"key": "value"}, map[string][]byte{"bin": []byte("value")}) 23059 maxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 253): "value"}, nil) 23060 23061 emptyName = newConfigMap("", "validns", nil, nil) 23062 invalidName = newConfigMap("NoUppercaseOrSpecialCharsLike=Equals", "validns", nil, nil) 23063 emptyNs = newConfigMap("validname", "", nil, nil) 23064 invalidNs = newConfigMap("validname", "NoUppercaseOrSpecialCharsLike=Equals", nil, nil) 23065 invalidKey = newConfigMap("validname", "validns", map[string]string{"a*b": "value"}, nil) 23066 leadingDotKey = newConfigMap("validname", "validns", map[string]string{".ab": "value"}, nil) 23067 dotKey = newConfigMap("validname", "validns", map[string]string{".": "value"}, nil) 23068 doubleDotKey = newConfigMap("validname", "validns", map[string]string{"..": "value"}, nil) 23069 overMaxKeyLength = newConfigMap("validname", "validns", map[string]string{strings.Repeat("a", 254): "value"}, nil) 23070 overMaxSize = newConfigMap("validname", "validns", map[string]string{"key": strings.Repeat("a", v1.MaxSecretSize+1)}, nil) 23071 duplicatedKey = newConfigMap("validname", "validns", map[string]string{"key": "value1"}, map[string][]byte{"key": []byte("value2")}) 23072 binDataInvalidKey = newConfigMap("validname", "validns", nil, map[string][]byte{"a*b": []byte("value")}) 23073 binDataLeadingDotKey = newConfigMap("validname", "validns", nil, map[string][]byte{".ab": []byte("value")}) 23074 binDataDotKey = newConfigMap("validname", "validns", nil, map[string][]byte{".": []byte("value")}) 23075 binDataDoubleDotKey = newConfigMap("validname", "validns", nil, map[string][]byte{"..": []byte("value")}) 23076 binDataOverMaxKeyLength = newConfigMap("validname", "validns", nil, map[string][]byte{strings.Repeat("a", 254): []byte("value")}) 23077 binDataOverMaxSize = newConfigMap("validname", "validns", nil, map[string][]byte{"bin": bytes.Repeat([]byte("a"), v1.MaxSecretSize+1)}) 23078 binNonUtf8Value = newConfigMap("validname", "validns", nil, map[string][]byte{"key": {0, 0xFE, 0, 0xFF}}) 23079 ) 23080 23081 tests := map[string]struct { 23082 cfg core.ConfigMap 23083 isValid bool 23084 }{ 23085 "valid": {validConfigMap, true}, 23086 "max key length": {maxKeyLength, true}, 23087 "leading dot key": {leadingDotKey, true}, 23088 "empty name": {emptyName, false}, 23089 "invalid name": {invalidName, false}, 23090 "invalid key": {invalidKey, false}, 23091 "empty namespace": {emptyNs, false}, 23092 "invalid namespace": {invalidNs, false}, 23093 "dot key": {dotKey, false}, 23094 "double dot key": {doubleDotKey, false}, 23095 "over max key length": {overMaxKeyLength, false}, 23096 "over max size": {overMaxSize, false}, 23097 "duplicated key": {duplicatedKey, false}, 23098 "binary data invalid key": {binDataInvalidKey, false}, 23099 "binary data leading dot key": {binDataLeadingDotKey, true}, 23100 "binary data dot key": {binDataDotKey, false}, 23101 "binary data double dot key": {binDataDoubleDotKey, false}, 23102 "binary data over max key length": {binDataOverMaxKeyLength, false}, 23103 "binary data max size": {binDataOverMaxSize, false}, 23104 "binary data non utf-8 bytes": {binNonUtf8Value, true}, 23105 } 23106 23107 for name, tc := range tests { 23108 errs := ValidateConfigMap(&tc.cfg) 23109 if tc.isValid && len(errs) > 0 { 23110 t.Errorf("%v: unexpected error: %v", name, errs) 23111 } 23112 if !tc.isValid && len(errs) == 0 { 23113 t.Errorf("%v: unexpected non-error", name) 23114 } 23115 } 23116 } 23117 23118 func TestValidateConfigMapUpdate(t *testing.T) { 23119 newConfigMap := func(version, name, namespace string, data map[string]string) core.ConfigMap { 23120 return core.ConfigMap{ 23121 ObjectMeta: metav1.ObjectMeta{ 23122 Name: name, 23123 Namespace: namespace, 23124 ResourceVersion: version, 23125 }, 23126 Data: data, 23127 } 23128 } 23129 validConfigMap := func() core.ConfigMap { 23130 return newConfigMap("1", "validname", "validdns", map[string]string{"key": "value"}) 23131 } 23132 23133 falseVal := false 23134 trueVal := true 23135 23136 configMap := validConfigMap() 23137 immutableConfigMap := validConfigMap() 23138 immutableConfigMap.Immutable = &trueVal 23139 mutableConfigMap := validConfigMap() 23140 mutableConfigMap.Immutable = &falseVal 23141 23142 configMapWithData := validConfigMap() 23143 configMapWithData.Data["key-2"] = "value-2" 23144 immutableConfigMapWithData := validConfigMap() 23145 immutableConfigMapWithData.Immutable = &trueVal 23146 immutableConfigMapWithData.Data["key-2"] = "value-2" 23147 23148 configMapWithChangedData := validConfigMap() 23149 configMapWithChangedData.Data["key"] = "foo" 23150 immutableConfigMapWithChangedData := validConfigMap() 23151 immutableConfigMapWithChangedData.Immutable = &trueVal 23152 immutableConfigMapWithChangedData.Data["key"] = "foo" 23153 23154 noVersion := newConfigMap("", "validname", "validns", map[string]string{"key": "value"}) 23155 23156 cases := []struct { 23157 name string 23158 newCfg core.ConfigMap 23159 oldCfg core.ConfigMap 23160 valid bool 23161 }{{ 23162 name: "valid", 23163 newCfg: configMap, 23164 oldCfg: configMap, 23165 valid: true, 23166 }, { 23167 name: "invalid", 23168 newCfg: noVersion, 23169 oldCfg: configMap, 23170 valid: false, 23171 }, { 23172 name: "mark configmap immutable", 23173 oldCfg: configMap, 23174 newCfg: immutableConfigMap, 23175 valid: true, 23176 }, { 23177 name: "revert immutable configmap", 23178 oldCfg: immutableConfigMap, 23179 newCfg: configMap, 23180 valid: false, 23181 }, { 23182 name: "mark immutable configmap mutable", 23183 oldCfg: immutableConfigMap, 23184 newCfg: mutableConfigMap, 23185 valid: false, 23186 }, { 23187 name: "add data in configmap", 23188 oldCfg: configMap, 23189 newCfg: configMapWithData, 23190 valid: true, 23191 }, { 23192 name: "add data in immutable configmap", 23193 oldCfg: immutableConfigMap, 23194 newCfg: immutableConfigMapWithData, 23195 valid: false, 23196 }, { 23197 name: "change data in configmap", 23198 oldCfg: configMap, 23199 newCfg: configMapWithChangedData, 23200 valid: true, 23201 }, { 23202 name: "change data in immutable configmap", 23203 oldCfg: immutableConfigMap, 23204 newCfg: immutableConfigMapWithChangedData, 23205 valid: false, 23206 }, 23207 } 23208 23209 for _, tc := range cases { 23210 t.Run(tc.name, func(t *testing.T) { 23211 errs := ValidateConfigMapUpdate(&tc.newCfg, &tc.oldCfg) 23212 if tc.valid && len(errs) > 0 { 23213 t.Errorf("Unexpected error: %v", errs) 23214 } 23215 if !tc.valid && len(errs) == 0 { 23216 t.Errorf("Unexpected lack of error") 23217 } 23218 }) 23219 } 23220 } 23221 23222 func TestValidateHasLabel(t *testing.T) { 23223 successCase := metav1.ObjectMeta{ 23224 Name: "123", 23225 Namespace: "ns", 23226 Labels: map[string]string{ 23227 "other": "blah", 23228 "foo": "bar", 23229 }, 23230 } 23231 if errs := ValidateHasLabel(successCase, field.NewPath("field"), "foo", "bar"); len(errs) != 0 { 23232 t.Errorf("expected success: %v", errs) 23233 } 23234 23235 missingCase := metav1.ObjectMeta{ 23236 Name: "123", 23237 Namespace: "ns", 23238 Labels: map[string]string{ 23239 "other": "blah", 23240 }, 23241 } 23242 if errs := ValidateHasLabel(missingCase, field.NewPath("field"), "foo", "bar"); len(errs) == 0 { 23243 t.Errorf("expected failure") 23244 } 23245 23246 wrongValueCase := metav1.ObjectMeta{ 23247 Name: "123", 23248 Namespace: "ns", 23249 Labels: map[string]string{ 23250 "other": "blah", 23251 "foo": "notbar", 23252 }, 23253 } 23254 if errs := ValidateHasLabel(wrongValueCase, field.NewPath("field"), "foo", "bar"); len(errs) == 0 { 23255 t.Errorf("expected failure") 23256 } 23257 } 23258 23259 func TestIsValidSysctlName(t *testing.T) { 23260 valid := []string{ 23261 "a.b.c.d", 23262 "a", 23263 "a_b", 23264 "a-b", 23265 "abc", 23266 "abc.def", 23267 "a/b/c/d", 23268 "a/b.c", 23269 } 23270 invalid := []string{ 23271 "", 23272 "*", 23273 "ä", 23274 "a_", 23275 "_", 23276 "__", 23277 "_a", 23278 "_a._b", 23279 "-", 23280 ".", 23281 "a.", 23282 ".a", 23283 "a.b.", 23284 "a*.b", 23285 "a*b", 23286 "*a", 23287 "a.*", 23288 "*", 23289 "abc*", 23290 "a.abc*", 23291 "a.b.*", 23292 "Abc", 23293 "/", 23294 "/a", 23295 "a/abc*", 23296 "a/b/*", 23297 func(n int) string { 23298 x := make([]byte, n) 23299 for i := range x { 23300 x[i] = byte('a') 23301 } 23302 return string(x) 23303 }(256), 23304 } 23305 23306 for _, s := range valid { 23307 if !IsValidSysctlName(s) { 23308 t.Errorf("%q expected to be a valid sysctl name", s) 23309 } 23310 } 23311 for _, s := range invalid { 23312 if IsValidSysctlName(s) { 23313 t.Errorf("%q expected to be an invalid sysctl name", s) 23314 } 23315 } 23316 } 23317 23318 func TestValidateSysctls(t *testing.T) { 23319 valid := []string{ 23320 "net.foo.bar", 23321 "kernel.shmmax", 23322 "net.ipv4.conf.enp3s0/200.forwarding", 23323 "net/ipv4/conf/enp3s0.200/forwarding", 23324 } 23325 invalid := []string{ 23326 "i..nvalid", 23327 "_invalid", 23328 } 23329 23330 invalidWithHostNet := []string{ 23331 "net.ipv4.conf.enp3s0/200.forwarding", 23332 "net/ipv4/conf/enp3s0.200/forwarding", 23333 } 23334 23335 invalidWithHostIPC := []string{ 23336 "kernel.shmmax", 23337 "kernel.msgmax", 23338 } 23339 23340 duplicates := []string{ 23341 "kernel.shmmax", 23342 "kernel.shmmax", 23343 } 23344 opts := PodValidationOptions{ 23345 AllowNamespacedSysctlsForHostNetAndHostIPC: false, 23346 } 23347 23348 sysctls := make([]core.Sysctl, len(valid)) 23349 validSecurityContext := &core.PodSecurityContext{ 23350 Sysctls: sysctls, 23351 } 23352 for i, sysctl := range valid { 23353 sysctls[i].Name = sysctl 23354 } 23355 errs := validateSysctls(validSecurityContext, field.NewPath("foo"), opts) 23356 if len(errs) != 0 { 23357 t.Errorf("unexpected validation errors: %v", errs) 23358 } 23359 23360 sysctls = make([]core.Sysctl, len(invalid)) 23361 for i, sysctl := range invalid { 23362 sysctls[i].Name = sysctl 23363 } 23364 inValidSecurityContext := &core.PodSecurityContext{ 23365 Sysctls: sysctls, 23366 } 23367 errs = validateSysctls(inValidSecurityContext, field.NewPath("foo"), opts) 23368 if len(errs) != 2 { 23369 t.Errorf("expected 2 validation errors. Got: %v", errs) 23370 } else { 23371 if got, expected := errs[0].Error(), "foo"; !strings.Contains(got, expected) { 23372 t.Errorf("unexpected errors: expected=%q, got=%q", expected, got) 23373 } 23374 if got, expected := errs[1].Error(), "foo"; !strings.Contains(got, expected) { 23375 t.Errorf("unexpected errors: expected=%q, got=%q", expected, got) 23376 } 23377 } 23378 23379 sysctls = make([]core.Sysctl, len(duplicates)) 23380 for i, sysctl := range duplicates { 23381 sysctls[i].Name = sysctl 23382 } 23383 securityContextWithDup := &core.PodSecurityContext{ 23384 Sysctls: sysctls, 23385 } 23386 errs = validateSysctls(securityContextWithDup, field.NewPath("foo"), opts) 23387 if len(errs) != 1 { 23388 t.Errorf("unexpected validation errors: %v", errs) 23389 } else if errs[0].Type != field.ErrorTypeDuplicate { 23390 t.Errorf("expected error type %v, got %v", field.ErrorTypeDuplicate, errs[0].Type) 23391 } 23392 23393 sysctls = make([]core.Sysctl, len(invalidWithHostNet)) 23394 for i, sysctl := range invalidWithHostNet { 23395 sysctls[i].Name = sysctl 23396 } 23397 invalidSecurityContextWithHostNet := &core.PodSecurityContext{ 23398 Sysctls: sysctls, 23399 HostIPC: false, 23400 HostNetwork: true, 23401 } 23402 errs = validateSysctls(invalidSecurityContextWithHostNet, field.NewPath("foo"), opts) 23403 if len(errs) != 2 { 23404 t.Errorf("unexpected validation errors: %v", errs) 23405 } 23406 opts.AllowNamespacedSysctlsForHostNetAndHostIPC = true 23407 errs = validateSysctls(invalidSecurityContextWithHostNet, field.NewPath("foo"), opts) 23408 if len(errs) != 0 { 23409 t.Errorf("unexpected validation errors: %v", errs) 23410 } 23411 23412 sysctls = make([]core.Sysctl, len(invalidWithHostIPC)) 23413 for i, sysctl := range invalidWithHostIPC { 23414 sysctls[i].Name = sysctl 23415 } 23416 invalidSecurityContextWithHostIPC := &core.PodSecurityContext{ 23417 Sysctls: sysctls, 23418 HostIPC: true, 23419 HostNetwork: false, 23420 } 23421 opts.AllowNamespacedSysctlsForHostNetAndHostIPC = false 23422 errs = validateSysctls(invalidSecurityContextWithHostIPC, field.NewPath("foo"), opts) 23423 if len(errs) != 2 { 23424 t.Errorf("unexpected validation errors: %v", errs) 23425 } 23426 opts.AllowNamespacedSysctlsForHostNetAndHostIPC = true 23427 errs = validateSysctls(invalidSecurityContextWithHostIPC, field.NewPath("foo"), opts) 23428 if len(errs) != 0 { 23429 t.Errorf("unexpected validation errors: %v", errs) 23430 } 23431 } 23432 23433 func newNodeNameEndpoint(nodeName string) *core.Endpoints { 23434 ep := &core.Endpoints{ 23435 ObjectMeta: metav1.ObjectMeta{ 23436 Name: "foo", 23437 Namespace: metav1.NamespaceDefault, 23438 ResourceVersion: "1", 23439 }, 23440 Subsets: []core.EndpointSubset{{ 23441 NotReadyAddresses: []core.EndpointAddress{}, 23442 Ports: []core.EndpointPort{{Name: "https", Port: 443, Protocol: "TCP"}}, 23443 Addresses: []core.EndpointAddress{{ 23444 IP: "8.8.8.8", 23445 Hostname: "zookeeper1", 23446 NodeName: &nodeName}}}}} 23447 return ep 23448 } 23449 23450 func TestEndpointAddressNodeNameUpdateRestrictions(t *testing.T) { 23451 oldEndpoint := newNodeNameEndpoint("kubernetes-node-setup-by-backend") 23452 updatedEndpoint := newNodeNameEndpoint("kubernetes-changed-nodename") 23453 // Check that NodeName can be changed during update, this is to accommodate the case where nodeIP or PodCIDR is reused. 23454 // The same ip will now have a different nodeName. 23455 errList := ValidateEndpoints(updatedEndpoint) 23456 errList = append(errList, ValidateEndpointsUpdate(updatedEndpoint, oldEndpoint)...) 23457 if len(errList) != 0 { 23458 t.Error("Endpoint should allow changing of Subset.Addresses.NodeName on update") 23459 } 23460 } 23461 23462 func TestEndpointAddressNodeNameInvalidDNSSubdomain(t *testing.T) { 23463 // Check NodeName DNS validation 23464 endpoint := newNodeNameEndpoint("illegal*.nodename") 23465 errList := ValidateEndpoints(endpoint) 23466 if len(errList) == 0 { 23467 t.Error("Endpoint should reject invalid NodeName") 23468 } 23469 } 23470 23471 func TestEndpointAddressNodeNameCanBeAnIPAddress(t *testing.T) { 23472 endpoint := newNodeNameEndpoint("10.10.1.1") 23473 errList := ValidateEndpoints(endpoint) 23474 if len(errList) != 0 { 23475 t.Error("Endpoint should accept a NodeName that is an IP address") 23476 } 23477 } 23478 23479 func TestValidateFlexVolumeSource(t *testing.T) { 23480 testcases := map[string]struct { 23481 source *core.FlexVolumeSource 23482 expectedErrs map[string]string 23483 }{ 23484 "valid": { 23485 source: &core.FlexVolumeSource{Driver: "foo"}, 23486 expectedErrs: map[string]string{}, 23487 }, 23488 "valid with options": { 23489 source: &core.FlexVolumeSource{Driver: "foo", Options: map[string]string{"foo": "bar"}}, 23490 expectedErrs: map[string]string{}, 23491 }, 23492 "no driver": { 23493 source: &core.FlexVolumeSource{Driver: ""}, 23494 expectedErrs: map[string]string{"driver": "Required value"}, 23495 }, 23496 "reserved option keys": { 23497 source: &core.FlexVolumeSource{ 23498 Driver: "foo", 23499 Options: map[string]string{ 23500 // valid options 23501 "myns.io": "A", 23502 "myns.io/bar": "A", 23503 "myns.io/kubernetes.io": "A", 23504 23505 // invalid options 23506 "KUBERNETES.IO": "A", 23507 "kubernetes.io": "A", 23508 "kubernetes.io/": "A", 23509 "kubernetes.io/foo": "A", 23510 23511 "alpha.kubernetes.io": "A", 23512 "alpha.kubernetes.io/": "A", 23513 "alpha.kubernetes.io/foo": "A", 23514 23515 "k8s.io": "A", 23516 "k8s.io/": "A", 23517 "k8s.io/foo": "A", 23518 23519 "alpha.k8s.io": "A", 23520 "alpha.k8s.io/": "A", 23521 "alpha.k8s.io/foo": "A", 23522 }, 23523 }, 23524 expectedErrs: map[string]string{ 23525 "options[KUBERNETES.IO]": "reserved", 23526 "options[kubernetes.io]": "reserved", 23527 "options[kubernetes.io/]": "reserved", 23528 "options[kubernetes.io/foo]": "reserved", 23529 "options[alpha.kubernetes.io]": "reserved", 23530 "options[alpha.kubernetes.io/]": "reserved", 23531 "options[alpha.kubernetes.io/foo]": "reserved", 23532 "options[k8s.io]": "reserved", 23533 "options[k8s.io/]": "reserved", 23534 "options[k8s.io/foo]": "reserved", 23535 "options[alpha.k8s.io]": "reserved", 23536 "options[alpha.k8s.io/]": "reserved", 23537 "options[alpha.k8s.io/foo]": "reserved", 23538 }, 23539 }, 23540 } 23541 23542 for k, tc := range testcases { 23543 errs := validateFlexVolumeSource(tc.source, nil) 23544 for _, err := range errs { 23545 expectedErr, ok := tc.expectedErrs[err.Field] 23546 if !ok { 23547 t.Errorf("%s: unexpected err on field %s: %v", k, err.Field, err) 23548 continue 23549 } 23550 if !strings.Contains(err.Error(), expectedErr) { 23551 t.Errorf("%s: expected err on field %s to contain '%s', was %v", k, err.Field, expectedErr, err.Error()) 23552 continue 23553 } 23554 } 23555 if len(errs) != len(tc.expectedErrs) { 23556 t.Errorf("%s: expected errs %#v, got %#v", k, tc.expectedErrs, errs) 23557 continue 23558 } 23559 } 23560 } 23561 23562 func TestValidateOrSetClientIPAffinityConfig(t *testing.T) { 23563 successCases := map[string]*core.SessionAffinityConfig{ 23564 "non-empty config, valid timeout: 1": { 23565 ClientIP: &core.ClientIPConfig{ 23566 TimeoutSeconds: utilpointer.Int32(1), 23567 }, 23568 }, 23569 "non-empty config, valid timeout: core.MaxClientIPServiceAffinitySeconds-1": { 23570 ClientIP: &core.ClientIPConfig{ 23571 TimeoutSeconds: utilpointer.Int32(core.MaxClientIPServiceAffinitySeconds - 1), 23572 }, 23573 }, 23574 "non-empty config, valid timeout: core.MaxClientIPServiceAffinitySeconds": { 23575 ClientIP: &core.ClientIPConfig{ 23576 TimeoutSeconds: utilpointer.Int32(core.MaxClientIPServiceAffinitySeconds), 23577 }, 23578 }, 23579 } 23580 23581 for name, test := range successCases { 23582 if errs := validateClientIPAffinityConfig(test, field.NewPath("field")); len(errs) != 0 { 23583 t.Errorf("case: %s, expected success: %v", name, errs) 23584 } 23585 } 23586 23587 errorCases := map[string]*core.SessionAffinityConfig{ 23588 "empty session affinity config": nil, 23589 "empty client IP config": { 23590 ClientIP: nil, 23591 }, 23592 "empty timeoutSeconds": { 23593 ClientIP: &core.ClientIPConfig{ 23594 TimeoutSeconds: nil, 23595 }, 23596 }, 23597 "non-empty config, invalid timeout: core.MaxClientIPServiceAffinitySeconds+1": { 23598 ClientIP: &core.ClientIPConfig{ 23599 TimeoutSeconds: utilpointer.Int32(core.MaxClientIPServiceAffinitySeconds + 1), 23600 }, 23601 }, 23602 "non-empty config, invalid timeout: -1": { 23603 ClientIP: &core.ClientIPConfig{ 23604 TimeoutSeconds: utilpointer.Int32(-1), 23605 }, 23606 }, 23607 "non-empty config, invalid timeout: 0": { 23608 ClientIP: &core.ClientIPConfig{ 23609 TimeoutSeconds: utilpointer.Int32(0), 23610 }, 23611 }, 23612 } 23613 23614 for name, test := range errorCases { 23615 if errs := validateClientIPAffinityConfig(test, field.NewPath("field")); len(errs) == 0 { 23616 t.Errorf("case: %v, expected failures: %v", name, errs) 23617 } 23618 } 23619 } 23620 23621 func TestValidateWindowsSecurityContextOptions(t *testing.T) { 23622 toPtr := func(s string) *string { 23623 return &s 23624 } 23625 23626 testCases := []struct { 23627 testName string 23628 23629 windowsOptions *core.WindowsSecurityContextOptions 23630 expectedErrorSubstring string 23631 }{{ 23632 testName: "a nil pointer", 23633 }, { 23634 testName: "an empty struct", 23635 windowsOptions: &core.WindowsSecurityContextOptions{}, 23636 }, { 23637 testName: "a valid input", 23638 windowsOptions: &core.WindowsSecurityContextOptions{ 23639 GMSACredentialSpecName: toPtr("dummy-gmsa-crep-spec-name"), 23640 GMSACredentialSpec: toPtr("dummy-gmsa-crep-spec-contents"), 23641 }, 23642 }, { 23643 testName: "a GMSA cred spec name that is not a valid resource name", 23644 windowsOptions: &core.WindowsSecurityContextOptions{ 23645 // invalid because of the underscore 23646 GMSACredentialSpecName: toPtr("not_a-valid-gmsa-crep-spec-name"), 23647 }, 23648 expectedErrorSubstring: dnsSubdomainLabelErrMsg, 23649 }, { 23650 testName: "empty GMSA cred spec contents", 23651 windowsOptions: &core.WindowsSecurityContextOptions{ 23652 GMSACredentialSpec: toPtr(""), 23653 }, 23654 expectedErrorSubstring: "gmsaCredentialSpec cannot be an empty string", 23655 }, { 23656 testName: "GMSA cred spec contents that are too long", 23657 windowsOptions: &core.WindowsSecurityContextOptions{ 23658 GMSACredentialSpec: toPtr(strings.Repeat("a", maxGMSACredentialSpecLength+1)), 23659 }, 23660 expectedErrorSubstring: "gmsaCredentialSpec size must be under", 23661 }, { 23662 testName: "RunAsUserName is nil", 23663 windowsOptions: &core.WindowsSecurityContextOptions{ 23664 RunAsUserName: nil, 23665 }, 23666 }, { 23667 testName: "a valid RunAsUserName", 23668 windowsOptions: &core.WindowsSecurityContextOptions{ 23669 RunAsUserName: toPtr("Container. User"), 23670 }, 23671 }, { 23672 testName: "a valid RunAsUserName with NetBios Domain", 23673 windowsOptions: &core.WindowsSecurityContextOptions{ 23674 RunAsUserName: toPtr("Network Service\\Container. User"), 23675 }, 23676 }, { 23677 testName: "a valid RunAsUserName with DNS Domain", 23678 windowsOptions: &core.WindowsSecurityContextOptions{ 23679 RunAsUserName: toPtr(strings.Repeat("fOo", 20) + ".liSH\\Container. User"), 23680 }, 23681 }, { 23682 testName: "a valid RunAsUserName with DNS Domain with a single character segment", 23683 windowsOptions: &core.WindowsSecurityContextOptions{ 23684 RunAsUserName: toPtr(strings.Repeat("fOo", 20) + ".l\\Container. User"), 23685 }, 23686 }, { 23687 testName: "a valid RunAsUserName with a long single segment DNS Domain", 23688 windowsOptions: &core.WindowsSecurityContextOptions{ 23689 RunAsUserName: toPtr(strings.Repeat("a", 42) + "\\Container. User"), 23690 }, 23691 }, { 23692 testName: "an empty RunAsUserName", 23693 windowsOptions: &core.WindowsSecurityContextOptions{ 23694 RunAsUserName: toPtr(""), 23695 }, 23696 expectedErrorSubstring: "runAsUserName cannot be an empty string", 23697 }, { 23698 testName: "RunAsUserName containing a control character", 23699 windowsOptions: &core.WindowsSecurityContextOptions{ 23700 RunAsUserName: toPtr("Container\tUser"), 23701 }, 23702 expectedErrorSubstring: "runAsUserName cannot contain control characters", 23703 }, { 23704 testName: "RunAsUserName containing too many backslashes", 23705 windowsOptions: &core.WindowsSecurityContextOptions{ 23706 RunAsUserName: toPtr("Container\\Foo\\Lish"), 23707 }, 23708 expectedErrorSubstring: "runAsUserName cannot contain more than one backslash", 23709 }, { 23710 testName: "RunAsUserName containing backslash but empty Domain", 23711 windowsOptions: &core.WindowsSecurityContextOptions{ 23712 RunAsUserName: toPtr("\\User"), 23713 }, 23714 expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios nor the DNS format", 23715 }, { 23716 testName: "RunAsUserName containing backslash but empty User", 23717 windowsOptions: &core.WindowsSecurityContextOptions{ 23718 RunAsUserName: toPtr("Container\\"), 23719 }, 23720 expectedErrorSubstring: "runAsUserName's User cannot be empty", 23721 }, { 23722 testName: "RunAsUserName's NetBios Domain is too long", 23723 windowsOptions: &core.WindowsSecurityContextOptions{ 23724 RunAsUserName: toPtr("NetBios " + strings.Repeat("a", 8) + "\\user"), 23725 }, 23726 expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", 23727 }, { 23728 testName: "RunAsUserName's DNS Domain is too long", 23729 windowsOptions: &core.WindowsSecurityContextOptions{ 23730 // even if this tests the max Domain length, the Domain should still be "valid". 23731 RunAsUserName: toPtr(strings.Repeat(strings.Repeat("a", 63)+".", 4)[:253] + ".com\\user"), 23732 }, 23733 expectedErrorSubstring: "runAsUserName's Domain length must be under", 23734 }, { 23735 testName: "RunAsUserName's User is too long", 23736 windowsOptions: &core.WindowsSecurityContextOptions{ 23737 RunAsUserName: toPtr(strings.Repeat("a", maxRunAsUserNameUserLength+1)), 23738 }, 23739 expectedErrorSubstring: "runAsUserName's User length must not be longer than", 23740 }, { 23741 testName: "RunAsUserName's User cannot contain only spaces or periods", 23742 windowsOptions: &core.WindowsSecurityContextOptions{ 23743 RunAsUserName: toPtr("... ..."), 23744 }, 23745 expectedErrorSubstring: "runAsUserName's User cannot contain only periods or spaces", 23746 }, { 23747 testName: "RunAsUserName's NetBios Domain cannot start with a dot", 23748 windowsOptions: &core.WindowsSecurityContextOptions{ 23749 RunAsUserName: toPtr(".FooLish\\User"), 23750 }, 23751 expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", 23752 }, { 23753 testName: "RunAsUserName's NetBios Domain cannot contain invalid characters", 23754 windowsOptions: &core.WindowsSecurityContextOptions{ 23755 RunAsUserName: toPtr("Foo? Lish?\\User"), 23756 }, 23757 expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios", 23758 }, { 23759 testName: "RunAsUserName's DNS Domain cannot contain invalid characters", 23760 windowsOptions: &core.WindowsSecurityContextOptions{ 23761 RunAsUserName: toPtr(strings.Repeat("a", 32) + ".com-\\user"), 23762 }, 23763 expectedErrorSubstring: "runAsUserName's Domain doesn't match the NetBios nor the DNS format", 23764 }, { 23765 testName: "RunAsUserName's User cannot contain invalid characters", 23766 windowsOptions: &core.WindowsSecurityContextOptions{ 23767 RunAsUserName: toPtr("Container/User"), 23768 }, 23769 expectedErrorSubstring: "runAsUserName's User cannot contain the following characters", 23770 }, 23771 } 23772 23773 for _, testCase := range testCases { 23774 t.Run("validateWindowsSecurityContextOptions with"+testCase.testName, func(t *testing.T) { 23775 errs := validateWindowsSecurityContextOptions(testCase.windowsOptions, field.NewPath("field")) 23776 23777 switch len(errs) { 23778 case 0: 23779 if testCase.expectedErrorSubstring != "" { 23780 t.Errorf("expected a failure containing the substring: %q", testCase.expectedErrorSubstring) 23781 } 23782 case 1: 23783 if testCase.expectedErrorSubstring == "" { 23784 t.Errorf("didn't expect a failure, got: %q", errs[0].Error()) 23785 } else if !strings.Contains(errs[0].Error(), testCase.expectedErrorSubstring) { 23786 t.Errorf("expected a failure with the substring %q, got %q instead", testCase.expectedErrorSubstring, errs[0].Error()) 23787 } 23788 default: 23789 t.Errorf("got %d failures", len(errs)) 23790 for i, err := range errs { 23791 t.Errorf("error %d: %q", i, err.Error()) 23792 } 23793 } 23794 }) 23795 } 23796 } 23797 23798 func testDataSourceInSpec(name, kind, apiGroup string) *core.PersistentVolumeClaimSpec { 23799 scName := "csi-plugin" 23800 dataSourceInSpec := core.PersistentVolumeClaimSpec{ 23801 AccessModes: []core.PersistentVolumeAccessMode{ 23802 core.ReadOnlyMany, 23803 }, 23804 Resources: core.VolumeResourceRequirements{ 23805 Requests: core.ResourceList{ 23806 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 23807 }, 23808 }, 23809 StorageClassName: &scName, 23810 DataSource: &core.TypedLocalObjectReference{ 23811 APIGroup: &apiGroup, 23812 Kind: kind, 23813 Name: name, 23814 }, 23815 } 23816 23817 return &dataSourceInSpec 23818 } 23819 23820 func TestAlphaVolumePVCDataSource(t *testing.T) { 23821 testCases := []struct { 23822 testName string 23823 claimSpec core.PersistentVolumeClaimSpec 23824 expectedFail bool 23825 }{{ 23826 testName: "test create from valid snapshot source", 23827 claimSpec: *testDataSourceInSpec("test_snapshot", "VolumeSnapshot", "snapshot.storage.k8s.io"), 23828 }, { 23829 testName: "test create from valid pvc source", 23830 claimSpec: *testDataSourceInSpec("test_pvc", "PersistentVolumeClaim", ""), 23831 }, { 23832 testName: "test missing name in snapshot datasource should fail", 23833 claimSpec: *testDataSourceInSpec("", "VolumeSnapshot", "snapshot.storage.k8s.io"), 23834 expectedFail: true, 23835 }, { 23836 testName: "test missing kind in snapshot datasource should fail", 23837 claimSpec: *testDataSourceInSpec("test_snapshot", "", "snapshot.storage.k8s.io"), 23838 expectedFail: true, 23839 }, { 23840 testName: "test create from valid generic custom resource source", 23841 claimSpec: *testDataSourceInSpec("test_generic", "Generic", "generic.storage.k8s.io"), 23842 }, { 23843 testName: "test invalid datasource should fail", 23844 claimSpec: *testDataSourceInSpec("test_pod", "Pod", ""), 23845 expectedFail: true, 23846 }, 23847 } 23848 23849 for _, tc := range testCases { 23850 opts := PersistentVolumeClaimSpecValidationOptions{} 23851 if tc.expectedFail { 23852 if errs := ValidatePersistentVolumeClaimSpec(&tc.claimSpec, field.NewPath("spec"), opts); len(errs) == 0 { 23853 t.Errorf("expected failure: %v", errs) 23854 } 23855 23856 } else { 23857 if errs := ValidatePersistentVolumeClaimSpec(&tc.claimSpec, field.NewPath("spec"), opts); len(errs) != 0 { 23858 t.Errorf("expected success: %v", errs) 23859 } 23860 } 23861 } 23862 } 23863 23864 func testAnyDataSource(t *testing.T, ds, dsRef bool) { 23865 testCases := []struct { 23866 testName string 23867 claimSpec core.PersistentVolumeClaimSpec 23868 expectedFail bool 23869 }{{ 23870 testName: "test create from valid snapshot source", 23871 claimSpec: *testDataSourceInSpec("test_snapshot", "VolumeSnapshot", "snapshot.storage.k8s.io"), 23872 }, { 23873 testName: "test create from valid pvc source", 23874 claimSpec: *testDataSourceInSpec("test_pvc", "PersistentVolumeClaim", ""), 23875 }, { 23876 testName: "test missing name in snapshot datasource should fail", 23877 claimSpec: *testDataSourceInSpec("", "VolumeSnapshot", "snapshot.storage.k8s.io"), 23878 expectedFail: true, 23879 }, { 23880 testName: "test missing kind in snapshot datasource should fail", 23881 claimSpec: *testDataSourceInSpec("test_snapshot", "", "snapshot.storage.k8s.io"), 23882 expectedFail: true, 23883 }, { 23884 testName: "test create from valid generic custom resource source", 23885 claimSpec: *testDataSourceInSpec("test_generic", "Generic", "generic.storage.k8s.io"), 23886 }, { 23887 testName: "test invalid datasource should fail", 23888 claimSpec: *testDataSourceInSpec("test_pod", "Pod", ""), 23889 expectedFail: true, 23890 }, 23891 } 23892 23893 for _, tc := range testCases { 23894 if dsRef { 23895 tc.claimSpec.DataSourceRef = &core.TypedObjectReference{ 23896 APIGroup: tc.claimSpec.DataSource.APIGroup, 23897 Kind: tc.claimSpec.DataSource.Kind, 23898 Name: tc.claimSpec.DataSource.Name, 23899 } 23900 } 23901 if !ds { 23902 tc.claimSpec.DataSource = nil 23903 } 23904 opts := PersistentVolumeClaimSpecValidationOptions{} 23905 if tc.expectedFail { 23906 if errs := ValidatePersistentVolumeClaimSpec(&tc.claimSpec, field.NewPath("spec"), opts); len(errs) == 0 { 23907 t.Errorf("expected failure: %v", errs) 23908 } 23909 } else { 23910 if errs := ValidatePersistentVolumeClaimSpec(&tc.claimSpec, field.NewPath("spec"), opts); len(errs) != 0 { 23911 t.Errorf("expected success: %v", errs) 23912 } 23913 } 23914 } 23915 } 23916 23917 func TestAnyDataSource(t *testing.T) { 23918 testAnyDataSource(t, true, false) 23919 testAnyDataSource(t, false, true) 23920 testAnyDataSource(t, true, false) 23921 } 23922 23923 func pvcSpecWithCrossNamespaceSource(apiGroup *string, kind string, namespace *string, name string, isDataSourceSet bool) *core.PersistentVolumeClaimSpec { 23924 scName := "csi-plugin" 23925 spec := core.PersistentVolumeClaimSpec{ 23926 AccessModes: []core.PersistentVolumeAccessMode{ 23927 core.ReadOnlyMany, 23928 }, 23929 Resources: core.VolumeResourceRequirements{ 23930 Requests: core.ResourceList{ 23931 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 23932 }, 23933 }, 23934 StorageClassName: &scName, 23935 DataSourceRef: &core.TypedObjectReference{ 23936 APIGroup: apiGroup, 23937 Kind: kind, 23938 Namespace: namespace, 23939 Name: name, 23940 }, 23941 } 23942 23943 if isDataSourceSet { 23944 spec.DataSource = &core.TypedLocalObjectReference{ 23945 APIGroup: apiGroup, 23946 Kind: kind, 23947 Name: name, 23948 } 23949 } 23950 return &spec 23951 } 23952 23953 func TestCrossNamespaceSource(t *testing.T) { 23954 snapAPIGroup := "snapshot.storage.k8s.io" 23955 coreAPIGroup := "" 23956 unsupportedAPIGroup := "unsupported.example.com" 23957 snapKind := "VolumeSnapshot" 23958 pvcKind := "PersistentVolumeClaim" 23959 goodNS := "ns1" 23960 badNS := "a*b" 23961 emptyNS := "" 23962 goodName := "snapshot1" 23963 23964 testCases := []struct { 23965 testName string 23966 expectedFail bool 23967 claimSpec *core.PersistentVolumeClaimSpec 23968 }{{ 23969 testName: "Feature gate enabled and valid xns DataSourceRef specified", 23970 expectedFail: false, 23971 claimSpec: pvcSpecWithCrossNamespaceSource(&snapAPIGroup, snapKind, &goodNS, goodName, false), 23972 }, { 23973 testName: "Feature gate enabled and xns DataSourceRef with PVC source specified", 23974 expectedFail: false, 23975 claimSpec: pvcSpecWithCrossNamespaceSource(&coreAPIGroup, pvcKind, &goodNS, goodName, false), 23976 }, { 23977 testName: "Feature gate enabled and xns DataSourceRef with unsupported source specified", 23978 expectedFail: false, 23979 claimSpec: pvcSpecWithCrossNamespaceSource(&unsupportedAPIGroup, "UnsupportedKind", &goodNS, goodName, false), 23980 }, { 23981 testName: "Feature gate enabled and xns DataSourceRef with nil apiGroup", 23982 expectedFail: true, 23983 claimSpec: pvcSpecWithCrossNamespaceSource(nil, "UnsupportedKind", &goodNS, goodName, false), 23984 }, { 23985 testName: "Feature gate enabled and xns DataSourceRef with invalid namspace specified", 23986 expectedFail: true, 23987 claimSpec: pvcSpecWithCrossNamespaceSource(&snapAPIGroup, snapKind, &badNS, goodName, false), 23988 }, { 23989 testName: "Feature gate enabled and xns DataSourceRef with nil namspace specified", 23990 expectedFail: false, 23991 claimSpec: pvcSpecWithCrossNamespaceSource(&snapAPIGroup, snapKind, nil, goodName, false), 23992 }, { 23993 testName: "Feature gate enabled and xns DataSourceRef with empty namspace specified", 23994 expectedFail: false, 23995 claimSpec: pvcSpecWithCrossNamespaceSource(&snapAPIGroup, snapKind, &emptyNS, goodName, false), 23996 }, { 23997 testName: "Feature gate enabled and both xns DataSourceRef and DataSource specified", 23998 expectedFail: true, 23999 claimSpec: pvcSpecWithCrossNamespaceSource(&snapAPIGroup, snapKind, &goodNS, goodName, true), 24000 }, 24001 } 24002 24003 for _, tc := range testCases { 24004 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AnyVolumeDataSource, true) 24005 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CrossNamespaceVolumeDataSource, true) 24006 opts := PersistentVolumeClaimSpecValidationOptions{} 24007 if tc.expectedFail { 24008 if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) == 0 { 24009 t.Errorf("%s: expected failure: %v", tc.testName, errs) 24010 } 24011 } else { 24012 if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) != 0 { 24013 t.Errorf("%s: expected success: %v", tc.testName, errs) 24014 } 24015 } 24016 } 24017 } 24018 24019 func pvcSpecWithVolumeAttributesClassName(vacName *string) *core.PersistentVolumeClaimSpec { 24020 scName := "csi-plugin" 24021 spec := core.PersistentVolumeClaimSpec{ 24022 AccessModes: []core.PersistentVolumeAccessMode{ 24023 core.ReadOnlyMany, 24024 }, 24025 Resources: core.VolumeResourceRequirements{ 24026 Requests: core.ResourceList{ 24027 core.ResourceName(core.ResourceStorage): resource.MustParse("10G"), 24028 }, 24029 }, 24030 StorageClassName: &scName, 24031 VolumeAttributesClassName: vacName, 24032 } 24033 return &spec 24034 } 24035 24036 func TestVolumeAttributesClass(t *testing.T) { 24037 testCases := []struct { 24038 testName string 24039 expectedFail bool 24040 enableVolumeAttributesClass bool 24041 claimSpec *core.PersistentVolumeClaimSpec 24042 }{ 24043 { 24044 testName: "Feature gate enabled and valid no volumeAttributesClassName specified", 24045 expectedFail: false, 24046 enableVolumeAttributesClass: true, 24047 claimSpec: pvcSpecWithVolumeAttributesClassName(nil), 24048 }, 24049 { 24050 testName: "Feature gate enabled and an empty volumeAttributesClassName specified", 24051 expectedFail: false, 24052 enableVolumeAttributesClass: true, 24053 claimSpec: pvcSpecWithVolumeAttributesClassName(utilpointer.String("")), 24054 }, 24055 { 24056 testName: "Feature gate enabled and valid volumeAttributesClassName specified", 24057 expectedFail: false, 24058 enableVolumeAttributesClass: true, 24059 claimSpec: pvcSpecWithVolumeAttributesClassName(utilpointer.String("foo")), 24060 }, 24061 { 24062 testName: "Feature gate enabled and invalid volumeAttributesClassName specified", 24063 expectedFail: true, 24064 enableVolumeAttributesClass: true, 24065 claimSpec: pvcSpecWithVolumeAttributesClassName(utilpointer.String("-invalid-")), 24066 }, 24067 } 24068 for _, tc := range testCases { 24069 opts := PersistentVolumeClaimSpecValidationOptions{ 24070 EnableVolumeAttributesClass: tc.enableVolumeAttributesClass, 24071 } 24072 if tc.expectedFail { 24073 if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) == 0 { 24074 t.Errorf("%s: expected failure: %v", tc.testName, errs) 24075 } 24076 } else { 24077 if errs := ValidatePersistentVolumeClaimSpec(tc.claimSpec, field.NewPath("spec"), opts); len(errs) != 0 { 24078 t.Errorf("%s: expected success: %v", tc.testName, errs) 24079 } 24080 } 24081 } 24082 } 24083 24084 func TestValidateTopologySpreadConstraints(t *testing.T) { 24085 fieldPath := field.NewPath("field") 24086 subFldPath0 := fieldPath.Index(0) 24087 fieldPathMinDomains := subFldPath0.Child("minDomains") 24088 fieldPathMaxSkew := subFldPath0.Child("maxSkew") 24089 fieldPathTopologyKey := subFldPath0.Child("topologyKey") 24090 fieldPathWhenUnsatisfiable := subFldPath0.Child("whenUnsatisfiable") 24091 fieldPathTopologyKeyAndWhenUnsatisfiable := subFldPath0.Child("{topologyKey, whenUnsatisfiable}") 24092 fieldPathMatchLabelKeys := subFldPath0.Child("matchLabelKeys") 24093 nodeAffinityField := subFldPath0.Child("nodeAffinityPolicy") 24094 nodeTaintsField := subFldPath0.Child("nodeTaintsPolicy") 24095 labelSelectorField := subFldPath0.Child("labelSelector") 24096 unknown := core.NodeInclusionPolicy("Unknown") 24097 ignore := core.NodeInclusionPolicyIgnore 24098 honor := core.NodeInclusionPolicyHonor 24099 24100 testCases := []struct { 24101 name string 24102 constraints []core.TopologySpreadConstraint 24103 wantFieldErrors field.ErrorList 24104 opts PodValidationOptions 24105 }{{ 24106 name: "all required fields ok", 24107 constraints: []core.TopologySpreadConstraint{{ 24108 MaxSkew: 1, 24109 TopologyKey: "k8s.io/zone", 24110 WhenUnsatisfiable: core.DoNotSchedule, 24111 MinDomains: utilpointer.Int32(3), 24112 }}, 24113 wantFieldErrors: field.ErrorList{}, 24114 }, { 24115 name: "missing MaxSkew", 24116 constraints: []core.TopologySpreadConstraint{ 24117 {TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24118 }, 24119 wantFieldErrors: []*field.Error{field.Invalid(fieldPathMaxSkew, int32(0), isNotPositiveErrorMsg)}, 24120 }, { 24121 name: "negative MaxSkew", 24122 constraints: []core.TopologySpreadConstraint{ 24123 {MaxSkew: -1, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24124 }, 24125 wantFieldErrors: []*field.Error{field.Invalid(fieldPathMaxSkew, int32(-1), isNotPositiveErrorMsg)}, 24126 }, { 24127 name: "can use MinDomains with ScheduleAnyway, when MinDomains = nil", 24128 constraints: []core.TopologySpreadConstraint{{ 24129 MaxSkew: 1, 24130 TopologyKey: "k8s.io/zone", 24131 WhenUnsatisfiable: core.ScheduleAnyway, 24132 MinDomains: nil, 24133 }}, 24134 wantFieldErrors: field.ErrorList{}, 24135 }, { 24136 name: "negative minDomains is invalid", 24137 constraints: []core.TopologySpreadConstraint{{ 24138 MaxSkew: 1, 24139 TopologyKey: "k8s.io/zone", 24140 WhenUnsatisfiable: core.DoNotSchedule, 24141 MinDomains: utilpointer.Int32(-1), 24142 }}, 24143 wantFieldErrors: []*field.Error{field.Invalid(fieldPathMinDomains, utilpointer.Int32(-1), isNotPositiveErrorMsg)}, 24144 }, { 24145 name: "cannot use non-nil MinDomains with ScheduleAnyway", 24146 constraints: []core.TopologySpreadConstraint{{ 24147 MaxSkew: 1, 24148 TopologyKey: "k8s.io/zone", 24149 WhenUnsatisfiable: core.ScheduleAnyway, 24150 MinDomains: utilpointer.Int32(10), 24151 }}, 24152 wantFieldErrors: []*field.Error{field.Invalid(fieldPathMinDomains, utilpointer.Int32(10), fmt.Sprintf("can only use minDomains if whenUnsatisfiable=%s, not %s", string(core.DoNotSchedule), string(core.ScheduleAnyway)))}, 24153 }, { 24154 name: "use negative MinDomains with ScheduleAnyway(invalid)", 24155 constraints: []core.TopologySpreadConstraint{{ 24156 MaxSkew: 1, 24157 TopologyKey: "k8s.io/zone", 24158 WhenUnsatisfiable: core.ScheduleAnyway, 24159 MinDomains: utilpointer.Int32(-1), 24160 }}, 24161 wantFieldErrors: []*field.Error{ 24162 field.Invalid(fieldPathMinDomains, utilpointer.Int32(-1), isNotPositiveErrorMsg), 24163 field.Invalid(fieldPathMinDomains, utilpointer.Int32(-1), fmt.Sprintf("can only use minDomains if whenUnsatisfiable=%s, not %s", string(core.DoNotSchedule), string(core.ScheduleAnyway))), 24164 }, 24165 }, { 24166 name: "missing TopologyKey", 24167 constraints: []core.TopologySpreadConstraint{ 24168 {MaxSkew: 1, WhenUnsatisfiable: core.DoNotSchedule}, 24169 }, 24170 wantFieldErrors: []*field.Error{field.Required(fieldPathTopologyKey, "can not be empty")}, 24171 }, { 24172 name: "missing scheduling mode", 24173 constraints: []core.TopologySpreadConstraint{ 24174 {MaxSkew: 1, TopologyKey: "k8s.io/zone"}, 24175 }, 24176 wantFieldErrors: []*field.Error{field.NotSupported(fieldPathWhenUnsatisfiable, core.UnsatisfiableConstraintAction(""), sets.List(supportedScheduleActions))}, 24177 }, { 24178 name: "unsupported scheduling mode", 24179 constraints: []core.TopologySpreadConstraint{ 24180 {MaxSkew: 1, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.UnsatisfiableConstraintAction("N/A")}, 24181 }, 24182 wantFieldErrors: []*field.Error{field.NotSupported(fieldPathWhenUnsatisfiable, core.UnsatisfiableConstraintAction("N/A"), sets.List(supportedScheduleActions))}, 24183 }, { 24184 name: "multiple constraints ok with all required fields", 24185 constraints: []core.TopologySpreadConstraint{ 24186 {MaxSkew: 1, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24187 {MaxSkew: 2, TopologyKey: "k8s.io/node", WhenUnsatisfiable: core.ScheduleAnyway}, 24188 }, 24189 wantFieldErrors: field.ErrorList{}, 24190 }, { 24191 name: "multiple constraints missing TopologyKey on partial ones", 24192 constraints: []core.TopologySpreadConstraint{ 24193 {MaxSkew: 1, WhenUnsatisfiable: core.ScheduleAnyway}, 24194 {MaxSkew: 2, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24195 }, 24196 wantFieldErrors: []*field.Error{field.Required(fieldPathTopologyKey, "can not be empty")}, 24197 }, { 24198 name: "duplicate constraints", 24199 constraints: []core.TopologySpreadConstraint{ 24200 {MaxSkew: 1, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24201 {MaxSkew: 2, TopologyKey: "k8s.io/zone", WhenUnsatisfiable: core.DoNotSchedule}, 24202 }, 24203 wantFieldErrors: []*field.Error{ 24204 field.Duplicate(fieldPathTopologyKeyAndWhenUnsatisfiable, fmt.Sprintf("{%v, %v}", "k8s.io/zone", core.DoNotSchedule)), 24205 }, 24206 }, { 24207 name: "supported policy name set on NodeAffinityPolicy and NodeTaintsPolicy", 24208 constraints: []core.TopologySpreadConstraint{{ 24209 MaxSkew: 1, 24210 TopologyKey: "k8s.io/zone", 24211 WhenUnsatisfiable: core.DoNotSchedule, 24212 NodeAffinityPolicy: &honor, 24213 NodeTaintsPolicy: &ignore, 24214 }}, 24215 wantFieldErrors: []*field.Error{}, 24216 }, { 24217 name: "unsupported policy name set on NodeAffinityPolicy", 24218 constraints: []core.TopologySpreadConstraint{{ 24219 MaxSkew: 1, 24220 TopologyKey: "k8s.io/zone", 24221 WhenUnsatisfiable: core.DoNotSchedule, 24222 NodeAffinityPolicy: &unknown, 24223 NodeTaintsPolicy: &ignore, 24224 }}, 24225 wantFieldErrors: []*field.Error{ 24226 field.NotSupported(nodeAffinityField, &unknown, sets.List(supportedPodTopologySpreadNodePolicies)), 24227 }, 24228 }, { 24229 name: "unsupported policy name set on NodeTaintsPolicy", 24230 constraints: []core.TopologySpreadConstraint{{ 24231 MaxSkew: 1, 24232 TopologyKey: "k8s.io/zone", 24233 WhenUnsatisfiable: core.DoNotSchedule, 24234 NodeAffinityPolicy: &honor, 24235 NodeTaintsPolicy: &unknown, 24236 }}, 24237 wantFieldErrors: []*field.Error{ 24238 field.NotSupported(nodeTaintsField, &unknown, sets.List(supportedPodTopologySpreadNodePolicies)), 24239 }, 24240 }, { 24241 name: "key in MatchLabelKeys isn't correctly defined", 24242 constraints: []core.TopologySpreadConstraint{{ 24243 MaxSkew: 1, 24244 TopologyKey: "k8s.io/zone", 24245 LabelSelector: &metav1.LabelSelector{}, 24246 WhenUnsatisfiable: core.DoNotSchedule, 24247 MatchLabelKeys: []string{"/simple"}, 24248 }}, 24249 wantFieldErrors: field.ErrorList{field.Invalid(fieldPathMatchLabelKeys.Index(0), "/simple", "prefix part must be non-empty")}, 24250 }, { 24251 name: "key exists in both matchLabelKeys and labelSelector", 24252 constraints: []core.TopologySpreadConstraint{{ 24253 MaxSkew: 1, 24254 TopologyKey: "k8s.io/zone", 24255 WhenUnsatisfiable: core.DoNotSchedule, 24256 MatchLabelKeys: []string{"foo"}, 24257 LabelSelector: &metav1.LabelSelector{ 24258 MatchExpressions: []metav1.LabelSelectorRequirement{ 24259 { 24260 Key: "foo", 24261 Operator: metav1.LabelSelectorOpNotIn, 24262 Values: []string{"value1", "value2"}, 24263 }, 24264 }, 24265 }, 24266 }}, 24267 wantFieldErrors: field.ErrorList{field.Invalid(fieldPathMatchLabelKeys.Index(0), "foo", "exists in both matchLabelKeys and labelSelector")}, 24268 }, { 24269 name: "key in MatchLabelKeys is forbidden to be specified when labelSelector is not set", 24270 constraints: []core.TopologySpreadConstraint{{ 24271 MaxSkew: 1, 24272 TopologyKey: "k8s.io/zone", 24273 WhenUnsatisfiable: core.DoNotSchedule, 24274 MatchLabelKeys: []string{"foo"}, 24275 }}, 24276 wantFieldErrors: field.ErrorList{field.Forbidden(fieldPathMatchLabelKeys, "must not be specified when labelSelector is not set")}, 24277 }, { 24278 name: "invalid matchLabels set on labelSelector when AllowInvalidTopologySpreadConstraintLabelSelector is false", 24279 constraints: []core.TopologySpreadConstraint{{ 24280 MaxSkew: 1, 24281 TopologyKey: "k8s.io/zone", 24282 WhenUnsatisfiable: core.DoNotSchedule, 24283 MinDomains: nil, 24284 LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "foo"}}, 24285 }}, 24286 wantFieldErrors: []*field.Error{field.Invalid(labelSelectorField.Child("matchLabels"), "NoUppercaseOrSpecialCharsLike=Equals", "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")}, 24287 opts: PodValidationOptions{AllowInvalidTopologySpreadConstraintLabelSelector: false}, 24288 }, { 24289 name: "invalid matchLabels set on labelSelector when AllowInvalidTopologySpreadConstraintLabelSelector is true", 24290 constraints: []core.TopologySpreadConstraint{{ 24291 MaxSkew: 1, 24292 TopologyKey: "k8s.io/zone", 24293 WhenUnsatisfiable: core.DoNotSchedule, 24294 MinDomains: nil, 24295 LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "foo"}}, 24296 }}, 24297 wantFieldErrors: []*field.Error{}, 24298 opts: PodValidationOptions{AllowInvalidTopologySpreadConstraintLabelSelector: true}, 24299 }, { 24300 name: "valid matchLabels set on labelSelector when AllowInvalidTopologySpreadConstraintLabelSelector is false", 24301 constraints: []core.TopologySpreadConstraint{{ 24302 MaxSkew: 1, 24303 TopologyKey: "k8s.io/zone", 24304 WhenUnsatisfiable: core.DoNotSchedule, 24305 MinDomains: nil, 24306 LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "foo"}}, 24307 }}, 24308 wantFieldErrors: []*field.Error{}, 24309 opts: PodValidationOptions{AllowInvalidTopologySpreadConstraintLabelSelector: false}, 24310 }, 24311 } 24312 24313 for _, tc := range testCases { 24314 t.Run(tc.name, func(t *testing.T) { 24315 errs := validateTopologySpreadConstraints(tc.constraints, fieldPath, tc.opts) 24316 if diff := cmp.Diff(tc.wantFieldErrors, errs); diff != "" { 24317 t.Errorf("unexpected field errors (-want, +got):\n%s", diff) 24318 } 24319 }) 24320 } 24321 } 24322 24323 func TestValidateOverhead(t *testing.T) { 24324 successCase := []struct { 24325 Name string 24326 overhead core.ResourceList 24327 }{{ 24328 Name: "Valid Overhead for CPU + Memory", 24329 overhead: core.ResourceList{ 24330 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 24331 core.ResourceName(core.ResourceMemory): resource.MustParse("10G"), 24332 }, 24333 }, 24334 } 24335 for _, tc := range successCase { 24336 if errs := validateOverhead(tc.overhead, field.NewPath("overheads"), PodValidationOptions{}); len(errs) != 0 { 24337 t.Errorf("%q unexpected error: %v", tc.Name, errs) 24338 } 24339 } 24340 24341 errorCase := []struct { 24342 Name string 24343 overhead core.ResourceList 24344 }{{ 24345 Name: "Invalid Overhead Resources", 24346 overhead: core.ResourceList{ 24347 core.ResourceName("my.org"): resource.MustParse("10m"), 24348 }, 24349 }, 24350 } 24351 for _, tc := range errorCase { 24352 if errs := validateOverhead(tc.overhead, field.NewPath("resources"), PodValidationOptions{}); len(errs) == 0 { 24353 t.Errorf("%q expected error", tc.Name) 24354 } 24355 } 24356 } 24357 24358 // helper creates a pod with name, namespace and IPs 24359 func makePod(podName string, podNamespace string, podIPs []core.PodIP) core.Pod { 24360 return core.Pod{ 24361 ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: podNamespace}, 24362 Spec: core.PodSpec{ 24363 Containers: []core.Container{{ 24364 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 24365 }}, 24366 RestartPolicy: core.RestartPolicyAlways, 24367 DNSPolicy: core.DNSClusterFirst, 24368 }, 24369 Status: core.PodStatus{ 24370 PodIPs: podIPs, 24371 }, 24372 } 24373 } 24374 func TestPodIPsValidation(t *testing.T) { 24375 testCases := []struct { 24376 pod core.Pod 24377 expectError bool 24378 }{{ 24379 expectError: false, 24380 pod: makePod("nil-ips", "ns", nil), 24381 }, { 24382 expectError: false, 24383 pod: makePod("empty-podips-list", "ns", []core.PodIP{}), 24384 }, { 24385 expectError: false, 24386 pod: makePod("single-ip-family-6", "ns", []core.PodIP{{IP: "::1"}}), 24387 }, { 24388 expectError: false, 24389 pod: makePod("single-ip-family-4", "ns", []core.PodIP{{IP: "1.1.1.1"}}), 24390 }, { 24391 expectError: false, 24392 pod: makePod("dual-stack-4-6", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "::1"}}), 24393 }, { 24394 expectError: false, 24395 pod: makePod("dual-stack-6-4", "ns", []core.PodIP{{IP: "::1"}, {IP: "1.1.1.1"}}), 24396 }, 24397 /* failure cases start here */ 24398 { 24399 expectError: true, 24400 pod: makePod("invalid-pod-ip", "ns", []core.PodIP{{IP: "this-is-not-an-ip"}}), 24401 }, { 24402 expectError: true, 24403 pod: makePod("dualstack-same-ip-family-6", "ns", []core.PodIP{{IP: "::1"}, {IP: "::2"}}), 24404 }, { 24405 expectError: true, 24406 pod: makePod("dualstack-same-ip-family-4", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "2.2.2.2"}}), 24407 }, { 24408 expectError: true, 24409 pod: makePod("dualstack-repeated-ip-family-6", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "::2"}}), 24410 }, { 24411 expectError: true, 24412 pod: makePod("dualstack-repeated-ip-family-4", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "2.2.2.2"}}), 24413 }, 24414 24415 { 24416 expectError: true, 24417 pod: makePod("dualstack-duplicate-ip-family-4", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "1.1.1.1"}, {IP: "::1"}}), 24418 }, { 24419 expectError: true, 24420 pod: makePod("dualstack-duplicate-ip-family-6", "ns", []core.PodIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "::1"}}), 24421 }, 24422 } 24423 24424 for _, testCase := range testCases { 24425 t.Run(testCase.pod.Name, func(t *testing.T) { 24426 for _, oldTestCase := range testCases { 24427 newPod := testCase.pod.DeepCopy() 24428 newPod.ResourceVersion = "1" 24429 24430 oldPod := oldTestCase.pod.DeepCopy() 24431 oldPod.ResourceVersion = "1" 24432 oldPod.Name = newPod.Name 24433 24434 errs := ValidatePodStatusUpdate(newPod, oldPod, PodValidationOptions{}) 24435 24436 if len(errs) == 0 && testCase.expectError { 24437 t.Fatalf("expected failure for %s, but there were none", testCase.pod.Name) 24438 } 24439 if len(errs) != 0 && !testCase.expectError { 24440 t.Fatalf("expected success for %s, but there were errors: %v", testCase.pod.Name, errs) 24441 } 24442 } 24443 }) 24444 } 24445 } 24446 24447 func makePodWithHostIPs(podName string, podNamespace string, hostIPs []core.HostIP) core.Pod { 24448 hostIP := "" 24449 if len(hostIPs) > 0 { 24450 hostIP = hostIPs[0].IP 24451 } 24452 return core.Pod{ 24453 ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: podNamespace}, 24454 Spec: core.PodSpec{ 24455 Containers: []core.Container{ 24456 { 24457 Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", 24458 }, 24459 }, 24460 RestartPolicy: core.RestartPolicyAlways, 24461 DNSPolicy: core.DNSClusterFirst, 24462 }, 24463 Status: core.PodStatus{ 24464 HostIP: hostIP, 24465 HostIPs: hostIPs, 24466 }, 24467 } 24468 } 24469 24470 func TestHostIPsValidation(t *testing.T) { 24471 testCases := []struct { 24472 pod core.Pod 24473 expectError bool 24474 }{ 24475 { 24476 expectError: false, 24477 pod: makePodWithHostIPs("nil-ips", "ns", nil), 24478 }, 24479 { 24480 expectError: false, 24481 pod: makePodWithHostIPs("empty-HostIPs-list", "ns", []core.HostIP{}), 24482 }, 24483 { 24484 expectError: false, 24485 pod: makePodWithHostIPs("single-ip-family-6", "ns", []core.HostIP{{IP: "::1"}}), 24486 }, 24487 { 24488 expectError: false, 24489 pod: makePodWithHostIPs("single-ip-family-4", "ns", []core.HostIP{{IP: "1.1.1.1"}}), 24490 }, 24491 { 24492 expectError: false, 24493 pod: makePodWithHostIPs("dual-stack-4-6", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "::1"}}), 24494 }, 24495 { 24496 expectError: false, 24497 pod: makePodWithHostIPs("dual-stack-6-4", "ns", []core.HostIP{{IP: "::1"}, {IP: "1.1.1.1"}}), 24498 }, 24499 /* failure cases start here */ 24500 { 24501 expectError: true, 24502 pod: makePodWithHostIPs("invalid-pod-ip", "ns", []core.HostIP{{IP: "this-is-not-an-ip"}}), 24503 }, 24504 { 24505 expectError: true, 24506 pod: makePodWithHostIPs("dualstack-same-ip-family-6", "ns", []core.HostIP{{IP: "::1"}, {IP: "::2"}}), 24507 }, 24508 { 24509 expectError: true, 24510 pod: makePodWithHostIPs("dualstack-same-ip-family-4", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "2.2.2.2"}}), 24511 }, 24512 { 24513 expectError: true, 24514 pod: makePodWithHostIPs("dualstack-repeated-ip-family-6", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "::2"}}), 24515 }, 24516 { 24517 expectError: true, 24518 pod: makePodWithHostIPs("dualstack-repeated-ip-family-4", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "2.2.2.2"}}), 24519 }, 24520 24521 { 24522 expectError: true, 24523 pod: makePodWithHostIPs("dualstack-duplicate-ip-family-4", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "1.1.1.1"}, {IP: "::1"}}), 24524 }, 24525 { 24526 expectError: true, 24527 pod: makePodWithHostIPs("dualstack-duplicate-ip-family-6", "ns", []core.HostIP{{IP: "1.1.1.1"}, {IP: "::1"}, {IP: "::1"}}), 24528 }, 24529 } 24530 24531 for _, testCase := range testCases { 24532 t.Run(testCase.pod.Name, func(t *testing.T) { 24533 for _, oldTestCase := range testCases { 24534 newPod := testCase.pod.DeepCopy() 24535 newPod.ResourceVersion = "1" 24536 24537 oldPod := oldTestCase.pod.DeepCopy() 24538 oldPod.ResourceVersion = "1" 24539 oldPod.Name = newPod.Name 24540 24541 errs := ValidatePodStatusUpdate(newPod, oldPod, PodValidationOptions{}) 24542 24543 if len(errs) == 0 && testCase.expectError { 24544 t.Fatalf("expected failure for %s, but there were none", testCase.pod.Name) 24545 } 24546 if len(errs) != 0 && !testCase.expectError { 24547 t.Fatalf("expected success for %s, but there were errors: %v", testCase.pod.Name, errs) 24548 } 24549 } 24550 }) 24551 } 24552 } 24553 24554 // makes a node with pod cidr and a name 24555 func makeNode(nodeName string, podCIDRs []string) core.Node { 24556 return core.Node{ 24557 ObjectMeta: metav1.ObjectMeta{ 24558 Name: nodeName, 24559 }, 24560 Status: core.NodeStatus{ 24561 Addresses: []core.NodeAddress{ 24562 {Type: core.NodeExternalIP, Address: "something"}, 24563 }, 24564 Capacity: core.ResourceList{ 24565 core.ResourceName(core.ResourceCPU): resource.MustParse("10"), 24566 core.ResourceName(core.ResourceMemory): resource.MustParse("0"), 24567 }, 24568 }, 24569 Spec: core.NodeSpec{ 24570 PodCIDRs: podCIDRs, 24571 }, 24572 } 24573 } 24574 func TestValidateNodeCIDRs(t *testing.T) { 24575 testCases := []struct { 24576 expectError bool 24577 node core.Node 24578 }{{ 24579 expectError: false, 24580 node: makeNode("nil-pod-cidr", nil), 24581 }, { 24582 expectError: false, 24583 node: makeNode("empty-pod-cidr", []string{}), 24584 }, { 24585 expectError: false, 24586 node: makeNode("single-pod-cidr-4", []string{"192.168.0.0/16"}), 24587 }, { 24588 expectError: false, 24589 node: makeNode("single-pod-cidr-6", []string{"2000::/10"}), 24590 }, 24591 24592 { 24593 expectError: false, 24594 node: makeNode("multi-pod-cidr-6-4", []string{"2000::/10", "192.168.0.0/16"}), 24595 }, { 24596 expectError: false, 24597 node: makeNode("multi-pod-cidr-4-6", []string{"192.168.0.0/16", "2000::/10"}), 24598 }, 24599 // error cases starts here 24600 { 24601 expectError: true, 24602 node: makeNode("invalid-pod-cidr", []string{"this-is-not-a-valid-cidr"}), 24603 }, { 24604 expectError: true, 24605 node: makeNode("duplicate-pod-cidr-4", []string{"10.0.0.1/16", "10.0.0.1/16"}), 24606 }, { 24607 expectError: true, 24608 node: makeNode("duplicate-pod-cidr-6", []string{"2000::/10", "2000::/10"}), 24609 }, { 24610 expectError: true, 24611 node: makeNode("not-a-dualstack-no-v4", []string{"2000::/10", "3000::/10"}), 24612 }, { 24613 expectError: true, 24614 node: makeNode("not-a-dualstack-no-v6", []string{"10.0.0.0/16", "10.1.0.0/16"}), 24615 }, { 24616 expectError: true, 24617 node: makeNode("not-a-dualstack-repeated-v6", []string{"2000::/10", "10.0.0.0/16", "3000::/10"}), 24618 }, { 24619 expectError: true, 24620 node: makeNode("not-a-dualstack-repeated-v4", []string{"10.0.0.0/16", "3000::/10", "10.1.0.0/16"}), 24621 }, 24622 } 24623 for _, testCase := range testCases { 24624 errs := ValidateNode(&testCase.node) 24625 if len(errs) == 0 && testCase.expectError { 24626 t.Errorf("expected failure for %s, but there were none", testCase.node.Name) 24627 return 24628 } 24629 if len(errs) != 0 && !testCase.expectError { 24630 t.Errorf("expected success for %s, but there were errors: %v", testCase.node.Name, errs) 24631 return 24632 } 24633 } 24634 } 24635 24636 func TestValidateSeccompAnnotationAndField(t *testing.T) { 24637 const containerName = "container" 24638 testProfile := "test" 24639 24640 for _, test := range []struct { 24641 description string 24642 pod *core.Pod 24643 validation func(*testing.T, string, field.ErrorList, *v1.Pod) 24644 }{{ 24645 description: "Field type unconfined and annotation does not match", 24646 pod: &core.Pod{ 24647 ObjectMeta: metav1.ObjectMeta{ 24648 Annotations: map[string]string{ 24649 v1.SeccompPodAnnotationKey: "not-matching", 24650 }, 24651 }, 24652 Spec: core.PodSpec{ 24653 SecurityContext: &core.PodSecurityContext{ 24654 SeccompProfile: &core.SeccompProfile{ 24655 Type: core.SeccompProfileTypeUnconfined, 24656 }, 24657 }, 24658 }, 24659 }, 24660 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24661 require.NotNil(t, allErrs, desc) 24662 }, 24663 }, { 24664 description: "Field type default and annotation does not match", 24665 pod: &core.Pod{ 24666 ObjectMeta: metav1.ObjectMeta{ 24667 Annotations: map[string]string{ 24668 v1.SeccompPodAnnotationKey: "not-matching", 24669 }, 24670 }, 24671 Spec: core.PodSpec{ 24672 SecurityContext: &core.PodSecurityContext{ 24673 SeccompProfile: &core.SeccompProfile{ 24674 Type: core.SeccompProfileTypeRuntimeDefault, 24675 }, 24676 }, 24677 }, 24678 }, 24679 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24680 require.NotNil(t, allErrs, desc) 24681 }, 24682 }, { 24683 description: "Field type localhost and annotation does not match", 24684 pod: &core.Pod{ 24685 ObjectMeta: metav1.ObjectMeta{ 24686 Annotations: map[string]string{ 24687 v1.SeccompPodAnnotationKey: "not-matching", 24688 }, 24689 }, 24690 Spec: core.PodSpec{ 24691 SecurityContext: &core.PodSecurityContext{ 24692 SeccompProfile: &core.SeccompProfile{ 24693 Type: core.SeccompProfileTypeLocalhost, 24694 LocalhostProfile: &testProfile, 24695 }, 24696 }, 24697 }, 24698 }, 24699 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24700 require.NotNil(t, allErrs, desc) 24701 }, 24702 }, { 24703 description: "Field type localhost and localhost/ prefixed annotation does not match", 24704 pod: &core.Pod{ 24705 ObjectMeta: metav1.ObjectMeta{ 24706 Annotations: map[string]string{ 24707 v1.SeccompPodAnnotationKey: "localhost/not-matching", 24708 }, 24709 }, 24710 Spec: core.PodSpec{ 24711 SecurityContext: &core.PodSecurityContext{ 24712 SeccompProfile: &core.SeccompProfile{ 24713 Type: core.SeccompProfileTypeLocalhost, 24714 LocalhostProfile: &testProfile, 24715 }, 24716 }, 24717 }, 24718 }, 24719 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24720 require.NotNil(t, allErrs, desc) 24721 }, 24722 }, { 24723 description: "Field type unconfined and annotation does not match (container)", 24724 pod: &core.Pod{ 24725 ObjectMeta: metav1.ObjectMeta{ 24726 Annotations: map[string]string{ 24727 v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching", 24728 }, 24729 }, 24730 Spec: core.PodSpec{ 24731 Containers: []core.Container{{ 24732 Name: containerName, 24733 SecurityContext: &core.SecurityContext{ 24734 SeccompProfile: &core.SeccompProfile{ 24735 Type: core.SeccompProfileTypeUnconfined, 24736 }, 24737 }, 24738 }}, 24739 }, 24740 }, 24741 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24742 require.NotNil(t, allErrs, desc) 24743 }, 24744 }, { 24745 description: "Field type default and annotation does not match (container)", 24746 pod: &core.Pod{ 24747 ObjectMeta: metav1.ObjectMeta{ 24748 Annotations: map[string]string{ 24749 v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching", 24750 }, 24751 }, 24752 Spec: core.PodSpec{ 24753 Containers: []core.Container{{ 24754 Name: containerName, 24755 SecurityContext: &core.SecurityContext{ 24756 SeccompProfile: &core.SeccompProfile{ 24757 Type: core.SeccompProfileTypeRuntimeDefault, 24758 }, 24759 }, 24760 }}, 24761 }, 24762 }, 24763 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24764 require.NotNil(t, allErrs, desc) 24765 }, 24766 }, { 24767 description: "Field type localhost and annotation does not match (container)", 24768 pod: &core.Pod{ 24769 ObjectMeta: metav1.ObjectMeta{ 24770 Annotations: map[string]string{ 24771 v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching", 24772 }, 24773 }, 24774 Spec: core.PodSpec{ 24775 Containers: []core.Container{{ 24776 Name: containerName, 24777 SecurityContext: &core.SecurityContext{ 24778 SeccompProfile: &core.SeccompProfile{ 24779 Type: core.SeccompProfileTypeLocalhost, 24780 LocalhostProfile: &testProfile, 24781 }, 24782 }, 24783 }}, 24784 }, 24785 }, 24786 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24787 require.NotNil(t, allErrs, desc) 24788 }, 24789 }, { 24790 description: "Field type localhost and localhost/ prefixed annotation does not match (container)", 24791 pod: &core.Pod{ 24792 ObjectMeta: metav1.ObjectMeta{ 24793 Annotations: map[string]string{ 24794 v1.SeccompContainerAnnotationKeyPrefix + containerName: "localhost/not-matching", 24795 }, 24796 }, 24797 Spec: core.PodSpec{ 24798 Containers: []core.Container{{ 24799 Name: containerName, 24800 SecurityContext: &core.SecurityContext{ 24801 SeccompProfile: &core.SeccompProfile{ 24802 Type: core.SeccompProfileTypeLocalhost, 24803 LocalhostProfile: &testProfile, 24804 }, 24805 }, 24806 }}, 24807 }, 24808 }, 24809 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24810 require.NotNil(t, allErrs, desc) 24811 }, 24812 }, { 24813 description: "Nil errors must not be appended (pod)", 24814 pod: &core.Pod{ 24815 ObjectMeta: metav1.ObjectMeta{ 24816 Annotations: map[string]string{ 24817 v1.SeccompPodAnnotationKey: "localhost/anyprofile", 24818 }, 24819 }, 24820 Spec: core.PodSpec{ 24821 SecurityContext: &core.PodSecurityContext{ 24822 SeccompProfile: &core.SeccompProfile{ 24823 Type: "Abc", 24824 }, 24825 }, 24826 Containers: []core.Container{{ 24827 Name: containerName, 24828 }}, 24829 }, 24830 }, 24831 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24832 require.Empty(t, allErrs, desc) 24833 }, 24834 }, { 24835 description: "Nil errors must not be appended (container)", 24836 pod: &core.Pod{ 24837 ObjectMeta: metav1.ObjectMeta{ 24838 Annotations: map[string]string{ 24839 v1.SeccompContainerAnnotationKeyPrefix + containerName: "localhost/not-matching", 24840 }, 24841 }, 24842 Spec: core.PodSpec{ 24843 Containers: []core.Container{{ 24844 SecurityContext: &core.SecurityContext{ 24845 SeccompProfile: &core.SeccompProfile{ 24846 Type: "Abc", 24847 }, 24848 }, 24849 Name: containerName, 24850 }}, 24851 }, 24852 }, 24853 validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) { 24854 require.Empty(t, allErrs, desc) 24855 }, 24856 }, 24857 } { 24858 output := &v1.Pod{ 24859 ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, 24860 } 24861 for i, ctr := range test.pod.Spec.Containers { 24862 output.Spec.Containers = append(output.Spec.Containers, v1.Container{}) 24863 if ctr.SecurityContext != nil && ctr.SecurityContext.SeccompProfile != nil { 24864 output.Spec.Containers[i].SecurityContext = &v1.SecurityContext{ 24865 SeccompProfile: &v1.SeccompProfile{ 24866 Type: v1.SeccompProfileType(ctr.SecurityContext.SeccompProfile.Type), 24867 LocalhostProfile: ctr.SecurityContext.SeccompProfile.LocalhostProfile, 24868 }, 24869 } 24870 } 24871 } 24872 errList := validateSeccompAnnotationsAndFields(test.pod.ObjectMeta, &test.pod.Spec, field.NewPath("")) 24873 test.validation(t, test.description, errList, output) 24874 } 24875 } 24876 24877 func TestValidateSeccompAnnotationsAndFieldsMatch(t *testing.T) { 24878 rootFld := field.NewPath("") 24879 tests := []struct { 24880 description string 24881 annotationValue string 24882 seccompField *core.SeccompProfile 24883 fldPath *field.Path 24884 expectedErr *field.Error 24885 }{{ 24886 description: "seccompField nil should return empty", 24887 expectedErr: nil, 24888 }, { 24889 description: "unconfined annotation and SeccompProfileTypeUnconfined should return empty", 24890 annotationValue: "unconfined", 24891 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeUnconfined}, 24892 expectedErr: nil, 24893 }, { 24894 description: "runtime/default annotation and SeccompProfileTypeRuntimeDefault should return empty", 24895 annotationValue: "runtime/default", 24896 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault}, 24897 expectedErr: nil, 24898 }, { 24899 description: "docker/default annotation and SeccompProfileTypeRuntimeDefault should return empty", 24900 annotationValue: "docker/default", 24901 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault}, 24902 expectedErr: nil, 24903 }, { 24904 description: "localhost/test.json annotation and SeccompProfileTypeLocalhost with correct profile should return empty", 24905 annotationValue: "localhost/test.json", 24906 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost, LocalhostProfile: utilpointer.String("test.json")}, 24907 expectedErr: nil, 24908 }, { 24909 description: "localhost/test.json annotation and SeccompProfileTypeLocalhost without profile should error", 24910 annotationValue: "localhost/test.json", 24911 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost}, 24912 fldPath: rootFld, 24913 expectedErr: field.Forbidden(rootFld.Child("localhostProfile"), "seccomp profile in annotation and field must match"), 24914 }, { 24915 description: "localhost/test.json annotation and SeccompProfileTypeLocalhost with different profile should error", 24916 annotationValue: "localhost/test.json", 24917 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost, LocalhostProfile: utilpointer.String("different.json")}, 24918 fldPath: rootFld, 24919 expectedErr: field.Forbidden(rootFld.Child("localhostProfile"), "seccomp profile in annotation and field must match"), 24920 }, { 24921 description: "localhost/test.json annotation and SeccompProfileTypeUnconfined with different profile should error", 24922 annotationValue: "localhost/test.json", 24923 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeUnconfined}, 24924 fldPath: rootFld, 24925 expectedErr: field.Forbidden(rootFld.Child("type"), "seccomp type in annotation and field must match"), 24926 }, { 24927 description: "localhost/test.json annotation and SeccompProfileTypeRuntimeDefault with different profile should error", 24928 annotationValue: "localhost/test.json", 24929 seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault}, 24930 fldPath: rootFld, 24931 expectedErr: field.Forbidden(rootFld.Child("type"), "seccomp type in annotation and field must match"), 24932 }, 24933 } 24934 24935 for i, test := range tests { 24936 err := validateSeccompAnnotationsAndFieldsMatch(test.annotationValue, test.seccompField, test.fldPath) 24937 assert.Equal(t, test.expectedErr, err, "TestCase[%d]: %s", i, test.description) 24938 } 24939 } 24940 24941 func TestValidatePodTemplateSpecSeccomp(t *testing.T) { 24942 rootFld := field.NewPath("template") 24943 tests := []struct { 24944 description string 24945 spec *core.PodTemplateSpec 24946 fldPath *field.Path 24947 expectedErr field.ErrorList 24948 }{{ 24949 description: "seccomp field and container annotation must match", 24950 fldPath: rootFld, 24951 expectedErr: field.ErrorList{ 24952 field.Forbidden( 24953 rootFld.Child("spec").Child("containers").Index(1).Child("securityContext").Child("seccompProfile").Child("type"), 24954 "seccomp type in annotation and field must match"), 24955 }, 24956 spec: &core.PodTemplateSpec{ 24957 ObjectMeta: metav1.ObjectMeta{ 24958 Annotations: map[string]string{ 24959 "container.seccomp.security.alpha.kubernetes.io/test2": "unconfined", 24960 }, 24961 }, 24962 Spec: core.PodSpec{ 24963 Containers: []core.Container{{ 24964 Name: "test1", 24965 Image: "alpine", 24966 ImagePullPolicy: core.PullAlways, 24967 TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError, 24968 }, { 24969 SecurityContext: &core.SecurityContext{ 24970 SeccompProfile: &core.SeccompProfile{ 24971 Type: core.SeccompProfileTypeRuntimeDefault, 24972 }, 24973 }, 24974 Name: "test2", 24975 Image: "alpine", 24976 ImagePullPolicy: core.PullAlways, 24977 TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError, 24978 }}, 24979 RestartPolicy: core.RestartPolicyAlways, 24980 DNSPolicy: core.DNSDefault, 24981 }, 24982 }, 24983 }, { 24984 description: "seccomp field and pod annotation must match", 24985 fldPath: rootFld, 24986 expectedErr: field.ErrorList{ 24987 field.Forbidden( 24988 rootFld.Child("spec").Child("securityContext").Child("seccompProfile").Child("type"), 24989 "seccomp type in annotation and field must match"), 24990 }, 24991 spec: &core.PodTemplateSpec{ 24992 ObjectMeta: metav1.ObjectMeta{ 24993 Annotations: map[string]string{ 24994 "seccomp.security.alpha.kubernetes.io/pod": "runtime/default", 24995 }, 24996 }, 24997 Spec: core.PodSpec{ 24998 SecurityContext: &core.PodSecurityContext{ 24999 SeccompProfile: &core.SeccompProfile{ 25000 Type: core.SeccompProfileTypeUnconfined, 25001 }, 25002 }, 25003 Containers: []core.Container{{ 25004 Name: "test", 25005 Image: "alpine", 25006 ImagePullPolicy: core.PullAlways, 25007 TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError, 25008 }}, 25009 RestartPolicy: core.RestartPolicyAlways, 25010 DNSPolicy: core.DNSDefault, 25011 }, 25012 }, 25013 }, { 25014 description: "init seccomp field and container annotation must match", 25015 fldPath: rootFld, 25016 expectedErr: field.ErrorList{ 25017 field.Forbidden( 25018 rootFld.Child("spec").Child("initContainers").Index(0).Child("securityContext").Child("seccompProfile").Child("type"), 25019 "seccomp type in annotation and field must match"), 25020 }, 25021 spec: &core.PodTemplateSpec{ 25022 ObjectMeta: metav1.ObjectMeta{ 25023 Annotations: map[string]string{ 25024 "container.seccomp.security.alpha.kubernetes.io/init-test": "unconfined", 25025 }, 25026 }, 25027 Spec: core.PodSpec{ 25028 Containers: []core.Container{{ 25029 Name: "test", 25030 Image: "alpine", 25031 ImagePullPolicy: core.PullAlways, 25032 TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError, 25033 }}, 25034 InitContainers: []core.Container{{ 25035 Name: "init-test", 25036 SecurityContext: &core.SecurityContext{ 25037 SeccompProfile: &core.SeccompProfile{ 25038 Type: core.SeccompProfileTypeRuntimeDefault, 25039 }, 25040 }, 25041 Image: "alpine", 25042 ImagePullPolicy: core.PullAlways, 25043 TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError, 25044 }}, 25045 RestartPolicy: core.RestartPolicyAlways, 25046 DNSPolicy: core.DNSDefault, 25047 }, 25048 }, 25049 }, 25050 } 25051 25052 for i, test := range tests { 25053 err := ValidatePodTemplateSpec(test.spec, rootFld, PodValidationOptions{}) 25054 assert.Equal(t, test.expectedErr, err, "TestCase[%d]: %s", i, test.description) 25055 } 25056 } 25057 25058 func TestValidateResourceRequirements(t *testing.T) { 25059 path := field.NewPath("resources") 25060 tests := []struct { 25061 name string 25062 requirements core.ResourceRequirements 25063 opts PodValidationOptions 25064 }{{ 25065 name: "limits and requests of hugepage resource are equal", 25066 requirements: core.ResourceRequirements{ 25067 Limits: core.ResourceList{ 25068 core.ResourceCPU: resource.MustParse("10"), 25069 core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("2Mi"), 25070 }, 25071 Requests: core.ResourceList{ 25072 core.ResourceCPU: resource.MustParse("10"), 25073 core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("2Mi"), 25074 }, 25075 }, 25076 opts: PodValidationOptions{}, 25077 }, { 25078 name: "limits and requests of memory resource are equal", 25079 requirements: core.ResourceRequirements{ 25080 Limits: core.ResourceList{ 25081 core.ResourceMemory: resource.MustParse("2Mi"), 25082 }, 25083 Requests: core.ResourceList{ 25084 core.ResourceMemory: resource.MustParse("2Mi"), 25085 }, 25086 }, 25087 opts: PodValidationOptions{}, 25088 }, { 25089 name: "limits and requests of cpu resource are equal", 25090 requirements: core.ResourceRequirements{ 25091 Limits: core.ResourceList{ 25092 core.ResourceCPU: resource.MustParse("10"), 25093 }, 25094 Requests: core.ResourceList{ 25095 core.ResourceCPU: resource.MustParse("10"), 25096 }, 25097 }, 25098 opts: PodValidationOptions{}, 25099 }, 25100 } 25101 25102 for _, tc := range tests { 25103 t.Run(tc.name, func(t *testing.T) { 25104 if errs := ValidateResourceRequirements(&tc.requirements, nil, path, tc.opts); len(errs) != 0 { 25105 t.Errorf("unexpected errors: %v", errs) 25106 } 25107 }) 25108 } 25109 25110 errTests := []struct { 25111 name string 25112 requirements core.ResourceRequirements 25113 opts PodValidationOptions 25114 }{{ 25115 name: "hugepage resource without cpu or memory", 25116 requirements: core.ResourceRequirements{ 25117 Limits: core.ResourceList{ 25118 core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("2Mi"), 25119 }, 25120 Requests: core.ResourceList{ 25121 core.ResourceName(core.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("2Mi"), 25122 }, 25123 }, 25124 opts: PodValidationOptions{}, 25125 }, 25126 } 25127 25128 for _, tc := range errTests { 25129 t.Run(tc.name, func(t *testing.T) { 25130 if errs := ValidateResourceRequirements(&tc.requirements, nil, path, tc.opts); len(errs) == 0 { 25131 t.Error("expected errors") 25132 } 25133 }) 25134 } 25135 } 25136 25137 func TestValidateNonSpecialIP(t *testing.T) { 25138 fp := field.NewPath("ip") 25139 25140 // Valid values. 25141 for _, tc := range []struct { 25142 desc string 25143 ip string 25144 }{ 25145 {"ipv4", "10.1.2.3"}, 25146 {"ipv4 class E", "244.1.2.3"}, 25147 {"ipv6", "2000::1"}, 25148 } { 25149 t.Run(tc.desc, func(t *testing.T) { 25150 errs := ValidateNonSpecialIP(tc.ip, fp) 25151 if len(errs) != 0 { 25152 t.Errorf("ValidateNonSpecialIP(%q, ...) = %v; want nil", tc.ip, errs) 25153 } 25154 }) 25155 } 25156 // Invalid cases 25157 for _, tc := range []struct { 25158 desc string 25159 ip string 25160 }{ 25161 {"ipv4 unspecified", "0.0.0.0"}, 25162 {"ipv6 unspecified", "::0"}, 25163 {"ipv4 localhost", "127.0.0.0"}, 25164 {"ipv4 localhost", "127.255.255.255"}, 25165 {"ipv6 localhost", "::1"}, 25166 {"ipv6 link local", "fe80::"}, 25167 {"ipv6 local multicast", "ff02::"}, 25168 } { 25169 t.Run(tc.desc, func(t *testing.T) { 25170 errs := ValidateNonSpecialIP(tc.ip, fp) 25171 if len(errs) == 0 { 25172 t.Errorf("ValidateNonSpecialIP(%q, ...) = nil; want non-nil (errors)", tc.ip) 25173 } 25174 }) 25175 } 25176 } 25177 25178 func TestValidateHostUsers(t *testing.T) { 25179 falseVar := false 25180 trueVar := true 25181 25182 cases := []struct { 25183 name string 25184 success bool 25185 spec *core.PodSpec 25186 }{{ 25187 name: "empty", 25188 success: true, 25189 spec: &core.PodSpec{}, 25190 }, { 25191 name: "hostUsers unset", 25192 success: true, 25193 spec: &core.PodSpec{ 25194 SecurityContext: &core.PodSecurityContext{}, 25195 }, 25196 }, { 25197 name: "hostUsers=false", 25198 success: true, 25199 spec: &core.PodSpec{ 25200 SecurityContext: &core.PodSecurityContext{ 25201 HostUsers: &falseVar, 25202 }, 25203 }, 25204 }, { 25205 name: "hostUsers=true", 25206 success: true, 25207 spec: &core.PodSpec{ 25208 SecurityContext: &core.PodSecurityContext{ 25209 HostUsers: &trueVar, 25210 }, 25211 }, 25212 }, { 25213 name: "hostUsers=false & volumes", 25214 success: true, 25215 spec: &core.PodSpec{ 25216 SecurityContext: &core.PodSecurityContext{ 25217 HostUsers: &falseVar, 25218 }, 25219 Volumes: []core.Volume{{ 25220 Name: "configmap", 25221 VolumeSource: core.VolumeSource{ 25222 ConfigMap: &core.ConfigMapVolumeSource{ 25223 LocalObjectReference: core.LocalObjectReference{Name: "configmap"}, 25224 }, 25225 }, 25226 }, { 25227 Name: "secret", 25228 VolumeSource: core.VolumeSource{ 25229 Secret: &core.SecretVolumeSource{ 25230 SecretName: "secret", 25231 }, 25232 }, 25233 }, { 25234 Name: "downward-api", 25235 VolumeSource: core.VolumeSource{ 25236 DownwardAPI: &core.DownwardAPIVolumeSource{}, 25237 }, 25238 }, { 25239 Name: "proj", 25240 VolumeSource: core.VolumeSource{ 25241 Projected: &core.ProjectedVolumeSource{}, 25242 }, 25243 }, { 25244 Name: "empty-dir", 25245 VolumeSource: core.VolumeSource{ 25246 EmptyDir: &core.EmptyDirVolumeSource{}, 25247 }, 25248 }}, 25249 }, 25250 }, { 25251 name: "hostUsers=false - stateful volume", 25252 success: true, 25253 spec: &core.PodSpec{ 25254 SecurityContext: &core.PodSecurityContext{ 25255 HostUsers: &falseVar, 25256 }, 25257 Volumes: []core.Volume{{ 25258 Name: "host-path", 25259 VolumeSource: core.VolumeSource{ 25260 HostPath: &core.HostPathVolumeSource{}, 25261 }, 25262 }}, 25263 }, 25264 }, { 25265 name: "hostUsers=true - unsupported volume", 25266 success: true, 25267 spec: &core.PodSpec{ 25268 SecurityContext: &core.PodSecurityContext{ 25269 HostUsers: &trueVar, 25270 }, 25271 Volumes: []core.Volume{{ 25272 Name: "host-path", 25273 VolumeSource: core.VolumeSource{ 25274 HostPath: &core.HostPathVolumeSource{}, 25275 }, 25276 }}, 25277 }, 25278 }, { 25279 name: "hostUsers=false & HostNetwork", 25280 success: false, 25281 spec: &core.PodSpec{ 25282 SecurityContext: &core.PodSecurityContext{ 25283 HostUsers: &falseVar, 25284 HostNetwork: true, 25285 }, 25286 }, 25287 }, { 25288 name: "hostUsers=false & HostPID", 25289 success: false, 25290 spec: &core.PodSpec{ 25291 SecurityContext: &core.PodSecurityContext{ 25292 HostUsers: &falseVar, 25293 HostPID: true, 25294 }, 25295 }, 25296 }, { 25297 name: "hostUsers=false & HostIPC", 25298 success: false, 25299 spec: &core.PodSpec{ 25300 SecurityContext: &core.PodSecurityContext{ 25301 HostUsers: &falseVar, 25302 HostIPC: true, 25303 }, 25304 }, 25305 }, 25306 } 25307 25308 for _, tc := range cases { 25309 t.Run(tc.name, func(t *testing.T) { 25310 fPath := field.NewPath("spec") 25311 25312 allErrs := validateHostUsers(tc.spec, fPath) 25313 if !tc.success && len(allErrs) == 0 { 25314 t.Errorf("Unexpected success") 25315 } 25316 if tc.success && len(allErrs) != 0 { 25317 t.Errorf("Unexpected error(s): %v", allErrs) 25318 } 25319 }) 25320 } 25321 } 25322 25323 func TestValidateWindowsHostProcessPod(t *testing.T) { 25324 const containerName = "container" 25325 falseVar := false 25326 trueVar := true 25327 25328 testCases := []struct { 25329 name string 25330 expectError bool 25331 allowPrivileged bool 25332 podSpec *core.PodSpec 25333 }{{ 25334 name: "Spec with feature enabled, pod-wide HostProcess=true, and HostNetwork unset should not validate", 25335 expectError: true, 25336 allowPrivileged: true, 25337 podSpec: &core.PodSpec{ 25338 SecurityContext: &core.PodSecurityContext{ 25339 WindowsOptions: &core.WindowsSecurityContextOptions{ 25340 HostProcess: &trueVar, 25341 }, 25342 }, 25343 Containers: []core.Container{{ 25344 Name: containerName, 25345 }}, 25346 }, 25347 }, { 25348 name: "Spec with feature enabled, pod-wide HostProcess=ture, and HostNetwork set should validate", 25349 expectError: false, 25350 allowPrivileged: true, 25351 podSpec: &core.PodSpec{ 25352 SecurityContext: &core.PodSecurityContext{ 25353 HostNetwork: true, 25354 WindowsOptions: &core.WindowsSecurityContextOptions{ 25355 HostProcess: &trueVar, 25356 }, 25357 }, 25358 Containers: []core.Container{{ 25359 Name: containerName, 25360 }}, 25361 }, 25362 }, { 25363 name: "Spec with feature enabled, pod-wide HostProcess=ture, HostNetwork set, and containers setting HostProcess=true should validate", 25364 expectError: false, 25365 allowPrivileged: true, 25366 podSpec: &core.PodSpec{ 25367 SecurityContext: &core.PodSecurityContext{ 25368 HostNetwork: true, 25369 WindowsOptions: &core.WindowsSecurityContextOptions{ 25370 HostProcess: &trueVar, 25371 }, 25372 }, 25373 Containers: []core.Container{{ 25374 Name: containerName, 25375 SecurityContext: &core.SecurityContext{ 25376 WindowsOptions: &core.WindowsSecurityContextOptions{ 25377 HostProcess: &trueVar, 25378 }, 25379 }, 25380 }}, 25381 InitContainers: []core.Container{{ 25382 Name: containerName, 25383 SecurityContext: &core.SecurityContext{ 25384 WindowsOptions: &core.WindowsSecurityContextOptions{ 25385 HostProcess: &trueVar, 25386 }, 25387 }, 25388 }}, 25389 }, 25390 }, { 25391 name: "Spec with feature enabled, pod-wide HostProcess=nil, HostNetwork set, and all containers setting HostProcess=true should validate", 25392 expectError: false, 25393 allowPrivileged: true, 25394 podSpec: &core.PodSpec{ 25395 SecurityContext: &core.PodSecurityContext{ 25396 HostNetwork: true, 25397 }, 25398 Containers: []core.Container{{ 25399 Name: containerName, 25400 SecurityContext: &core.SecurityContext{ 25401 WindowsOptions: &core.WindowsSecurityContextOptions{ 25402 HostProcess: &trueVar, 25403 }, 25404 }, 25405 }}, 25406 InitContainers: []core.Container{{ 25407 Name: containerName, 25408 SecurityContext: &core.SecurityContext{ 25409 WindowsOptions: &core.WindowsSecurityContextOptions{ 25410 HostProcess: &trueVar, 25411 }, 25412 }, 25413 }}, 25414 }, 25415 }, { 25416 name: "Pods with feature enabled, some containers setting HostProcess=true, and others setting HostProcess=false should not validate", 25417 expectError: true, 25418 allowPrivileged: true, 25419 podSpec: &core.PodSpec{ 25420 SecurityContext: &core.PodSecurityContext{ 25421 HostNetwork: true, 25422 }, 25423 Containers: []core.Container{{ 25424 Name: containerName, 25425 SecurityContext: &core.SecurityContext{ 25426 WindowsOptions: &core.WindowsSecurityContextOptions{ 25427 HostProcess: &trueVar, 25428 }, 25429 }, 25430 }}, 25431 InitContainers: []core.Container{{ 25432 Name: containerName, 25433 SecurityContext: &core.SecurityContext{ 25434 WindowsOptions: &core.WindowsSecurityContextOptions{ 25435 HostProcess: &falseVar, 25436 }, 25437 }, 25438 }}, 25439 }, 25440 }, { 25441 name: "Spec with feature enabled, some containers setting HostProcess=true, and other leaving HostProcess unset should not validate", 25442 expectError: true, 25443 allowPrivileged: true, 25444 podSpec: &core.PodSpec{ 25445 SecurityContext: &core.PodSecurityContext{ 25446 HostNetwork: true, 25447 }, 25448 Containers: []core.Container{{ 25449 Name: containerName, 25450 SecurityContext: &core.SecurityContext{ 25451 WindowsOptions: &core.WindowsSecurityContextOptions{ 25452 HostProcess: &trueVar, 25453 }, 25454 }, 25455 }}, 25456 InitContainers: []core.Container{{ 25457 Name: containerName, 25458 }}, 25459 }, 25460 }, { 25461 name: "Spec with feature enabled, pod-wide HostProcess=true, some containers setting HostProcess=true, and init containers setting HostProcess=false should not validate", 25462 expectError: true, 25463 allowPrivileged: true, 25464 podSpec: &core.PodSpec{ 25465 SecurityContext: &core.PodSecurityContext{ 25466 HostNetwork: true, 25467 WindowsOptions: &core.WindowsSecurityContextOptions{ 25468 HostProcess: &trueVar, 25469 }, 25470 }, 25471 Containers: []core.Container{{ 25472 Name: containerName, 25473 SecurityContext: &core.SecurityContext{ 25474 WindowsOptions: &core.WindowsSecurityContextOptions{ 25475 HostProcess: &trueVar, 25476 }, 25477 }, 25478 }}, 25479 InitContainers: []core.Container{{ 25480 Name: containerName, 25481 SecurityContext: &core.SecurityContext{ 25482 WindowsOptions: &core.WindowsSecurityContextOptions{ 25483 HostProcess: &falseVar, 25484 }, 25485 }, 25486 }}, 25487 }, 25488 }, { 25489 name: "Spec with feature enabled, pod-wide HostProcess=true, some containers setting HostProcess=true, and others setting HostProcess=false should not validate", 25490 expectError: true, 25491 allowPrivileged: true, 25492 podSpec: &core.PodSpec{ 25493 SecurityContext: &core.PodSecurityContext{ 25494 HostNetwork: true, 25495 WindowsOptions: &core.WindowsSecurityContextOptions{ 25496 HostProcess: &trueVar, 25497 }, 25498 }, 25499 Containers: []core.Container{{ 25500 Name: containerName, 25501 SecurityContext: &core.SecurityContext{ 25502 WindowsOptions: &core.WindowsSecurityContextOptions{ 25503 HostProcess: &trueVar, 25504 }, 25505 }, 25506 }, { 25507 Name: containerName, 25508 SecurityContext: &core.SecurityContext{ 25509 WindowsOptions: &core.WindowsSecurityContextOptions{ 25510 HostProcess: &falseVar, 25511 }, 25512 }, 25513 }}, 25514 }, 25515 }, { 25516 name: "Spec with feature enabled, pod-wide HostProcess=true, some containers setting HostProcess=true, and others leaving HostProcess=nil should validate", 25517 expectError: false, 25518 allowPrivileged: true, 25519 podSpec: &core.PodSpec{ 25520 SecurityContext: &core.PodSecurityContext{ 25521 HostNetwork: true, 25522 WindowsOptions: &core.WindowsSecurityContextOptions{ 25523 HostProcess: &trueVar, 25524 }, 25525 }, 25526 Containers: []core.Container{{ 25527 Name: containerName, 25528 SecurityContext: &core.SecurityContext{ 25529 WindowsOptions: &core.WindowsSecurityContextOptions{ 25530 HostProcess: &trueVar, 25531 }, 25532 }, 25533 }}, 25534 InitContainers: []core.Container{{ 25535 Name: containerName, 25536 }}, 25537 }, 25538 }, { 25539 name: "Spec with feature enabled, pod-wide HostProcess=false, some contaienrs setting HostProccess=true should not validate", 25540 expectError: true, 25541 allowPrivileged: true, 25542 podSpec: &core.PodSpec{ 25543 SecurityContext: &core.PodSecurityContext{ 25544 HostNetwork: true, 25545 WindowsOptions: &core.WindowsSecurityContextOptions{ 25546 HostProcess: &falseVar, 25547 }, 25548 }, 25549 Containers: []core.Container{{ 25550 Name: containerName, 25551 SecurityContext: &core.SecurityContext{ 25552 WindowsOptions: &core.WindowsSecurityContextOptions{ 25553 HostProcess: &trueVar, 25554 }, 25555 }, 25556 }}, 25557 InitContainers: []core.Container{{ 25558 Name: containerName, 25559 }}, 25560 }, 25561 }, { 25562 name: "Pod's HostProcess set to true but all containers override to false should not validate", 25563 expectError: true, 25564 allowPrivileged: true, 25565 podSpec: &core.PodSpec{ 25566 SecurityContext: &core.PodSecurityContext{ 25567 HostNetwork: true, 25568 WindowsOptions: &core.WindowsSecurityContextOptions{ 25569 HostProcess: &trueVar, 25570 }, 25571 }, 25572 Containers: []core.Container{{ 25573 Name: containerName, 25574 SecurityContext: &core.SecurityContext{ 25575 WindowsOptions: &core.WindowsSecurityContextOptions{ 25576 HostProcess: &falseVar, 25577 }, 25578 }, 25579 }}, 25580 }, 25581 }, { 25582 name: "Valid HostProcess pod should spec should not validate if allowPrivileged is not set", 25583 expectError: true, 25584 allowPrivileged: false, 25585 podSpec: &core.PodSpec{ 25586 SecurityContext: &core.PodSecurityContext{ 25587 HostNetwork: true, 25588 }, 25589 Containers: []core.Container{{ 25590 Name: containerName, 25591 SecurityContext: &core.SecurityContext{ 25592 WindowsOptions: &core.WindowsSecurityContextOptions{ 25593 HostProcess: &trueVar, 25594 }, 25595 }, 25596 }}, 25597 }, 25598 }, { 25599 name: "Non-HostProcess ephemeral container in HostProcess pod should not validate", 25600 expectError: true, 25601 allowPrivileged: true, 25602 podSpec: &core.PodSpec{ 25603 SecurityContext: &core.PodSecurityContext{ 25604 HostNetwork: true, 25605 WindowsOptions: &core.WindowsSecurityContextOptions{ 25606 HostProcess: &trueVar, 25607 }, 25608 }, 25609 Containers: []core.Container{{ 25610 Name: containerName, 25611 }}, 25612 EphemeralContainers: []core.EphemeralContainer{{ 25613 EphemeralContainerCommon: core.EphemeralContainerCommon{ 25614 SecurityContext: &core.SecurityContext{ 25615 WindowsOptions: &core.WindowsSecurityContextOptions{ 25616 HostProcess: &falseVar, 25617 }, 25618 }, 25619 }, 25620 }}, 25621 }, 25622 }, { 25623 name: "HostProcess ephemeral container in HostProcess pod should validate", 25624 expectError: false, 25625 allowPrivileged: true, 25626 podSpec: &core.PodSpec{ 25627 SecurityContext: &core.PodSecurityContext{ 25628 HostNetwork: true, 25629 WindowsOptions: &core.WindowsSecurityContextOptions{ 25630 HostProcess: &trueVar, 25631 }, 25632 }, 25633 Containers: []core.Container{{ 25634 Name: containerName, 25635 }}, 25636 EphemeralContainers: []core.EphemeralContainer{{ 25637 EphemeralContainerCommon: core.EphemeralContainerCommon{}, 25638 }}, 25639 }, 25640 }, { 25641 name: "Non-HostProcess ephemeral container in Non-HostProcess pod should validate", 25642 expectError: false, 25643 allowPrivileged: true, 25644 podSpec: &core.PodSpec{ 25645 Containers: []core.Container{{ 25646 Name: containerName, 25647 }}, 25648 EphemeralContainers: []core.EphemeralContainer{{ 25649 EphemeralContainerCommon: core.EphemeralContainerCommon{ 25650 SecurityContext: &core.SecurityContext{ 25651 WindowsOptions: &core.WindowsSecurityContextOptions{ 25652 HostProcess: &falseVar, 25653 }, 25654 }, 25655 }, 25656 }}, 25657 }, 25658 }, { 25659 name: "HostProcess ephemeral container in Non-HostProcess pod should not validate", 25660 expectError: true, 25661 allowPrivileged: true, 25662 podSpec: &core.PodSpec{ 25663 Containers: []core.Container{{ 25664 Name: containerName, 25665 }}, 25666 EphemeralContainers: []core.EphemeralContainer{{ 25667 EphemeralContainerCommon: core.EphemeralContainerCommon{ 25668 SecurityContext: &core.SecurityContext{ 25669 WindowsOptions: &core.WindowsSecurityContextOptions{ 25670 HostProcess: &trueVar, 25671 }, 25672 }, 25673 }, 25674 }}, 25675 }, 25676 }, 25677 } 25678 25679 for _, testCase := range testCases { 25680 t.Run(testCase.name, func(t *testing.T) { 25681 25682 capabilities.SetForTests(capabilities.Capabilities{ 25683 AllowPrivileged: testCase.allowPrivileged, 25684 }) 25685 25686 errs := validateWindowsHostProcessPod(testCase.podSpec, field.NewPath("spec")) 25687 if testCase.expectError && len(errs) == 0 { 25688 t.Errorf("Unexpected success") 25689 } 25690 if !testCase.expectError && len(errs) != 0 { 25691 t.Errorf("Unexpected error(s): %v", errs) 25692 } 25693 }) 25694 } 25695 } 25696 25697 func TestValidateOS(t *testing.T) { 25698 testCases := []struct { 25699 name string 25700 expectError bool 25701 podSpec *core.PodSpec 25702 }{{ 25703 name: "no OS field, featuregate", 25704 expectError: false, 25705 podSpec: &core.PodSpec{OS: nil}, 25706 }, { 25707 name: "empty OS field, featuregate", 25708 expectError: true, 25709 podSpec: &core.PodSpec{OS: &core.PodOS{}}, 25710 }, { 25711 name: "OS field, featuregate, valid OS", 25712 expectError: false, 25713 podSpec: &core.PodSpec{OS: &core.PodOS{Name: core.Linux}}, 25714 }, { 25715 name: "OS field, featuregate, valid OS", 25716 expectError: false, 25717 podSpec: &core.PodSpec{OS: &core.PodOS{Name: core.Windows}}, 25718 }, { 25719 name: "OS field, featuregate, empty OS", 25720 expectError: true, 25721 podSpec: &core.PodSpec{OS: &core.PodOS{Name: ""}}, 25722 }, { 25723 name: "OS field, featuregate, invalid OS", 25724 expectError: true, 25725 podSpec: &core.PodSpec{OS: &core.PodOS{Name: "dummyOS"}}, 25726 }, 25727 } 25728 for _, testCase := range testCases { 25729 t.Run(testCase.name, func(t *testing.T) { 25730 errs := validateOS(testCase.podSpec, field.NewPath("spec"), PodValidationOptions{}) 25731 if testCase.expectError && len(errs) == 0 { 25732 t.Errorf("Unexpected success") 25733 } 25734 if !testCase.expectError && len(errs) != 0 { 25735 t.Errorf("Unexpected error(s): %v", errs) 25736 } 25737 }) 25738 } 25739 } 25740 25741 func TestValidateAppArmorProfileFormat(t *testing.T) { 25742 tests := []struct { 25743 profile string 25744 expectValid bool 25745 }{ 25746 {"", true}, 25747 {v1.DeprecatedAppArmorBetaProfileRuntimeDefault, true}, 25748 {v1.DeprecatedAppArmorBetaProfileNameUnconfined, true}, 25749 {"baz", false}, // Missing local prefix. 25750 {v1.DeprecatedAppArmorBetaProfileNamePrefix + "/usr/sbin/ntpd", true}, 25751 {v1.DeprecatedAppArmorBetaProfileNamePrefix + "foo-bar", true}, 25752 } 25753 25754 for _, test := range tests { 25755 err := ValidateAppArmorProfileFormat(test.profile) 25756 if test.expectValid { 25757 assert.NoError(t, err, "Profile %s should be valid", test.profile) 25758 } else { 25759 assert.Error(t, err, fmt.Sprintf("Profile %s should not be valid", test.profile)) 25760 } 25761 } 25762 } 25763 25764 func TestValidateDownwardAPIHostIPs(t *testing.T) { 25765 testCases := []struct { 25766 name string 25767 expectError bool 25768 featureEnabled bool 25769 fieldSel *core.ObjectFieldSelector 25770 }{ 25771 { 25772 name: "has no hostIPs field, featuregate enabled", 25773 expectError: false, 25774 featureEnabled: true, 25775 fieldSel: &core.ObjectFieldSelector{FieldPath: "status.hostIP"}, 25776 }, 25777 { 25778 name: "has hostIPs field, featuregate enabled", 25779 expectError: false, 25780 featureEnabled: true, 25781 fieldSel: &core.ObjectFieldSelector{FieldPath: "status.hostIPs"}, 25782 }, 25783 } 25784 for _, testCase := range testCases { 25785 t.Run(testCase.name, func(t *testing.T) { 25786 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodHostIPs, testCase.featureEnabled) 25787 25788 errs := validateDownwardAPIHostIPs(testCase.fieldSel, field.NewPath("fieldSel"), PodValidationOptions{AllowHostIPsField: testCase.featureEnabled}) 25789 if testCase.expectError && len(errs) == 0 { 25790 t.Errorf("Unexpected success") 25791 } 25792 if !testCase.expectError && len(errs) != 0 { 25793 t.Errorf("Unexpected error(s): %v", errs) 25794 } 25795 }) 25796 } 25797 } 25798 25799 func TestValidatePVSecretReference(t *testing.T) { 25800 rootFld := field.NewPath("name") 25801 type args struct { 25802 secretRef *core.SecretReference 25803 fldPath *field.Path 25804 } 25805 tests := []struct { 25806 name string 25807 args args 25808 expectError bool 25809 expectedError string 25810 }{{ 25811 name: "invalid secret ref name", 25812 args: args{&core.SecretReference{Name: "$%^&*#", Namespace: "default"}, rootFld}, 25813 expectError: true, 25814 expectedError: "name.name: Invalid value: \"$%^&*#\": " + dnsSubdomainLabelErrMsg, 25815 }, { 25816 name: "invalid secret ref namespace", 25817 args: args{&core.SecretReference{Name: "valid", Namespace: "$%^&*#"}, rootFld}, 25818 expectError: true, 25819 expectedError: "name.namespace: Invalid value: \"$%^&*#\": " + dnsLabelErrMsg, 25820 }, { 25821 name: "invalid secret: missing namespace", 25822 args: args{&core.SecretReference{Name: "valid"}, rootFld}, 25823 expectError: true, 25824 expectedError: "name.namespace: Required value", 25825 }, { 25826 name: "invalid secret : missing name", 25827 args: args{&core.SecretReference{Namespace: "default"}, rootFld}, 25828 expectError: true, 25829 expectedError: "name.name: Required value", 25830 }, { 25831 name: "valid secret", 25832 args: args{&core.SecretReference{Name: "valid", Namespace: "default"}, rootFld}, 25833 expectError: false, 25834 expectedError: "", 25835 }, 25836 } 25837 for _, tt := range tests { 25838 t.Run(tt.name, func(t *testing.T) { 25839 errs := validatePVSecretReference(tt.args.secretRef, tt.args.fldPath) 25840 if tt.expectError && len(errs) == 0 { 25841 t.Errorf("Unexpected success") 25842 } 25843 if tt.expectError && len(errs) != 0 { 25844 str := errs[0].Error() 25845 if str != "" && !strings.Contains(str, tt.expectedError) { 25846 t.Errorf("%s: expected error detail either empty or %q, got %q", tt.name, tt.expectedError, str) 25847 } 25848 } 25849 if !tt.expectError && len(errs) != 0 { 25850 t.Errorf("Unexpected error(s): %v", errs) 25851 } 25852 }) 25853 } 25854 } 25855 25856 func TestValidateDynamicResourceAllocation(t *testing.T) { 25857 externalClaimName := "some-claim" 25858 externalClaimTemplateName := "some-claim-template" 25859 goodClaimSource := core.ClaimSource{ 25860 ResourceClaimName: &externalClaimName, 25861 } 25862 shortPodName := &metav1.ObjectMeta{ 25863 Name: "some-pod", 25864 } 25865 brokenPodName := &metav1.ObjectMeta{ 25866 Name: ".dot.com", 25867 } 25868 goodClaimTemplate := core.PodSpec{ 25869 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim-template"}}}}}, 25870 RestartPolicy: core.RestartPolicyAlways, 25871 DNSPolicy: core.DNSClusterFirst, 25872 ResourceClaims: []core.PodResourceClaim{{ 25873 Name: "my-claim-template", 25874 Source: core.ClaimSource{ 25875 ResourceClaimTemplateName: &externalClaimTemplateName, 25876 }, 25877 }}, 25878 } 25879 goodClaimReference := core.PodSpec{ 25880 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim-reference"}}}}}, 25881 RestartPolicy: core.RestartPolicyAlways, 25882 DNSPolicy: core.DNSClusterFirst, 25883 ResourceClaims: []core.PodResourceClaim{{ 25884 Name: "my-claim-reference", 25885 Source: core.ClaimSource{ 25886 ResourceClaimName: &externalClaimName, 25887 }, 25888 }}, 25889 } 25890 25891 successCases := map[string]core.PodSpec{ 25892 "resource claim reference": goodClaimTemplate, 25893 "resource claim template": goodClaimTemplate, 25894 "multiple claims": { 25895 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}, {Name: "another-claim"}}}}}, 25896 RestartPolicy: core.RestartPolicyAlways, 25897 DNSPolicy: core.DNSClusterFirst, 25898 ResourceClaims: []core.PodResourceClaim{{ 25899 Name: "my-claim", 25900 Source: goodClaimSource, 25901 }, { 25902 Name: "another-claim", 25903 Source: goodClaimSource, 25904 }}, 25905 }, 25906 "init container": { 25907 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 25908 InitContainers: []core.Container{{Name: "ctr-init", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 25909 RestartPolicy: core.RestartPolicyAlways, 25910 DNSPolicy: core.DNSClusterFirst, 25911 ResourceClaims: []core.PodResourceClaim{{ 25912 Name: "my-claim", 25913 Source: goodClaimSource, 25914 }}, 25915 }, 25916 } 25917 for k, v := range successCases { 25918 t.Run(k, func(t *testing.T) { 25919 if errs := ValidatePodSpec(&v, shortPodName, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { 25920 t.Errorf("expected success: %v", errs) 25921 } 25922 }) 25923 } 25924 25925 failureCases := map[string]core.PodSpec{ 25926 "pod claim name with prefix": { 25927 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 25928 RestartPolicy: core.RestartPolicyAlways, 25929 DNSPolicy: core.DNSClusterFirst, 25930 ResourceClaims: []core.PodResourceClaim{{ 25931 Name: "../my-claim", 25932 Source: goodClaimSource, 25933 }}, 25934 }, 25935 "pod claim name with path": { 25936 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 25937 RestartPolicy: core.RestartPolicyAlways, 25938 DNSPolicy: core.DNSClusterFirst, 25939 ResourceClaims: []core.PodResourceClaim{{ 25940 Name: "my/claim", 25941 Source: goodClaimSource, 25942 }}, 25943 }, 25944 "pod claim name empty": { 25945 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 25946 RestartPolicy: core.RestartPolicyAlways, 25947 DNSPolicy: core.DNSClusterFirst, 25948 ResourceClaims: []core.PodResourceClaim{{ 25949 Name: "", 25950 Source: goodClaimSource, 25951 }}, 25952 }, 25953 "duplicate pod claim entries": { 25954 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, 25955 RestartPolicy: core.RestartPolicyAlways, 25956 DNSPolicy: core.DNSClusterFirst, 25957 ResourceClaims: []core.PodResourceClaim{{ 25958 Name: "my-claim", 25959 Source: goodClaimSource, 25960 }, { 25961 Name: "my-claim", 25962 Source: goodClaimSource, 25963 }}, 25964 }, 25965 "resource claim source empty": { 25966 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 25967 RestartPolicy: core.RestartPolicyAlways, 25968 DNSPolicy: core.DNSClusterFirst, 25969 ResourceClaims: []core.PodResourceClaim{{ 25970 Name: "my-claim", 25971 Source: core.ClaimSource{}, 25972 }}, 25973 }, 25974 "resource claim reference and template": { 25975 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 25976 RestartPolicy: core.RestartPolicyAlways, 25977 DNSPolicy: core.DNSClusterFirst, 25978 ResourceClaims: []core.PodResourceClaim{{ 25979 Name: "my-claim", 25980 Source: core.ClaimSource{ 25981 ResourceClaimName: &externalClaimName, 25982 ResourceClaimTemplateName: &externalClaimTemplateName, 25983 }, 25984 }}, 25985 }, 25986 "claim not found": { 25987 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "no-such-claim"}}}}}, 25988 RestartPolicy: core.RestartPolicyAlways, 25989 DNSPolicy: core.DNSClusterFirst, 25990 ResourceClaims: []core.PodResourceClaim{{ 25991 Name: "my-claim", 25992 Source: goodClaimSource, 25993 }}, 25994 }, 25995 "claim name empty": { 25996 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: ""}}}}}, 25997 RestartPolicy: core.RestartPolicyAlways, 25998 DNSPolicy: core.DNSClusterFirst, 25999 ResourceClaims: []core.PodResourceClaim{{ 26000 Name: "my-claim", 26001 Source: goodClaimSource, 26002 }}, 26003 }, 26004 "pod claim name duplicates": { 26005 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}, {Name: "my-claim"}}}}}, 26006 RestartPolicy: core.RestartPolicyAlways, 26007 DNSPolicy: core.DNSClusterFirst, 26008 ResourceClaims: []core.PodResourceClaim{{ 26009 Name: "my-claim", 26010 Source: goodClaimSource, 26011 }}, 26012 }, 26013 "no claims defined": { 26014 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 26015 RestartPolicy: core.RestartPolicyAlways, 26016 DNSPolicy: core.DNSClusterFirst, 26017 }, 26018 "duplicate pod claim name": { 26019 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 26020 RestartPolicy: core.RestartPolicyAlways, 26021 DNSPolicy: core.DNSClusterFirst, 26022 ResourceClaims: []core.PodResourceClaim{{ 26023 Name: "my-claim", 26024 Source: goodClaimSource, 26025 }, { 26026 Name: "my-claim", 26027 Source: goodClaimSource, 26028 }}, 26029 }, 26030 "ephemeral container don't support resource requirements": { 26031 Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}}, 26032 EphemeralContainers: []core.EphemeralContainer{{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ctr-ephemeral", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", Resources: core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}}, TargetContainerName: "ctr"}}, 26033 RestartPolicy: core.RestartPolicyAlways, 26034 DNSPolicy: core.DNSClusterFirst, 26035 ResourceClaims: []core.PodResourceClaim{{ 26036 Name: "my-claim", 26037 Source: goodClaimSource, 26038 }}, 26039 }, 26040 "invalid claim template name": func() core.PodSpec { 26041 spec := goodClaimTemplate.DeepCopy() 26042 notLabel := ".foo_bar" 26043 spec.ResourceClaims[0].Source.ResourceClaimTemplateName = ¬Label 26044 return *spec 26045 }(), 26046 "invalid claim reference name": func() core.PodSpec { 26047 spec := goodClaimReference.DeepCopy() 26048 notLabel := ".foo_bar" 26049 spec.ResourceClaims[0].Source.ResourceClaimName = ¬Label 26050 return *spec 26051 }(), 26052 } 26053 for k, v := range failureCases { 26054 if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { 26055 t.Errorf("expected failure for %q", k) 26056 } 26057 } 26058 26059 t.Run("generated-claim-name", func(t *testing.T) { 26060 for _, spec := range []*core.PodSpec{&goodClaimTemplate, &goodClaimReference} { 26061 claimName := spec.ResourceClaims[0].Name 26062 t.Run(claimName, func(t *testing.T) { 26063 for _, podMeta := range []*metav1.ObjectMeta{shortPodName, brokenPodName} { 26064 t.Run(podMeta.Name, func(t *testing.T) { 26065 errs := ValidatePodSpec(spec, podMeta, field.NewPath("field"), PodValidationOptions{}) 26066 // Only one out of the four combinations fails. 26067 expectError := spec == &goodClaimTemplate && podMeta == brokenPodName 26068 if expectError && len(errs) == 0 { 26069 t.Error("did not get the expected failure") 26070 } 26071 if !expectError && len(errs) > 0 { 26072 t.Errorf("unexpected failures: %+v", errs) 26073 } 26074 }) 26075 } 26076 }) 26077 } 26078 }) 26079 } 26080 26081 func TestValidateLoadBalancerStatus(t *testing.T) { 26082 ipModeVIP := core.LoadBalancerIPModeVIP 26083 ipModeProxy := core.LoadBalancerIPModeProxy 26084 ipModeDummy := core.LoadBalancerIPMode("dummy") 26085 26086 testCases := []struct { 26087 name string 26088 ipModeEnabled bool 26089 nonLBAllowed bool 26090 tweakLBStatus func(s *core.LoadBalancerStatus) 26091 tweakSvcSpec func(s *core.ServiceSpec) 26092 numErrs int 26093 }{ 26094 { 26095 name: "type is not LB", 26096 nonLBAllowed: false, 26097 tweakSvcSpec: func(s *core.ServiceSpec) { 26098 s.Type = core.ServiceTypeClusterIP 26099 }, 26100 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26101 s.Ingress = []core.LoadBalancerIngress{{ 26102 IP: "1.2.3.4", 26103 }} 26104 }, 26105 numErrs: 1, 26106 }, { 26107 name: "type is not LB. back-compat", 26108 nonLBAllowed: true, 26109 tweakSvcSpec: func(s *core.ServiceSpec) { 26110 s.Type = core.ServiceTypeClusterIP 26111 }, 26112 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26113 s.Ingress = []core.LoadBalancerIngress{{ 26114 IP: "1.2.3.4", 26115 }} 26116 }, 26117 numErrs: 0, 26118 }, { 26119 name: "valid vip ipMode", 26120 ipModeEnabled: true, 26121 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26122 s.Ingress = []core.LoadBalancerIngress{{ 26123 IP: "1.2.3.4", 26124 IPMode: &ipModeVIP, 26125 }} 26126 }, 26127 numErrs: 0, 26128 }, { 26129 name: "valid proxy ipMode", 26130 ipModeEnabled: true, 26131 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26132 s.Ingress = []core.LoadBalancerIngress{{ 26133 IP: "1.2.3.4", 26134 IPMode: &ipModeProxy, 26135 }} 26136 }, 26137 numErrs: 0, 26138 }, { 26139 name: "invalid ipMode", 26140 ipModeEnabled: true, 26141 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26142 s.Ingress = []core.LoadBalancerIngress{{ 26143 IP: "1.2.3.4", 26144 IPMode: &ipModeDummy, 26145 }} 26146 }, 26147 numErrs: 1, 26148 }, { 26149 name: "missing ipMode with LoadbalancerIPMode enabled", 26150 ipModeEnabled: true, 26151 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26152 s.Ingress = []core.LoadBalancerIngress{{ 26153 IP: "1.2.3.4", 26154 }} 26155 }, 26156 numErrs: 1, 26157 }, { 26158 name: "missing ipMode with LoadbalancerIPMode disabled", 26159 ipModeEnabled: false, 26160 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26161 s.Ingress = []core.LoadBalancerIngress{{ 26162 IP: "1.2.3.4", 26163 }} 26164 }, 26165 numErrs: 0, 26166 }, { 26167 name: "missing ip with ipMode present", 26168 ipModeEnabled: true, 26169 tweakLBStatus: func(s *core.LoadBalancerStatus) { 26170 s.Ingress = []core.LoadBalancerIngress{{ 26171 IPMode: &ipModeProxy, 26172 }} 26173 }, 26174 numErrs: 1, 26175 }, 26176 } 26177 for _, tc := range testCases { 26178 t.Run(tc.name, func(t *testing.T) { 26179 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled) 26180 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AllowServiceLBStatusOnNonLB, tc.nonLBAllowed) 26181 status := core.LoadBalancerStatus{} 26182 tc.tweakLBStatus(&status) 26183 spec := core.ServiceSpec{Type: core.ServiceTypeLoadBalancer} 26184 if tc.tweakSvcSpec != nil { 26185 tc.tweakSvcSpec(&spec) 26186 } 26187 errs := ValidateLoadBalancerStatus(&status, field.NewPath("status"), &spec) 26188 if len(errs) != tc.numErrs { 26189 t.Errorf("Unexpected error list for case %q(expected:%v got %v) - Errors:\n %v", tc.name, tc.numErrs, len(errs), errs.ToAggregate()) 26190 } 26191 }) 26192 } 26193 } 26194 26195 func TestValidateSleepAction(t *testing.T) { 26196 fldPath := field.NewPath("root") 26197 getInvalidStr := func(gracePeriod int64) string { 26198 return fmt.Sprintf("must be greater than 0 and less than terminationGracePeriodSeconds (%d)", gracePeriod) 26199 } 26200 26201 testCases := []struct { 26202 name string 26203 action *core.SleepAction 26204 gracePeriod int64 26205 expectErr field.ErrorList 26206 }{ 26207 { 26208 name: "valid setting", 26209 action: &core.SleepAction{ 26210 Seconds: 5, 26211 }, 26212 gracePeriod: 30, 26213 }, 26214 { 26215 name: "negative seconds", 26216 action: &core.SleepAction{ 26217 Seconds: -1, 26218 }, 26219 gracePeriod: 30, 26220 expectErr: field.ErrorList{field.Invalid(fldPath, -1, getInvalidStr(30))}, 26221 }, 26222 { 26223 name: "longer than gracePeriod", 26224 action: &core.SleepAction{ 26225 Seconds: 5, 26226 }, 26227 gracePeriod: 3, 26228 expectErr: field.ErrorList{field.Invalid(fldPath, 5, getInvalidStr(3))}, 26229 }, 26230 } 26231 26232 for _, tc := range testCases { 26233 t.Run(tc.name, func(t *testing.T) { 26234 errs := validateSleepAction(tc.action, tc.gracePeriod, fldPath) 26235 26236 if len(tc.expectErr) > 0 && len(errs) == 0 { 26237 t.Errorf("Unexpected success") 26238 } else if len(tc.expectErr) == 0 && len(errs) != 0 { 26239 t.Errorf("Unexpected error(s): %v", errs) 26240 } else if len(tc.expectErr) > 0 { 26241 if tc.expectErr[0].Error() != errs[0].Error() { 26242 t.Errorf("Unexpected error(s): %v", errs) 26243 } 26244 } 26245 }) 26246 } 26247 }