k8s.io/kubernetes@v1.29.3/test/integration/disruption/disruption_test.go (about) 1 /* 2 Copyright 2019 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 disruption 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "path" 24 "reflect" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 clientv3 "go.etcd.io/etcd/client/v3" 31 v1 "k8s.io/api/core/v1" 32 policyv1 "k8s.io/api/policy/v1" 33 "k8s.io/api/policy/v1beta1" 34 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 35 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 36 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" 41 "k8s.io/apimachinery/pkg/types" 42 "k8s.io/apimachinery/pkg/util/intstr" 43 "k8s.io/apimachinery/pkg/util/wait" 44 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 45 "k8s.io/apiserver/pkg/registry/rest" 46 cacheddiscovery "k8s.io/client-go/discovery/cached/memory" 47 "k8s.io/client-go/dynamic" 48 "k8s.io/client-go/informers" 49 clientset "k8s.io/client-go/kubernetes" 50 restclient "k8s.io/client-go/rest" 51 "k8s.io/client-go/restmapper" 52 "k8s.io/client-go/scale" 53 "k8s.io/client-go/tools/cache" 54 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 55 "k8s.io/kubernetes/pkg/api/legacyscheme" 56 "k8s.io/kubernetes/pkg/controller/disruption" 57 "k8s.io/kubernetes/test/integration/etcd" 58 "k8s.io/kubernetes/test/integration/framework" 59 "k8s.io/kubernetes/test/integration/util" 60 "k8s.io/kubernetes/test/utils/ktesting" 61 "k8s.io/utils/clock" 62 "k8s.io/utils/ptr" 63 ) 64 65 const stalePodDisruptionTimeout = 3 * time.Second 66 67 func setup(ctx context.Context, t *testing.T) (*kubeapiservertesting.TestServer, *disruption.DisruptionController, informers.SharedInformerFactory, clientset.Interface, *apiextensionsclientset.Clientset, dynamic.Interface) { 68 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins", "ServiceAccount"}, framework.SharedEtcd()) 69 70 clientSet, err := clientset.NewForConfig(server.ClientConfig) 71 if err != nil { 72 t.Fatalf("Error creating clientset: %v", err) 73 } 74 resyncPeriod := 12 * time.Hour 75 informers := informers.NewSharedInformerFactory(clientset.NewForConfigOrDie(restclient.AddUserAgent(server.ClientConfig, "pdb-informers")), resyncPeriod) 76 77 client := clientset.NewForConfigOrDie(restclient.AddUserAgent(server.ClientConfig, "disruption-controller")) 78 79 discoveryClient := cacheddiscovery.NewMemCacheClient(clientSet.Discovery()) 80 mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 81 82 scaleKindResolver := scale.NewDiscoveryScaleKindResolver(client.Discovery()) 83 scaleClient, err := scale.NewForConfig(server.ClientConfig, mapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver) 84 if err != nil { 85 t.Fatalf("Error creating scaleClient: %v", err) 86 } 87 88 apiExtensionClient, err := apiextensionsclientset.NewForConfig(server.ClientConfig) 89 if err != nil { 90 t.Fatalf("Error creating extension clientset: %v", err) 91 } 92 93 dynamicClient, err := dynamic.NewForConfig(server.ClientConfig) 94 if err != nil { 95 t.Fatalf("Error creating dynamicClient: %v", err) 96 } 97 98 pdbc := disruption.NewDisruptionControllerInternal( 99 ctx, 100 informers.Core().V1().Pods(), 101 informers.Policy().V1().PodDisruptionBudgets(), 102 informers.Core().V1().ReplicationControllers(), 103 informers.Apps().V1().ReplicaSets(), 104 informers.Apps().V1().Deployments(), 105 informers.Apps().V1().StatefulSets(), 106 client, 107 mapper, 108 scaleClient, 109 client.Discovery(), 110 clock.RealClock{}, 111 stalePodDisruptionTimeout, 112 ) 113 return server, pdbc, informers, clientSet, apiExtensionClient, dynamicClient 114 } 115 116 func TestPDBWithScaleSubresource(t *testing.T) { 117 _, ctx := ktesting.NewTestContext(t) 118 ctx, cancel := context.WithCancel(ctx) 119 120 s, pdbc, informers, clientSet, apiExtensionClient, dynamicClient := setup(ctx, t) 121 defer s.TearDownFn() 122 defer cancel() 123 124 nsName := "pdb-scale-subresource" 125 createNs(ctx, t, nsName, clientSet) 126 127 informers.Start(ctx.Done()) 128 go pdbc.Run(ctx) 129 130 crdDefinition := newCustomResourceDefinition() 131 etcd.CreateTestCRDs(t, apiExtensionClient, true, crdDefinition) 132 gvr := schema.GroupVersionResource{Group: crdDefinition.Spec.Group, Version: crdDefinition.Spec.Versions[0].Name, Resource: crdDefinition.Spec.Names.Plural} 133 resourceClient := dynamicClient.Resource(gvr).Namespace(nsName) 134 135 replicas := 4 136 maxUnavailable := int32(2) 137 138 resource := &unstructured.Unstructured{ 139 Object: map[string]interface{}{ 140 "kind": crdDefinition.Spec.Names.Kind, 141 "apiVersion": crdDefinition.Spec.Group + "/" + crdDefinition.Spec.Versions[0].Name, 142 "metadata": map[string]interface{}{ 143 "name": "resource", 144 "namespace": nsName, 145 }, 146 "spec": map[string]interface{}{ 147 "replicas": replicas, 148 }, 149 }, 150 } 151 createdResource, err := resourceClient.Create(ctx, resource, metav1.CreateOptions{}) 152 if err != nil { 153 t.Error(err) 154 } 155 156 trueValue := true 157 ownerRefs := []metav1.OwnerReference{ 158 { 159 Name: resource.GetName(), 160 Kind: crdDefinition.Spec.Names.Kind, 161 APIVersion: crdDefinition.Spec.Group + "/" + crdDefinition.Spec.Versions[0].Name, 162 UID: createdResource.GetUID(), 163 Controller: &trueValue, 164 }, 165 } 166 for i := 0; i < replicas; i++ { 167 createPod(ctx, t, fmt.Sprintf("pod-%d", i), nsName, map[string]string{"app": "test-crd"}, clientSet, ownerRefs) 168 } 169 170 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 4, v1.PodRunning) 171 172 pdb := &policyv1.PodDisruptionBudget{ 173 ObjectMeta: metav1.ObjectMeta{ 174 Name: "test-pdb", 175 }, 176 Spec: policyv1.PodDisruptionBudgetSpec{ 177 MaxUnavailable: &intstr.IntOrString{ 178 Type: intstr.Int, 179 IntVal: maxUnavailable, 180 }, 181 Selector: &metav1.LabelSelector{ 182 MatchLabels: map[string]string{"app": "test-crd"}, 183 }, 184 }, 185 } 186 if _, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Create(ctx, pdb, metav1.CreateOptions{}); err != nil { 187 t.Errorf("Error creating PodDisruptionBudget: %v", err) 188 } 189 190 waitPDBStable(ctx, t, clientSet, 4, nsName, pdb.Name) 191 192 newPdb, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Get(ctx, pdb.Name, metav1.GetOptions{}) 193 if err != nil { 194 t.Errorf("Error getting PodDisruptionBudget: %v", err) 195 } 196 197 if expected, found := int32(replicas), newPdb.Status.ExpectedPods; expected != found { 198 t.Errorf("Expected %d, but found %d", expected, found) 199 } 200 if expected, found := int32(replicas)-maxUnavailable, newPdb.Status.DesiredHealthy; expected != found { 201 t.Errorf("Expected %d, but found %d", expected, found) 202 } 203 if expected, found := maxUnavailable, newPdb.Status.DisruptionsAllowed; expected != found { 204 t.Errorf("Expected %d, but found %d", expected, found) 205 } 206 } 207 208 func TestEmptySelector(t *testing.T) { 209 testcases := []struct { 210 name string 211 createPDBFunc func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error 212 expectedCurrentHealthy int32 213 }{ 214 { 215 name: "v1beta1 should not target any pods", 216 createPDBFunc: func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error { 217 pdb := &v1beta1.PodDisruptionBudget{ 218 ObjectMeta: metav1.ObjectMeta{ 219 Name: name, 220 }, 221 Spec: v1beta1.PodDisruptionBudgetSpec{ 222 MinAvailable: &minAvailable, 223 Selector: &metav1.LabelSelector{}, 224 }, 225 } 226 return createPDBUsingRemovedAPI(ctx, etcdClient, etcdStoragePrefix, nsName, pdb) 227 }, 228 expectedCurrentHealthy: 0, 229 }, 230 { 231 name: "v1 should target all pods", 232 createPDBFunc: func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error { 233 pdb := &policyv1.PodDisruptionBudget{ 234 ObjectMeta: metav1.ObjectMeta{ 235 Name: name, 236 }, 237 Spec: policyv1.PodDisruptionBudgetSpec{ 238 MinAvailable: &minAvailable, 239 Selector: &metav1.LabelSelector{}, 240 }, 241 } 242 _, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Create(ctx, pdb, metav1.CreateOptions{}) 243 return err 244 }, 245 expectedCurrentHealthy: 4, 246 }, 247 } 248 249 for i, tc := range testcases { 250 t.Run(tc.name, func(t *testing.T) { 251 _, ctx := ktesting.NewTestContext(t) 252 ctx, cancel := context.WithCancel(ctx) 253 254 s, pdbc, informers, clientSet, _, _ := setup(ctx, t) 255 defer s.TearDownFn() 256 defer cancel() 257 258 nsName := fmt.Sprintf("pdb-empty-selector-%d", i) 259 createNs(ctx, t, nsName, clientSet) 260 261 informers.Start(ctx.Done()) 262 go pdbc.Run(ctx) 263 264 replicas := 4 265 minAvailable := intstr.FromInt32(2) 266 267 for j := 0; j < replicas; j++ { 268 createPod(ctx, t, fmt.Sprintf("pod-%d", j), nsName, map[string]string{"app": "test-crd"}, 269 clientSet, []metav1.OwnerReference{}) 270 } 271 272 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 4, v1.PodRunning) 273 274 pdbName := "test-pdb" 275 if err := tc.createPDBFunc(ctx, clientSet, s.EtcdClient, s.EtcdStoragePrefix, pdbName, nsName, minAvailable); err != nil { 276 t.Errorf("Error creating PodDisruptionBudget: %v", err) 277 } 278 279 waitPDBStable(ctx, t, clientSet, tc.expectedCurrentHealthy, nsName, pdbName) 280 281 newPdb, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Get(ctx, pdbName, metav1.GetOptions{}) 282 if err != nil { 283 t.Errorf("Error getting PodDisruptionBudget: %v", err) 284 } 285 286 if expected, found := tc.expectedCurrentHealthy, newPdb.Status.CurrentHealthy; expected != found { 287 t.Errorf("Expected %d, but found %d", expected, found) 288 } 289 }) 290 } 291 } 292 293 func TestSelectorsForPodsWithoutLabels(t *testing.T) { 294 testcases := []struct { 295 name string 296 createPDBFunc func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error 297 expectedCurrentHealthy int32 298 }{ 299 { 300 name: "pods with no labels can be targeted by v1 PDBs with empty selector", 301 createPDBFunc: func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error { 302 pdb := &policyv1.PodDisruptionBudget{ 303 ObjectMeta: metav1.ObjectMeta{ 304 Name: name, 305 }, 306 Spec: policyv1.PodDisruptionBudgetSpec{ 307 MinAvailable: &minAvailable, 308 Selector: &metav1.LabelSelector{}, 309 }, 310 } 311 _, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Create(context.TODO(), pdb, metav1.CreateOptions{}) 312 return err 313 }, 314 expectedCurrentHealthy: 1, 315 }, 316 { 317 name: "pods with no labels can be targeted by v1 PDBs with DoesNotExist selector", 318 createPDBFunc: func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error { 319 pdb := &policyv1.PodDisruptionBudget{ 320 ObjectMeta: metav1.ObjectMeta{ 321 Name: name, 322 }, 323 Spec: policyv1.PodDisruptionBudgetSpec{ 324 MinAvailable: &minAvailable, 325 Selector: &metav1.LabelSelector{ 326 MatchExpressions: []metav1.LabelSelectorRequirement{ 327 { 328 Key: "DoesNotExist", 329 Operator: metav1.LabelSelectorOpDoesNotExist, 330 }, 331 }, 332 }, 333 }, 334 } 335 _, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Create(ctx, pdb, metav1.CreateOptions{}) 336 return err 337 }, 338 expectedCurrentHealthy: 1, 339 }, 340 { 341 name: "pods with no labels can be targeted by v1beta1 PDBs with DoesNotExist selector", 342 createPDBFunc: func(ctx context.Context, clientSet clientset.Interface, etcdClient *clientv3.Client, etcdStoragePrefix, name, nsName string, minAvailable intstr.IntOrString) error { 343 pdb := &v1beta1.PodDisruptionBudget{ 344 ObjectMeta: metav1.ObjectMeta{ 345 Name: name, 346 }, 347 Spec: v1beta1.PodDisruptionBudgetSpec{ 348 MinAvailable: &minAvailable, 349 Selector: &metav1.LabelSelector{ 350 MatchExpressions: []metav1.LabelSelectorRequirement{ 351 { 352 Key: "DoesNotExist", 353 Operator: metav1.LabelSelectorOpDoesNotExist, 354 }, 355 }, 356 }, 357 }, 358 } 359 return createPDBUsingRemovedAPI(ctx, etcdClient, etcdStoragePrefix, nsName, pdb) 360 }, 361 expectedCurrentHealthy: 1, 362 }, 363 } 364 365 for i, tc := range testcases { 366 t.Run(tc.name, func(t *testing.T) { 367 _, ctx := ktesting.NewTestContext(t) 368 ctx, cancel := context.WithCancel(ctx) 369 370 s, pdbc, informers, clientSet, _, _ := setup(ctx, t) 371 defer s.TearDownFn() 372 defer cancel() 373 374 nsName := fmt.Sprintf("pdb-selectors-%d", i) 375 createNs(ctx, t, nsName, clientSet) 376 377 informers.Start(ctx.Done()) 378 go pdbc.Run(ctx) 379 380 minAvailable := intstr.FromInt32(1) 381 382 // Create the PDB first and wait for it to settle. 383 pdbName := "test-pdb" 384 if err := tc.createPDBFunc(ctx, clientSet, s.EtcdClient, s.EtcdStoragePrefix, pdbName, nsName, minAvailable); err != nil { 385 t.Errorf("Error creating PodDisruptionBudget: %v", err) 386 } 387 waitPDBStable(ctx, t, clientSet, 0, nsName, pdbName) 388 389 // Create a pod and wait for it be reach the running phase. 390 createPod(ctx, t, "pod", nsName, map[string]string{}, clientSet, []metav1.OwnerReference{}) 391 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 1, v1.PodRunning) 392 393 // Then verify that the added pod are picked up by the disruption controller. 394 waitPDBStable(ctx, t, clientSet, 1, nsName, pdbName) 395 396 newPdb, err := clientSet.PolicyV1().PodDisruptionBudgets(nsName).Get(ctx, pdbName, metav1.GetOptions{}) 397 if err != nil { 398 t.Errorf("Error getting PodDisruptionBudget: %v", err) 399 } 400 401 if expected, found := tc.expectedCurrentHealthy, newPdb.Status.CurrentHealthy; expected != found { 402 t.Errorf("Expected %d, but found %d", expected, found) 403 } 404 }) 405 } 406 } 407 408 func createPod(ctx context.Context, t *testing.T, name, namespace string, labels map[string]string, clientSet clientset.Interface, ownerRefs []metav1.OwnerReference) { 409 pod := &v1.Pod{ 410 ObjectMeta: metav1.ObjectMeta{ 411 Name: name, 412 Namespace: namespace, 413 Labels: labels, 414 OwnerReferences: ownerRefs, 415 }, 416 Spec: v1.PodSpec{ 417 Containers: []v1.Container{ 418 { 419 Name: "fake-name", 420 Image: "fakeimage", 421 }, 422 }, 423 }, 424 } 425 _, err := clientSet.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{}) 426 if err != nil { 427 t.Error(err) 428 } 429 addPodConditionReady(pod) 430 if _, err := clientSet.CoreV1().Pods(namespace).UpdateStatus(ctx, pod, metav1.UpdateOptions{}); err != nil { 431 t.Error(err) 432 } 433 } 434 435 func createNs(ctx context.Context, t *testing.T, name string, clientSet clientset.Interface) { 436 _, err := clientSet.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ 437 ObjectMeta: metav1.ObjectMeta{ 438 Name: name, 439 }, 440 }, metav1.CreateOptions{}) 441 if err != nil { 442 t.Errorf("Error creating namespace: %v", err) 443 } 444 } 445 446 func addPodConditionReady(pod *v1.Pod) { 447 pod.Status = v1.PodStatus{ 448 Phase: v1.PodRunning, 449 Conditions: []v1.PodCondition{ 450 { 451 Type: v1.PodReady, 452 Status: v1.ConditionTrue, 453 }, 454 }, 455 } 456 } 457 458 func newCustomResourceDefinition() *apiextensionsv1.CustomResourceDefinition { 459 return &apiextensionsv1.CustomResourceDefinition{ 460 ObjectMeta: metav1.ObjectMeta{Name: "crds.mygroup.example.com"}, 461 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 462 Group: "mygroup.example.com", 463 Names: apiextensionsv1.CustomResourceDefinitionNames{ 464 Plural: "crds", 465 Singular: "crd", 466 Kind: "Crd", 467 ListKind: "CrdList", 468 }, 469 Scope: apiextensionsv1.NamespaceScoped, 470 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 471 { 472 Name: "v1beta1", 473 Served: true, 474 Storage: true, 475 Schema: fixtures.AllowAllSchema(), 476 Subresources: &apiextensionsv1.CustomResourceSubresources{ 477 Scale: &apiextensionsv1.CustomResourceSubresourceScale{ 478 SpecReplicasPath: ".spec.replicas", 479 StatusReplicasPath: ".status.replicas", 480 }, 481 }, 482 }, 483 }, 484 }, 485 } 486 } 487 488 func waitPDBStable(ctx context.Context, t *testing.T, clientSet clientset.Interface, podNum int32, ns, pdbName string) { 489 if err := wait.PollImmediate(2*time.Second, 60*time.Second, func() (bool, error) { 490 pdb, err := clientSet.PolicyV1().PodDisruptionBudgets(ns).Get(ctx, pdbName, metav1.GetOptions{}) 491 if err != nil { 492 return false, err 493 } 494 if pdb.Status.ObservedGeneration == 0 || pdb.Status.CurrentHealthy != podNum { 495 return false, nil 496 } 497 return true, nil 498 }); err != nil { 499 t.Fatal(err) 500 } 501 } 502 503 func waitToObservePods(t *testing.T, podInformer cache.SharedIndexInformer, podNum int, phase v1.PodPhase) { 504 if err := wait.PollImmediate(2*time.Second, 60*time.Second, func() (bool, error) { 505 objects := podInformer.GetIndexer().List() 506 if len(objects) != podNum { 507 return false, nil 508 } 509 for _, obj := range objects { 510 pod := obj.(*v1.Pod) 511 if pod.Status.Phase != phase { 512 return false, nil 513 } 514 } 515 return true, nil 516 }); err != nil { 517 t.Fatal(err) 518 } 519 } 520 521 // createPDBUsingRemovedAPI creates a PDB directly using etcd. This is must *ONLY* be used for checks of compatibility 522 // with removed data. Do not use this just because you don't want to update your test to use v1. Only use this 523 // when it actually matters. 524 func createPDBUsingRemovedAPI(ctx context.Context, etcdClient *clientv3.Client, etcdStoragePrefix, nsName string, betaPDB *v1beta1.PodDisruptionBudget) error { 525 betaPDB.APIVersion = v1beta1.SchemeGroupVersion.Group + "/" + v1beta1.SchemeGroupVersion.Version 526 betaPDB.Kind = "PodDisruptionBudget" 527 betaPDB.Namespace = nsName 528 betaPDB.Generation = 1 529 rest.FillObjectMetaSystemFields(betaPDB) 530 ctx = genericapirequest.WithNamespace(ctx, nsName) 531 key := path.Join("/", etcdStoragePrefix, "poddisruptionbudgets", nsName, betaPDB.Name) 532 protoSerializer := protobuf.NewSerializer(legacyscheme.Scheme, legacyscheme.Scheme) 533 buffer := bytes.NewBuffer(nil) 534 if err := protoSerializer.Encode(betaPDB, buffer); err != nil { 535 return err 536 } 537 _, err := etcdClient.Put(ctx, key, buffer.String()) 538 return err 539 } 540 541 func TestPatchCompatibility(t *testing.T) { 542 ctx, cancel := context.WithCancel(context.Background()) 543 544 s, pdbc, _, clientSet, _, _ := setup(ctx, t) 545 defer s.TearDownFn() 546 // Even though pdbc isn't used in this test, its creation is already 547 // spawning some goroutines. So we need to run it to ensure they won't leak. 548 cancel() 549 pdbc.Run(ctx) 550 551 testcases := []struct { 552 name string 553 version string 554 startingSelector *metav1.LabelSelector 555 patchType types.PatchType 556 patch string 557 force *bool 558 fieldManager string 559 expectSelector *metav1.LabelSelector 560 }{ 561 { 562 name: "v1-smp", 563 version: "v1", 564 patchType: types.StrategicMergePatchType, 565 patch: `{"spec":{"selector":{"matchLabels":{"patchmatch":"true"},"matchExpressions":[{"key":"patchexpression","operator":"In","values":["true"]}]}}}`, 566 // matchLabels and matchExpressions are both replaced (because selector patchStrategy=replace in v1) 567 expectSelector: &metav1.LabelSelector{ 568 MatchLabels: map[string]string{"patchmatch": "true"}, 569 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "patchexpression", Operator: "In", Values: []string{"true"}}}, 570 }, 571 }, 572 { 573 name: "v1-mergepatch", 574 version: "v1", 575 patchType: types.MergePatchType, 576 patch: `{"spec":{"selector":{"matchLabels":{"patchmatch":"true"},"matchExpressions":[{"key":"patchexpression","operator":"In","values":["true"]}]}}}`, 577 // matchLabels portion is merged, matchExpressions portion is replaced (because it's a list) 578 expectSelector: &metav1.LabelSelector{ 579 MatchLabels: map[string]string{"basematch": "true", "patchmatch": "true"}, 580 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "patchexpression", Operator: "In", Values: []string{"true"}}}, 581 }, 582 }, 583 { 584 name: "v1-apply", 585 version: "v1", 586 patchType: types.ApplyPatchType, 587 patch: `{"apiVersion":"policy/v1","kind":"PodDisruptionBudget","spec":{"selector":{"matchLabels":{"patchmatch":"true"},"matchExpressions":[{"key":"patchexpression","operator":"In","values":["true"]}]}}}`, 588 force: ptr.To(true), 589 fieldManager: "test", 590 // entire selector is replaced (because structType=atomic) 591 expectSelector: &metav1.LabelSelector{ 592 MatchLabels: map[string]string{"patchmatch": "true"}, 593 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "patchexpression", Operator: "In", Values: []string{"true"}}}, 594 }, 595 }, 596 } 597 598 for _, tc := range testcases { 599 t.Run(tc.name, func(t *testing.T) { 600 ns := "default" 601 maxUnavailable := int32(2) 602 pdb := &policyv1.PodDisruptionBudget{ 603 ObjectMeta: metav1.ObjectMeta{ 604 Name: "test-pdb", 605 }, 606 Spec: policyv1.PodDisruptionBudgetSpec{ 607 MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: maxUnavailable}, 608 Selector: &metav1.LabelSelector{ 609 MatchLabels: map[string]string{"basematch": "true"}, 610 MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "baseexpression", Operator: "In", Values: []string{"true"}}}, 611 }, 612 }, 613 } 614 if _, err := clientSet.PolicyV1().PodDisruptionBudgets(ns).Create(context.TODO(), pdb, metav1.CreateOptions{}); err != nil { 615 t.Fatalf("Error creating PodDisruptionBudget: %v", err) 616 } 617 defer func() { 618 err := clientSet.PolicyV1().PodDisruptionBudgets(ns).Delete(context.TODO(), pdb.Name, metav1.DeleteOptions{}) 619 if err != nil { 620 t.Fatal(err) 621 } 622 }() 623 624 var resultSelector *metav1.LabelSelector 625 switch tc.version { 626 case "v1": 627 result, err := clientSet.PolicyV1().PodDisruptionBudgets(ns).Patch(context.TODO(), pdb.Name, tc.patchType, []byte(tc.patch), metav1.PatchOptions{Force: tc.force, FieldManager: tc.fieldManager}) 628 if err != nil { 629 t.Fatal(err) 630 } 631 resultSelector = result.Spec.Selector 632 default: 633 t.Error("unknown version") 634 } 635 636 if !reflect.DeepEqual(resultSelector, tc.expectSelector) { 637 t.Fatalf("unexpected selector:\n%s", cmp.Diff(tc.expectSelector, resultSelector)) 638 } 639 }) 640 } 641 } 642 643 func TestStalePodDisruption(t *testing.T) { 644 _, ctx := ktesting.NewTestContext(t) 645 ctx, cancel := context.WithCancel(ctx) 646 647 s, pdbc, informers, clientSet, _, _ := setup(ctx, t) 648 defer s.TearDownFn() 649 defer cancel() 650 651 nsName := "pdb-stale-pod-disruption" 652 createNs(ctx, t, nsName, clientSet) 653 654 informers.Start(ctx.Done()) 655 informers.WaitForCacheSync(ctx.Done()) 656 go pdbc.Run(ctx) 657 658 cases := map[string]struct { 659 deletePod bool 660 podPhase v1.PodPhase 661 reason string 662 wantConditions []v1.PodCondition 663 }{ 664 "stale-condition": { 665 podPhase: v1.PodRunning, 666 wantConditions: []v1.PodCondition{ 667 { 668 Type: v1.DisruptionTarget, 669 Status: v1.ConditionFalse, 670 }, 671 }, 672 }, 673 "deleted-pod": { 674 podPhase: v1.PodRunning, 675 deletePod: true, 676 wantConditions: []v1.PodCondition{ 677 { 678 Type: v1.DisruptionTarget, 679 Status: v1.ConditionTrue, 680 }, 681 }, 682 }, 683 "disruption-condition-by-kubelet": { 684 podPhase: v1.PodFailed, 685 reason: v1.PodReasonTerminationByKubelet, 686 wantConditions: []v1.PodCondition{ 687 { 688 Type: v1.DisruptionTarget, 689 Status: v1.ConditionTrue, 690 Reason: v1.PodReasonTerminationByKubelet, 691 }, 692 }, 693 }, 694 "disruption-condition-on-failed-pod": { 695 podPhase: v1.PodFailed, 696 wantConditions: []v1.PodCondition{ 697 { 698 Type: v1.DisruptionTarget, 699 Status: v1.ConditionTrue, 700 }, 701 }, 702 }, 703 } 704 705 for name, tc := range cases { 706 t.Run(name, func(t *testing.T) { 707 pod := util.InitPausePod(&util.PausePodConfig{ 708 Name: name, 709 Namespace: nsName, 710 NodeName: "foo", // mock pod as scheduled so that it's not immediately deleted when calling Delete. 711 }) 712 var err error 713 pod, err = util.CreatePausePod(clientSet, pod) 714 if err != nil { 715 t.Fatalf("Failed creating pod: %v", err) 716 } 717 718 pod.Status.Phase = tc.podPhase 719 pod.Status.Conditions = append(pod.Status.Conditions, v1.PodCondition{ 720 Type: v1.DisruptionTarget, 721 Status: v1.ConditionTrue, 722 Reason: tc.reason, 723 LastTransitionTime: metav1.Now(), 724 }) 725 pod, err = clientSet.CoreV1().Pods(nsName).UpdateStatus(ctx, pod, metav1.UpdateOptions{}) 726 if err != nil { 727 t.Fatalf("Failed updating pod: %v", err) 728 } 729 730 if tc.deletePod { 731 if err := clientSet.CoreV1().Pods(nsName).Delete(ctx, name, metav1.DeleteOptions{}); err != nil { 732 t.Fatalf("Failed to delete pod: %v", err) 733 } 734 } 735 time.Sleep(stalePodDisruptionTimeout) 736 diff := "" 737 if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (done bool, err error) { 738 pod, err = clientSet.CoreV1().Pods(nsName).Get(ctx, name, metav1.GetOptions{}) 739 if err != nil { 740 return false, err 741 } 742 if tc.deletePod && pod.DeletionTimestamp == nil { 743 return false, nil 744 } 745 diff = cmp.Diff(tc.wantConditions, pod.Status.Conditions, cmpopts.IgnoreFields(v1.PodCondition{}, "LastTransitionTime")) 746 return diff == "", nil 747 }); err != nil { 748 t.Errorf("Failed waiting for status to change: %v", err) 749 if diff != "" { 750 t.Errorf("Pod has conditions (-want,+got):\n%s", diff) 751 } 752 } 753 }) 754 } 755 }