k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/evictions/evictions_test.go (about) 1 /* 2 Copyright 2015 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 evictions 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "reflect" 24 "strings" 25 "sync" 26 "sync/atomic" 27 "testing" 28 "time" 29 30 v1 "k8s.io/api/core/v1" 31 policyv1 "k8s.io/api/policy/v1" 32 policyv1beta1 "k8s.io/api/policy/v1beta1" 33 apierrors "k8s.io/apimachinery/pkg/api/errors" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/types" 39 utilerrors "k8s.io/apimachinery/pkg/util/errors" 40 "k8s.io/apimachinery/pkg/util/intstr" 41 "k8s.io/apimachinery/pkg/util/uuid" 42 "k8s.io/apimachinery/pkg/util/wait" 43 "k8s.io/apiserver/pkg/util/feature" 44 cacheddiscovery "k8s.io/client-go/discovery/cached/memory" 45 "k8s.io/client-go/dynamic" 46 "k8s.io/client-go/informers" 47 clientset "k8s.io/client-go/kubernetes" 48 policyv1client "k8s.io/client-go/kubernetes/typed/policy/v1" 49 restclient "k8s.io/client-go/rest" 50 "k8s.io/client-go/restmapper" 51 "k8s.io/client-go/scale" 52 "k8s.io/client-go/tools/cache" 53 featuregatetesting "k8s.io/component-base/featuregate/testing" 54 "k8s.io/klog/v2" 55 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 56 podutil "k8s.io/kubernetes/pkg/api/v1/pod" 57 "k8s.io/kubernetes/pkg/controller/disruption" 58 "k8s.io/kubernetes/pkg/features" 59 "k8s.io/kubernetes/test/integration/framework" 60 "k8s.io/kubernetes/test/utils/ktesting" 61 ) 62 63 const ( 64 numOfEvictions = 10 65 ) 66 67 // TestConcurrentEvictionRequests is to make sure pod disruption budgets (PDB) controller is able to 68 // handle concurrent eviction requests. Original issue:#37605 69 func TestConcurrentEvictionRequests(t *testing.T) { 70 podNameFormat := "test-pod-%d" 71 72 tCtx := ktesting.Init(t) 73 closeFn, rm, informers, _, clientSet := rmSetup(tCtx, t) 74 defer closeFn() 75 76 ns := framework.CreateNamespaceOrDie(clientSet, "concurrent-eviction-requests", t) 77 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 78 defer tCtx.Cancel("test has completed") 79 80 informers.Start(tCtx.Done()) 81 go rm.Run(tCtx) 82 83 var gracePeriodSeconds int64 = 30 84 deleteOption := metav1.DeleteOptions{ 85 GracePeriodSeconds: &gracePeriodSeconds, 86 } 87 88 // Generate numOfEvictions pods to evict 89 for i := 0; i < numOfEvictions; i++ { 90 podName := fmt.Sprintf(podNameFormat, i) 91 pod := newPod(podName) 92 93 if _, err := clientSet.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 94 t.Errorf("Failed to create pod: %v", err) 95 } 96 pod.Status.Phase = v1.PodRunning 97 addPodConditionReady(pod) 98 if _, err := clientSet.CoreV1().Pods(ns.Name).UpdateStatus(context.TODO(), pod, metav1.UpdateOptions{}); err != nil { 99 t.Fatal(err) 100 } 101 } 102 103 waitToObservePods(t, informers.Core().V1().Pods().Informer(), numOfEvictions, v1.PodRunning) 104 105 pdb := newPDB() 106 if _, err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).Create(context.TODO(), pdb, metav1.CreateOptions{}); err != nil { 107 t.Errorf("Failed to create PodDisruptionBudget: %v", err) 108 } 109 110 waitPDBStable(t, clientSet, ns.Name, pdb.Name, numOfEvictions) 111 112 var numberPodsEvicted uint32 113 errCh := make(chan error, 3*numOfEvictions) 114 var wg sync.WaitGroup 115 // spawn numOfEvictions goroutines to concurrently evict the pods 116 for i := 0; i < numOfEvictions; i++ { 117 wg.Add(1) 118 go func(id int, errCh chan error) { 119 defer wg.Done() 120 podName := fmt.Sprintf(podNameFormat, id) 121 eviction := newV1Eviction(ns.Name, podName, deleteOption) 122 123 err := wait.PollImmediate(5*time.Second, 60*time.Second, func() (bool, error) { 124 e := clientSet.PolicyV1().Evictions(ns.Name).Evict(context.TODO(), eviction) 125 switch { 126 case apierrors.IsTooManyRequests(e): 127 return false, nil 128 case apierrors.IsConflict(e): 129 return false, fmt.Errorf("Unexpected Conflict (409) error caused by failing to handle concurrent PDB updates: %v", e) 130 case e == nil: 131 return true, nil 132 default: 133 return false, e 134 } 135 }) 136 137 if err != nil { 138 errCh <- err 139 // should not return here otherwise we would leak the pod 140 } 141 142 _, err = clientSet.CoreV1().Pods(ns.Name).Get(context.TODO(), podName, metav1.GetOptions{}) 143 switch { 144 case apierrors.IsNotFound(err): 145 atomic.AddUint32(&numberPodsEvicted, 1) 146 // pod was evicted and deleted so return from goroutine immediately 147 return 148 case err == nil: 149 // this shouldn't happen if the pod was evicted successfully 150 errCh <- fmt.Errorf("Pod %q is expected to be evicted", podName) 151 default: 152 errCh <- err 153 } 154 155 // delete pod which still exists due to error 156 e := clientSet.CoreV1().Pods(ns.Name).Delete(context.TODO(), podName, deleteOption) 157 if e != nil { 158 errCh <- e 159 } 160 161 }(i, errCh) 162 } 163 164 wg.Wait() 165 166 close(errCh) 167 var errList []error 168 if err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).Delete(context.TODO(), pdb.Name, deleteOption); err != nil { 169 errList = append(errList, fmt.Errorf("Failed to delete PodDisruptionBudget: %v", err)) 170 } 171 for err := range errCh { 172 errList = append(errList, err) 173 } 174 if len(errList) > 0 { 175 t.Fatal(utilerrors.NewAggregate(errList)) 176 } 177 178 if atomic.LoadUint32(&numberPodsEvicted) != numOfEvictions { 179 t.Fatalf("fewer number of successful evictions than expected : %d", numberPodsEvicted) 180 } 181 } 182 183 // TestTerminalPodEviction ensures that PDB is not checked for terminal pods. 184 func TestTerminalPodEviction(t *testing.T) { 185 tCtx := ktesting.Init(t) 186 closeFn, rm, informers, _, clientSet := rmSetup(tCtx, t) 187 defer closeFn() 188 189 ns := framework.CreateNamespaceOrDie(clientSet, "terminalpod-eviction", t) 190 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 191 defer tCtx.Cancel("test has completed") 192 193 informers.Start(tCtx.Done()) 194 go rm.Run(tCtx) 195 196 var gracePeriodSeconds int64 = 30 197 deleteOption := metav1.DeleteOptions{ 198 GracePeriodSeconds: &gracePeriodSeconds, 199 } 200 pod := newPod("test-terminal-pod1") 201 if _, err := clientSet.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 202 t.Errorf("Failed to create pod: %v", err) 203 } 204 205 pod.Status.Phase = v1.PodSucceeded 206 addPodConditionReady(pod) 207 if _, err := clientSet.CoreV1().Pods(ns.Name).UpdateStatus(context.TODO(), pod, metav1.UpdateOptions{}); err != nil { 208 t.Fatal(err) 209 } 210 211 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 1, v1.PodSucceeded) 212 213 pdb := newPDB() 214 if _, err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).Create(context.TODO(), pdb, metav1.CreateOptions{}); err != nil { 215 t.Errorf("Failed to create PodDisruptionBudget: %v", err) 216 } 217 218 waitPDBStable(t, clientSet, ns.Name, pdb.Name, 1) 219 220 pdbList, err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).List(context.TODO(), metav1.ListOptions{}) 221 if err != nil { 222 t.Fatalf("Error while listing pod disruption budget") 223 } 224 oldPdb := pdbList.Items[0] 225 eviction := newV1Eviction(ns.Name, pod.Name, deleteOption) 226 err = wait.PollImmediate(5*time.Second, 60*time.Second, func() (bool, error) { 227 e := clientSet.PolicyV1().Evictions(ns.Name).Evict(context.TODO(), eviction) 228 switch { 229 case apierrors.IsTooManyRequests(e): 230 return false, nil 231 case apierrors.IsConflict(e): 232 return false, fmt.Errorf("Unexpected Conflict (409) error caused by failing to handle concurrent PDB updates: %v", e) 233 case e == nil: 234 return true, nil 235 default: 236 return false, e 237 } 238 }) 239 if err != nil { 240 t.Fatalf("Eviction of pod failed %v", err) 241 } 242 pdbList, err = clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).List(context.TODO(), metav1.ListOptions{}) 243 if err != nil { 244 t.Fatalf("Error while listing pod disruption budget") 245 } 246 newPdb := pdbList.Items[0] 247 // We shouldn't see an update in pod disruption budget status' generation number as we are evicting terminal pods without checking for pod disruption. 248 if !reflect.DeepEqual(newPdb.Status.ObservedGeneration, oldPdb.Status.ObservedGeneration) { 249 t.Fatalf("Expected the pdb generation to be of same value %v but got %v", newPdb.Status.ObservedGeneration, oldPdb.Status.ObservedGeneration) 250 } 251 252 if err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).Delete(context.TODO(), pdb.Name, deleteOption); err != nil { 253 t.Fatalf("Failed to delete pod disruption budget") 254 } 255 } 256 257 // TestEvictionVersions ensures the eviction endpoint accepts and returns the correct API versions 258 func TestEvictionVersions(t *testing.T) { 259 tCtx := ktesting.Init(t) 260 closeFn, rm, informers, config, clientSet := rmSetup(tCtx, t) 261 defer closeFn() 262 defer tCtx.Cancel("test has completed") 263 264 informers.Start(tCtx.Done()) 265 go rm.Run(tCtx) 266 267 ns := "default" 268 subresource := "eviction" 269 pod := newPod("test") 270 if _, err := clientSet.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 271 t.Errorf("Failed to create pod: %v", err) 272 } 273 274 dynamicClient, err := dynamic.NewForConfig(config) 275 if err != nil { 276 t.Fatalf("Failed to create clientset: %v", err) 277 } 278 279 podClient := dynamicClient.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}).Namespace(ns) 280 281 // get should not be supported 282 if _, err := podClient.Get(context.TODO(), pod.Name, metav1.GetOptions{}, subresource); !apierrors.IsMethodNotSupported(err) { 283 t.Fatalf("expected MethodNotSupported for GET, got %v", err) 284 } 285 286 // patch should not be supported 287 for _, patchType := range []types.PatchType{types.JSONPatchType, types.MergePatchType, types.StrategicMergePatchType, types.ApplyPatchType} { 288 if _, err := podClient.Patch(context.TODO(), pod.Name, patchType, []byte{}, metav1.PatchOptions{}, subresource); !apierrors.IsMethodNotSupported(err) { 289 t.Fatalf("expected MethodNotSupported for GET, got %v", err) 290 } 291 } 292 293 allowedEvictions := []runtime.Object{ 294 // v1beta1, no apiVersion/kind 295 &policyv1beta1.Eviction{ 296 TypeMeta: metav1.TypeMeta{}, 297 ObjectMeta: metav1.ObjectMeta{Name: pod.Name}, 298 DeleteOptions: &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, 299 }, 300 // v1beta1, apiVersion/kind 301 &policyv1beta1.Eviction{ 302 TypeMeta: metav1.TypeMeta{APIVersion: "policy/v1beta1", Kind: "Eviction"}, 303 ObjectMeta: metav1.ObjectMeta{Name: pod.Name}, 304 DeleteOptions: &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, 305 }, 306 // v1, no apiVersion/kind 307 &policyv1.Eviction{ 308 TypeMeta: metav1.TypeMeta{}, 309 ObjectMeta: metav1.ObjectMeta{Name: pod.Name}, 310 DeleteOptions: &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, 311 }, 312 // v1, apiVersion/kind 313 &policyv1.Eviction{ 314 TypeMeta: metav1.TypeMeta{APIVersion: "policy/v1", Kind: "Eviction"}, 315 ObjectMeta: metav1.ObjectMeta{Name: pod.Name}, 316 DeleteOptions: &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, 317 }, 318 } 319 v1Status := schema.GroupVersionKind{Version: "v1", Kind: "Status"} 320 for _, allowedEviction := range allowedEvictions { 321 data, _ := json.Marshal(allowedEviction) 322 u := &unstructured.Unstructured{} 323 json.Unmarshal(data, u) 324 result, err := podClient.Create(context.TODO(), u, metav1.CreateOptions{}, subresource) 325 if err != nil { 326 t.Fatalf("error posting %s: %v", string(data), err) 327 } 328 if result.GroupVersionKind() != v1Status { 329 t.Fatalf("expected v1 Status, got %#v", result) 330 } 331 } 332 333 // create unknown eviction version with apiVersion/kind should fail 334 u := &unstructured.Unstructured{Object: map[string]interface{}{ 335 "metadata": map[string]interface{}{"name": pod.Name}, 336 "apiVersion": "policy/v2", 337 "kind": "Eviction", 338 }} 339 if _, err := podClient.Create(context.TODO(), u, metav1.CreateOptions{}, subresource); err == nil { 340 t.Fatal("expected error posting unknown Eviction version, got none") 341 } else if !strings.Contains(err.Error(), "policy/v2") { 342 t.Fatalf("expected error about policy/v2, got %#v", err) 343 } 344 } 345 346 // TestEvictionWithFinalizers tests eviction with the use of finalizers 347 func TestEvictionWithFinalizers(t *testing.T) { 348 cases := map[string]struct { 349 enablePodDisruptionConditions bool 350 phase v1.PodPhase 351 dryRun bool 352 wantDisruptionTargetCond bool 353 }{ 354 "terminal pod with PodDisruptionConditions enabled": { 355 enablePodDisruptionConditions: true, 356 phase: v1.PodSucceeded, 357 wantDisruptionTargetCond: true, 358 }, 359 "terminal pod with PodDisruptionConditions disabled": { 360 enablePodDisruptionConditions: false, 361 phase: v1.PodSucceeded, 362 wantDisruptionTargetCond: false, 363 }, 364 "running pod with PodDisruptionConditions enabled": { 365 enablePodDisruptionConditions: true, 366 phase: v1.PodRunning, 367 wantDisruptionTargetCond: true, 368 }, 369 "running pod with PodDisruptionConditions disabled": { 370 enablePodDisruptionConditions: false, 371 phase: v1.PodRunning, 372 wantDisruptionTargetCond: false, 373 }, 374 "running pod with PodDisruptionConditions enabled should not update conditions in dry-run mode": { 375 enablePodDisruptionConditions: true, 376 phase: v1.PodRunning, 377 dryRun: true, 378 wantDisruptionTargetCond: false, 379 }, 380 } 381 for name, tc := range cases { 382 t.Run(name, func(t *testing.T) { 383 tCtx := ktesting.Init(t) 384 closeFn, rm, informers, _, clientSet := rmSetup(tCtx, t) 385 defer closeFn() 386 387 ns := framework.CreateNamespaceOrDie(clientSet, "eviction-with-finalizers", t) 388 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 389 featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.PodDisruptionConditions, tc.enablePodDisruptionConditions) 390 defer tCtx.Cancel("test has completed") 391 392 informers.Start(tCtx.Done()) 393 go rm.Run(tCtx) 394 395 pod := newPod("pod") 396 pod.ObjectMeta.Finalizers = []string{"test.k8s.io/finalizer"} 397 if _, err := clientSet.CoreV1().Pods(ns.Name).Create(tCtx, pod, metav1.CreateOptions{}); err != nil { 398 t.Errorf("Failed to create pod: %v", err) 399 } 400 401 pod.Status.Phase = tc.phase 402 addPodConditionReady(pod) 403 if _, err := clientSet.CoreV1().Pods(ns.Name).UpdateStatus(tCtx, pod, metav1.UpdateOptions{}); err != nil { 404 t.Fatal(err) 405 } 406 407 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 1, tc.phase) 408 deleteOption := metav1.DeleteOptions{} 409 if tc.dryRun { 410 deleteOption.DryRun = []string{metav1.DryRunAll} 411 } 412 413 eviction := newV1Eviction(ns.Name, pod.Name, deleteOption) 414 415 err := clientSet.PolicyV1().Evictions(ns.Name).Evict(tCtx, eviction) 416 if err != nil { 417 t.Fatalf("Eviction of pod failed %v", err) 418 } 419 420 updatedPod, e := clientSet.CoreV1().Pods(ns.Name).Get(tCtx, pod.Name, metav1.GetOptions{}) 421 if e != nil { 422 t.Fatalf("Failed to get the pod %q with error: %q", klog.KObj(pod), e) 423 } 424 _, cond := podutil.GetPodCondition(&updatedPod.Status, v1.PodConditionType(v1.DisruptionTarget)) 425 if tc.wantDisruptionTargetCond && cond == nil { 426 t.Errorf("Pod %q does not have the expected condition: %q", klog.KObj(updatedPod), v1.DisruptionTarget) 427 } else if !tc.wantDisruptionTargetCond && cond != nil { 428 t.Errorf("Pod %q has an unexpected condition: %q", klog.KObj(updatedPod), v1.DisruptionTarget) 429 } 430 }) 431 } 432 } 433 434 // TestEvictionWithUnhealthyPodEvictionPolicy tests eviction with a PDB that has a UnhealthyPodEvictionPolicy 435 func TestEvictionWithUnhealthyPodEvictionPolicy(t *testing.T) { 436 cases := map[string]struct { 437 enableUnhealthyPodEvictionPolicy bool 438 unhealthyPodEvictionPolicy *policyv1.UnhealthyPodEvictionPolicyType 439 isPodReady bool 440 }{ 441 "UnhealthyPodEvictionPolicy disabled and policy not set": { 442 enableUnhealthyPodEvictionPolicy: false, 443 unhealthyPodEvictionPolicy: nil, 444 isPodReady: true, 445 }, 446 "UnhealthyPodEvictionPolicy enabled but policy not set": { 447 enableUnhealthyPodEvictionPolicy: true, 448 unhealthyPodEvictionPolicy: nil, 449 isPodReady: true, 450 }, 451 "UnhealthyPodEvictionPolicy enabled but policy set to IfHealthyBudget with ready pod": { 452 enableUnhealthyPodEvictionPolicy: true, 453 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policyv1.IfHealthyBudget), 454 isPodReady: true, 455 }, 456 "UnhealthyPodEvictionPolicy enabled but policy set to AlwaysAllow with ready pod": { 457 enableUnhealthyPodEvictionPolicy: true, 458 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policyv1.AlwaysAllow), 459 isPodReady: true, 460 }, 461 "UnhealthyPodEvictionPolicy enabled but policy set to AlwaysAllow with unready pod": { 462 enableUnhealthyPodEvictionPolicy: true, 463 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policyv1.AlwaysAllow), 464 isPodReady: false, 465 }, 466 } 467 for name, tc := range cases { 468 t.Run(name, func(t *testing.T) { 469 tCtx := ktesting.Init(t) 470 featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.PDBUnhealthyPodEvictionPolicy, tc.enableUnhealthyPodEvictionPolicy) 471 closeFn, rm, informers, _, clientSet := rmSetup(tCtx, t) 472 defer closeFn() 473 474 ns := framework.CreateNamespaceOrDie(clientSet, "eviction-with-pdb-pod-healthy-policy", t) 475 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 476 defer tCtx.Cancel("test has completed") 477 478 informers.Start(tCtx.Done()) 479 go rm.Run(tCtx) 480 481 pod := newPod("pod") 482 if _, err := clientSet.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}); err != nil { 483 t.Errorf("Failed to create pod: %v", err) 484 } 485 486 pod.Status.Phase = v1.PodRunning 487 if tc.isPodReady { 488 addPodConditionReady(pod) 489 } 490 491 if _, err := clientSet.CoreV1().Pods(ns.Name).UpdateStatus(tCtx, pod, metav1.UpdateOptions{}); err != nil { 492 t.Fatal(err) 493 } 494 495 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 1, v1.PodRunning) 496 497 pdb := newPDB() 498 pdb.Spec.UnhealthyPodEvictionPolicy = tc.unhealthyPodEvictionPolicy 499 if _, err := clientSet.PolicyV1().PodDisruptionBudgets(ns.Name).Create(context.TODO(), pdb, metav1.CreateOptions{}); err != nil { 500 t.Errorf("Failed to create PodDisruptionBudget: %v", err) 501 } 502 503 if tc.isPodReady { 504 waitPDBStable(t, clientSet, ns.Name, pdb.Name, 1) 505 } else { 506 waitPDB(t, clientSet, ns.Name, pdb.Name, func(pdb *policyv1.PodDisruptionBudget) bool { 507 return pdb.Status.ExpectedPods == 1 508 }) 509 } 510 // Eviction API can potentially return http.StatusTooManyRequests (429) or http.StatusGatewayTimeout (504) with retryAfterSeconds == 10s 511 // Do not retry - we want to test that the first request succeeds and make sure it doesn't unnecessarily block the test for 10s 512 policyV1NoRetriesRESTClient := &noRetriesRESTClient{Interface: clientSet.PolicyV1().RESTClient()} 513 policyV1NoRetriesClient := policyv1client.New(policyV1NoRetriesRESTClient) 514 515 deleteOption := metav1.DeleteOptions{} 516 eviction := newV1Eviction(ns.Name, pod.Name, deleteOption) 517 err := policyV1NoRetriesClient.Evictions(ns.Name).Evict(tCtx, eviction) 518 if err != nil { 519 t.Fatalf("Eviction of pod failed %v", err) 520 } 521 if policyV1NoRetriesRESTClient.postCalls != 1 { 522 t.Fatalf("expected a single POST call, got %d", policyV1NoRetriesRESTClient.postCalls) 523 } 524 525 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 0, v1.PodRunning) 526 waitPDBStable(t, clientSet, ns.Name, pdb.Name, 0) 527 }) 528 } 529 } 530 531 // TestEvictionWithPrecondition tests eviction with delete preconditions 532 func TestEvictionWithPrecondition(t *testing.T) { 533 cases := map[string]struct { 534 enforceResourceVersion bool 535 injectWrongResourceVersion bool 536 enforceUID bool 537 injectWrongUID bool 538 shouldErr bool 539 }{ 540 "eviction enforcing resource version": { 541 enforceResourceVersion: true, 542 }, 543 "eviction enforcing UID": { 544 enforceUID: true, 545 }, 546 "eviction enforcing resource version and UID": { 547 enforceUID: true, 548 enforceResourceVersion: true, 549 }, 550 "eviction enforcing wrong resource version should fail": { 551 enforceResourceVersion: true, 552 injectWrongResourceVersion: true, 553 shouldErr: true, 554 }, 555 "eviction enforcing wrong UID should fail": { 556 enforceUID: true, 557 injectWrongUID: true, 558 shouldErr: true, 559 }, 560 } 561 for name, tc := range cases { 562 t.Run(name, func(t *testing.T) { 563 tCtx := ktesting.Init(t) 564 closeFn, rm, informers, _, clientSet := rmSetup(tCtx, t) 565 defer closeFn() 566 567 ns := framework.CreateNamespaceOrDie(clientSet, "eviction-with-preconditions", t) 568 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 569 570 defer tCtx.Cancel("test has completed") 571 informers.Start(tCtx.Done()) 572 go rm.Run(tCtx) 573 574 pod := newPod("pod") 575 pod, err := clientSet.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}) 576 if err != nil { 577 t.Errorf("Failed to create pod: %q", err) 578 } 579 580 pod.Status.Phase = v1.PodRunning 581 addPodConditionReady(pod) 582 583 // generate a new resource version 584 updatedPod, err := clientSet.CoreV1().Pods(ns.Name).UpdateStatus(tCtx, pod, metav1.UpdateOptions{}) 585 if err != nil { 586 t.Fatal(err) 587 } 588 589 waitToObservePods(t, informers.Core().V1().Pods().Informer(), 1, v1.PodRunning) 590 591 deleteOption := metav1.DeleteOptions{} 592 593 if tc.enforceResourceVersion || tc.enforceUID { 594 deleteOption.Preconditions = &metav1.Preconditions{} 595 } 596 597 if tc.enforceResourceVersion { 598 if tc.injectWrongResourceVersion { 599 deleteOption.Preconditions.ResourceVersion = &pod.ResourceVersion 600 } else { 601 deleteOption.Preconditions.ResourceVersion = &updatedPod.ResourceVersion 602 } 603 604 } 605 if tc.enforceUID { 606 if tc.injectWrongUID { 607 newUID := uuid.NewUUID() 608 deleteOption.Preconditions.UID = &newUID 609 } else { 610 deleteOption.Preconditions.UID = &updatedPod.UID 611 } 612 } 613 614 // Eviction API can potentially return http.StatusTooManyRequests (429) or http.StatusGatewayTimeout (504) with retryAfterSeconds == 10s 615 // Do not retry - we want to test that the first request succeeds and make sure it doesn't unnecessarily block the test for 10s 616 policyV1NoRetriesRESTClient := &noRetriesRESTClient{Interface: clientSet.PolicyV1().RESTClient()} 617 policyV1NoRetriesClient := policyv1client.New(policyV1NoRetriesRESTClient) 618 619 eviction := newV1Eviction(ns.Name, updatedPod.Name, deleteOption) 620 err = policyV1NoRetriesClient.Evictions(ns.Name).Evict(tCtx, eviction) 621 if err != nil && !tc.shouldErr { 622 t.Fatalf("Eviction of pod failed %q", err) 623 } 624 if err == nil && tc.shouldErr { 625 t.Fatal("Eviction of pod should fail") 626 } 627 if policyV1NoRetriesRESTClient.postCalls != 1 { 628 t.Fatalf("expected a single POST call, got %d", policyV1NoRetriesRESTClient.postCalls) 629 } 630 }) 631 } 632 } 633 634 func newPod(podName string) *v1.Pod { 635 return &v1.Pod{ 636 ObjectMeta: metav1.ObjectMeta{ 637 Name: podName, 638 Labels: map[string]string{"app": "test-evictions"}, 639 }, 640 Spec: v1.PodSpec{ 641 Containers: []v1.Container{ 642 { 643 Name: "fake-name", 644 Image: "fakeimage", 645 }, 646 }, 647 }, 648 } 649 } 650 651 func addPodConditionReady(pod *v1.Pod) { 652 pod.Status.Conditions = append(pod.Status.Conditions, v1.PodCondition{ 653 Type: v1.PodReady, 654 Status: v1.ConditionTrue, 655 }) 656 } 657 658 func newPDB() *policyv1.PodDisruptionBudget { 659 return &policyv1.PodDisruptionBudget{ 660 ObjectMeta: metav1.ObjectMeta{ 661 Name: "test-pdb", 662 }, 663 Spec: policyv1.PodDisruptionBudgetSpec{ 664 MinAvailable: &intstr.IntOrString{ 665 Type: intstr.Int, 666 IntVal: 0, 667 }, 668 Selector: &metav1.LabelSelector{ 669 MatchLabels: map[string]string{"app": "test-evictions"}, 670 }, 671 }, 672 } 673 } 674 675 func newV1Eviction(ns, evictionName string, deleteOption metav1.DeleteOptions) *policyv1.Eviction { 676 return &policyv1.Eviction{ 677 TypeMeta: metav1.TypeMeta{ 678 APIVersion: "policy/v1", 679 Kind: "Eviction", 680 }, 681 ObjectMeta: metav1.ObjectMeta{ 682 Name: evictionName, 683 Namespace: ns, 684 }, 685 DeleteOptions: &deleteOption, 686 } 687 } 688 689 func rmSetup(ctx context.Context, t *testing.T) (kubeapiservertesting.TearDownFunc, *disruption.DisruptionController, informers.SharedInformerFactory, *restclient.Config, clientset.Interface) { 690 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 691 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 692 693 config := restclient.CopyConfig(server.ClientConfig) 694 clientSet, err := clientset.NewForConfig(config) 695 if err != nil { 696 t.Fatalf("Error in create clientset: %v", err) 697 } 698 resyncPeriod := 12 * time.Hour 699 informers := informers.NewSharedInformerFactory(clientset.NewForConfigOrDie(restclient.AddUserAgent(config, "pdb-informers")), resyncPeriod) 700 701 client := clientset.NewForConfigOrDie(restclient.AddUserAgent(config, "disruption-controller")) 702 703 discoveryClient := cacheddiscovery.NewMemCacheClient(clientSet.Discovery()) 704 mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 705 706 scaleKindResolver := scale.NewDiscoveryScaleKindResolver(client.Discovery()) 707 scaleClient, err := scale.NewForConfig(config, mapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver) 708 if err != nil { 709 t.Fatalf("Error in create scaleClient: %v", err) 710 } 711 712 rm := disruption.NewDisruptionController( 713 ctx, 714 informers.Core().V1().Pods(), 715 informers.Policy().V1().PodDisruptionBudgets(), 716 informers.Core().V1().ReplicationControllers(), 717 informers.Apps().V1().ReplicaSets(), 718 informers.Apps().V1().Deployments(), 719 informers.Apps().V1().StatefulSets(), 720 client, 721 mapper, 722 scaleClient, 723 client.Discovery(), 724 ) 725 return server.TearDownFn, rm, informers, config, clientSet 726 } 727 728 // wait for the podInformer to observe the pods. Call this function before 729 // running the RS controller to prevent the rc manager from creating new pods 730 // rather than adopting the existing ones. 731 func waitToObservePods(t *testing.T, podInformer cache.SharedIndexInformer, podNum int, phase v1.PodPhase) { 732 if err := wait.PollImmediate(2*time.Second, 60*time.Second, func() (bool, error) { 733 objects := podInformer.GetIndexer().List() 734 if len(objects) != podNum { 735 return false, nil 736 } 737 for _, obj := range objects { 738 pod := obj.(*v1.Pod) 739 if pod.Status.Phase != phase { 740 return false, nil 741 } 742 } 743 return true, nil 744 }); err != nil { 745 t.Fatal(err) 746 } 747 } 748 749 func waitPDBStable(t *testing.T, clientSet clientset.Interface, ns, pdbName string, podNum int32) { 750 waitPDB(t, clientSet, ns, pdbName, func(pdb *policyv1.PodDisruptionBudget) bool { 751 return pdb.Status.CurrentHealthy == podNum 752 }) 753 } 754 755 func waitPDB(t *testing.T, clientSet clientset.Interface, ns, pdbName string, condition func(budget *policyv1.PodDisruptionBudget) bool) { 756 if err := wait.PollImmediate(2*time.Second, 60*time.Second, func() (bool, error) { 757 pdb, err := clientSet.PolicyV1().PodDisruptionBudgets(ns).Get(context.TODO(), pdbName, metav1.GetOptions{}) 758 if err != nil { 759 return false, err 760 } 761 return condition(pdb), nil 762 }); err != nil { 763 t.Fatal(err) 764 } 765 } 766 767 func unhealthyPolicyPtr(unhealthyPodEvictionPolicy policyv1.UnhealthyPodEvictionPolicyType) *policyv1.UnhealthyPodEvictionPolicyType { 768 return &unhealthyPodEvictionPolicy 769 } 770 771 type noRetriesRESTClient struct { 772 mu sync.Mutex 773 postCalls int 774 restclient.Interface 775 } 776 777 func (n *noRetriesRESTClient) Post() *restclient.Request { 778 n.mu.Lock() 779 defer n.mu.Unlock() 780 n.postCalls++ 781 return n.Interface.Post().MaxRetries(0) 782 }