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  }