k8s.io/kubernetes@v1.29.3/pkg/controller/volume/pvcprotection/pvc_protection_controller_test.go (about)

     1  /*
     2  Copyright 2017 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 pvcprotection
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/apimachinery/pkg/util/dump"
    34  	"k8s.io/client-go/informers"
    35  	"k8s.io/client-go/kubernetes/fake"
    36  	clienttesting "k8s.io/client-go/testing"
    37  	"k8s.io/klog/v2/ktesting"
    38  	"k8s.io/kubernetes/pkg/controller"
    39  	volumeutil "k8s.io/kubernetes/pkg/volume/util"
    40  )
    41  
    42  type reaction struct {
    43  	verb      string
    44  	resource  string
    45  	reactorfn clienttesting.ReactionFunc
    46  }
    47  
    48  const (
    49  	defaultNS       = "default"
    50  	defaultPVCName  = "pvc1"
    51  	defaultPodName  = "pod1"
    52  	defaultNodeName = "node1"
    53  	defaultUID      = "uid1"
    54  )
    55  
    56  func pod() *v1.Pod {
    57  	return &v1.Pod{
    58  		ObjectMeta: metav1.ObjectMeta{
    59  			Name:      defaultPodName,
    60  			Namespace: defaultNS,
    61  			UID:       defaultUID,
    62  		},
    63  		Spec: v1.PodSpec{
    64  			NodeName: defaultNodeName,
    65  		},
    66  		Status: v1.PodStatus{
    67  			Phase: v1.PodPending,
    68  		},
    69  	}
    70  }
    71  
    72  func unscheduled(pod *v1.Pod) *v1.Pod {
    73  	pod.Spec.NodeName = ""
    74  	return pod
    75  }
    76  
    77  func withPVC(pvcName string, pod *v1.Pod) *v1.Pod {
    78  	volume := v1.Volume{
    79  		Name: pvcName,
    80  		VolumeSource: v1.VolumeSource{
    81  			PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
    82  				ClaimName: pvcName,
    83  			},
    84  		},
    85  	}
    86  	pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
    87  	return pod
    88  }
    89  
    90  func withEmptyDir(pod *v1.Pod) *v1.Pod {
    91  	volume := v1.Volume{
    92  		Name: "emptyDir",
    93  		VolumeSource: v1.VolumeSource{
    94  			EmptyDir: &v1.EmptyDirVolumeSource{},
    95  		},
    96  	}
    97  	pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
    98  	return pod
    99  }
   100  
   101  func withStatus(phase v1.PodPhase, pod *v1.Pod) *v1.Pod {
   102  	pod.Status.Phase = phase
   103  	return pod
   104  }
   105  
   106  func withUID(uid types.UID, pod *v1.Pod) *v1.Pod {
   107  	pod.ObjectMeta.UID = uid
   108  	return pod
   109  }
   110  
   111  func pvc() *v1.PersistentVolumeClaim {
   112  	return &v1.PersistentVolumeClaim{
   113  		ObjectMeta: metav1.ObjectMeta{
   114  			Name:      defaultPVCName,
   115  			Namespace: defaultNS,
   116  		},
   117  	}
   118  }
   119  
   120  func withProtectionFinalizer(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
   121  	pvc.Finalizers = append(pvc.Finalizers, volumeutil.PVCProtectionFinalizer)
   122  	return pvc
   123  }
   124  
   125  func deleted(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim {
   126  	pvc.DeletionTimestamp = &metav1.Time{}
   127  	return pvc
   128  }
   129  
   130  func generateUpdateErrorFunc(t *testing.T, failures int) clienttesting.ReactionFunc {
   131  	i := 0
   132  	return func(action clienttesting.Action) (bool, runtime.Object, error) {
   133  		i++
   134  		if i <= failures {
   135  			// Update fails
   136  			update, ok := action.(clienttesting.UpdateAction)
   137  
   138  			if !ok {
   139  				t.Fatalf("Reactor got non-update action: %+v", action)
   140  			}
   141  			acc, _ := meta.Accessor(update.GetObject())
   142  			return true, nil, apierrors.NewForbidden(update.GetResource().GroupResource(), acc.GetName(), errors.New("Mock error"))
   143  		}
   144  		// Update succeeds
   145  		return false, nil, nil
   146  	}
   147  }
   148  
   149  func TestPVCProtectionController(t *testing.T) {
   150  	pvcGVR := schema.GroupVersionResource{
   151  		Group:    v1.GroupName,
   152  		Version:  "v1",
   153  		Resource: "persistentvolumeclaims",
   154  	}
   155  	podGVR := schema.GroupVersionResource{
   156  		Group:    v1.GroupName,
   157  		Version:  "v1",
   158  		Resource: "pods",
   159  	}
   160  	podGVK := schema.GroupVersionKind{
   161  		Group:   v1.GroupName,
   162  		Version: "v1",
   163  		Kind:    "Pod",
   164  	}
   165  
   166  	tests := []struct {
   167  		name string
   168  		// Object to insert into fake kubeclient before the test starts.
   169  		initialObjects []runtime.Object
   170  		// Whether not to insert the content of initialObjects into the
   171  		// informers before the test starts. Set it to true to simulate the case
   172  		// where informers have not been notified yet of certain API objects.
   173  		informersAreLate bool
   174  		// Optional client reactors.
   175  		reactors []reaction
   176  		// PVC event to simulate. This PVC will be automatically added to
   177  		// initialObjects.
   178  		updatedPVC *v1.PersistentVolumeClaim
   179  		// Pod event to simulate. This Pod will be automatically added to
   180  		// initialObjects.
   181  		updatedPod *v1.Pod
   182  		// Pod event to simulate. This Pod is *not* added to
   183  		// initialObjects.
   184  		deletedPod *v1.Pod
   185  		// List of expected kubeclient actions that should happen during the
   186  		// test.
   187  		expectedActions []clienttesting.Action
   188  	}{
   189  		//
   190  		// PVC events
   191  		//
   192  		{
   193  			name:       "PVC without finalizer -> finalizer is added",
   194  			updatedPVC: pvc(),
   195  			expectedActions: []clienttesting.Action{
   196  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())),
   197  			},
   198  		},
   199  		{
   200  			name:            "PVC with finalizer -> no action",
   201  			updatedPVC:      withProtectionFinalizer(pvc()),
   202  			expectedActions: []clienttesting.Action{},
   203  		},
   204  		{
   205  			name:       "saving PVC finalizer fails -> controller retries",
   206  			updatedPVC: pvc(),
   207  			reactors: []reaction{
   208  				{
   209  					verb:      "update",
   210  					resource:  "persistentvolumeclaims",
   211  					reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
   212  				},
   213  			},
   214  			expectedActions: []clienttesting.Action{
   215  				// This fails
   216  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())),
   217  				// This fails too
   218  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())),
   219  				// This succeeds
   220  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())),
   221  			},
   222  		},
   223  		{
   224  			name:       "deleted PVC with finalizer -> finalizer is removed",
   225  			updatedPVC: deleted(withProtectionFinalizer(pvc())),
   226  			expectedActions: []clienttesting.Action{
   227  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   228  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   229  			},
   230  		},
   231  		{
   232  			name:       "finalizer removal fails -> controller retries",
   233  			updatedPVC: deleted(withProtectionFinalizer(pvc())),
   234  			reactors: []reaction{
   235  				{
   236  					verb:      "update",
   237  					resource:  "persistentvolumeclaims",
   238  					reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/),
   239  				},
   240  			},
   241  			expectedActions: []clienttesting.Action{
   242  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   243  				// Fails
   244  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   245  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   246  				// Fails too
   247  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   248  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   249  				// Succeeds
   250  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   251  			},
   252  		},
   253  		{
   254  			name: "deleted PVC with finalizer + pod with the PVC exists -> finalizer is not removed",
   255  			initialObjects: []runtime.Object{
   256  				withPVC(defaultPVCName, pod()),
   257  			},
   258  			updatedPVC:      deleted(withProtectionFinalizer(pvc())),
   259  			expectedActions: []clienttesting.Action{},
   260  		},
   261  		{
   262  			name: "deleted PVC with finalizer + pod with unrelated PVC and EmptyDir exists -> finalizer is removed",
   263  			initialObjects: []runtime.Object{
   264  				withEmptyDir(withPVC("unrelatedPVC", pod())),
   265  			},
   266  			updatedPVC: deleted(withProtectionFinalizer(pvc())),
   267  			expectedActions: []clienttesting.Action{
   268  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   269  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   270  			},
   271  		},
   272  		{
   273  			name: "deleted PVC with finalizer + pod with the PVC finished but is not deleted -> finalizer is not removed",
   274  			initialObjects: []runtime.Object{
   275  				withStatus(v1.PodFailed, withPVC(defaultPVCName, pod())),
   276  			},
   277  			updatedPVC:      deleted(withProtectionFinalizer(pvc())),
   278  			expectedActions: []clienttesting.Action{},
   279  		},
   280  		{
   281  			name: "deleted PVC with finalizer + pod with the PVC exists but is not in the Informer's cache yet -> finalizer is not removed",
   282  			initialObjects: []runtime.Object{
   283  				withPVC(defaultPVCName, pod()),
   284  			},
   285  			informersAreLate: true,
   286  			updatedPVC:       deleted(withProtectionFinalizer(pvc())),
   287  			expectedActions: []clienttesting.Action{
   288  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   289  			},
   290  		},
   291  		//
   292  		// Pod events
   293  		//
   294  		{
   295  			name: "updated running Pod -> no action",
   296  			initialObjects: []runtime.Object{
   297  				deleted(withProtectionFinalizer(pvc())),
   298  			},
   299  			updatedPod:      withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())),
   300  			expectedActions: []clienttesting.Action{},
   301  		},
   302  		{
   303  			name: "updated finished Pod -> finalizer is not removed",
   304  			initialObjects: []runtime.Object{
   305  				deleted(withProtectionFinalizer(pvc())),
   306  			},
   307  			updatedPod:      withStatus(v1.PodSucceeded, withPVC(defaultPVCName, pod())),
   308  			expectedActions: []clienttesting.Action{},
   309  		},
   310  		{
   311  			name: "updated unscheduled Pod -> finalizer is removed",
   312  			initialObjects: []runtime.Object{
   313  				deleted(withProtectionFinalizer(pvc())),
   314  			},
   315  			updatedPod: unscheduled(withPVC(defaultPVCName, pod())),
   316  			expectedActions: []clienttesting.Action{
   317  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   318  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   319  			},
   320  		},
   321  		{
   322  			name: "deleted running Pod -> finalizer is removed",
   323  			initialObjects: []runtime.Object{
   324  				deleted(withProtectionFinalizer(pvc())),
   325  			},
   326  			deletedPod: withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())),
   327  			expectedActions: []clienttesting.Action{
   328  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   329  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   330  			},
   331  		},
   332  		{
   333  			name: "pod delete and create with same namespaced name seen as an update, old pod used deleted PVC -> finalizer is removed",
   334  			initialObjects: []runtime.Object{
   335  				deleted(withProtectionFinalizer(pvc())),
   336  			},
   337  			deletedPod: withPVC(defaultPVCName, pod()),
   338  			updatedPod: withUID("uid2", pod()),
   339  			expectedActions: []clienttesting.Action{
   340  				clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}),
   341  				clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())),
   342  			},
   343  		},
   344  		{
   345  			name: "pod delete and create with same namespaced name seen as an update, old pod used non-deleted PVC -> finalizer is not removed",
   346  			initialObjects: []runtime.Object{
   347  				withProtectionFinalizer(pvc()),
   348  			},
   349  			deletedPod:      withPVC(defaultPVCName, pod()),
   350  			updatedPod:      withUID("uid2", pod()),
   351  			expectedActions: []clienttesting.Action{},
   352  		},
   353  		{
   354  			name: "pod delete and create with same namespaced name seen as an update, both pods reference deleted PVC -> finalizer is not removed",
   355  			initialObjects: []runtime.Object{
   356  				deleted(withProtectionFinalizer(pvc())),
   357  			},
   358  			deletedPod:      withPVC(defaultPVCName, pod()),
   359  			updatedPod:      withUID("uid2", withPVC(defaultPVCName, pod())),
   360  			expectedActions: []clienttesting.Action{},
   361  		},
   362  		{
   363  			name: "pod update from unscheduled to scheduled, deleted PVC is referenced -> finalizer is not removed",
   364  			initialObjects: []runtime.Object{
   365  				deleted(withProtectionFinalizer(pvc())),
   366  			},
   367  			deletedPod:      unscheduled(withPVC(defaultPVCName, pod())),
   368  			updatedPod:      withPVC(defaultPVCName, pod()),
   369  			expectedActions: []clienttesting.Action{},
   370  		},
   371  	}
   372  
   373  	for _, test := range tests {
   374  		// Create initial data for client and informers.
   375  		var (
   376  			clientObjs    []runtime.Object
   377  			informersObjs []runtime.Object
   378  		)
   379  		if test.updatedPVC != nil {
   380  			clientObjs = append(clientObjs, test.updatedPVC)
   381  			informersObjs = append(informersObjs, test.updatedPVC)
   382  		}
   383  		if test.updatedPod != nil {
   384  			clientObjs = append(clientObjs, test.updatedPod)
   385  			informersObjs = append(informersObjs, test.updatedPod)
   386  		}
   387  		clientObjs = append(clientObjs, test.initialObjects...)
   388  		if !test.informersAreLate {
   389  			informersObjs = append(informersObjs, test.initialObjects...)
   390  		}
   391  
   392  		// Create client with initial data
   393  		client := fake.NewSimpleClientset(clientObjs...)
   394  
   395  		// Create informers
   396  		informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
   397  		pvcInformer := informers.Core().V1().PersistentVolumeClaims()
   398  		podInformer := informers.Core().V1().Pods()
   399  
   400  		// Create the controller
   401  		logger, _ := ktesting.NewTestContext(t)
   402  		ctrl, err := NewPVCProtectionController(logger, pvcInformer, podInformer, client)
   403  		if err != nil {
   404  			t.Fatalf("unexpected error: %v", err)
   405  		}
   406  
   407  		// Populate the informers with initial objects so the controller can
   408  		// Get() and List() it.
   409  		for _, obj := range informersObjs {
   410  			switch obj.(type) {
   411  			case *v1.PersistentVolumeClaim:
   412  				pvcInformer.Informer().GetStore().Add(obj)
   413  			case *v1.Pod:
   414  				podInformer.Informer().GetStore().Add(obj)
   415  			default:
   416  				t.Fatalf("Unknown initalObject type: %+v", obj)
   417  			}
   418  		}
   419  
   420  		// Add reactor to inject test errors.
   421  		for _, reactor := range test.reactors {
   422  			client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorfn)
   423  		}
   424  
   425  		// Start the test by simulating an event
   426  		if test.updatedPVC != nil {
   427  			ctrl.pvcAddedUpdated(logger, test.updatedPVC)
   428  		}
   429  		switch {
   430  		case test.deletedPod != nil && test.updatedPod != nil && test.deletedPod.Namespace == test.updatedPod.Namespace && test.deletedPod.Name == test.updatedPod.Name:
   431  			ctrl.podAddedDeletedUpdated(logger, test.deletedPod, test.updatedPod, false)
   432  		case test.updatedPod != nil:
   433  			ctrl.podAddedDeletedUpdated(logger, nil, test.updatedPod, false)
   434  		case test.deletedPod != nil:
   435  			ctrl.podAddedDeletedUpdated(logger, nil, test.deletedPod, true)
   436  		}
   437  
   438  		// Process the controller queue until we get expected results
   439  		timeout := time.Now().Add(10 * time.Second)
   440  		lastReportedActionCount := 0
   441  		for {
   442  			if time.Now().After(timeout) {
   443  				t.Errorf("Test %q: timed out", test.name)
   444  				break
   445  			}
   446  			if ctrl.queue.Len() > 0 {
   447  				logger.V(5).Info("Non-empty queue, processing one", "test", test.name, "queueLength", ctrl.queue.Len())
   448  				ctrl.processNextWorkItem(context.TODO())
   449  			}
   450  			if ctrl.queue.Len() > 0 {
   451  				// There is still some work in the queue, process it now
   452  				continue
   453  			}
   454  			currentActionCount := len(client.Actions())
   455  			if currentActionCount < len(test.expectedActions) {
   456  				// Do not log every wait, only when the action count changes.
   457  				if lastReportedActionCount < currentActionCount {
   458  					logger.V(5).Info("Waiting for the remaining actions", "test", test.name, "currentActionCount", currentActionCount, "expectedActionCount", len(test.expectedActions))
   459  					lastReportedActionCount = currentActionCount
   460  				}
   461  				// The test expected more to happen, wait for the actions.
   462  				// Most probably it's exponential backoff
   463  				time.Sleep(10 * time.Millisecond)
   464  				continue
   465  			}
   466  			break
   467  		}
   468  		actions := client.Actions()
   469  		for i, action := range actions {
   470  			if len(test.expectedActions) < i+1 {
   471  				t.Errorf("Test %q: %d unexpected actions: %+v", test.name, len(actions)-len(test.expectedActions), dump.Pretty(actions[i:]))
   472  				break
   473  			}
   474  
   475  			expectedAction := test.expectedActions[i]
   476  			if !reflect.DeepEqual(expectedAction, action) {
   477  				t.Errorf("Test %q: action %d\nExpected:\n%s\ngot:\n%s", test.name, i, dump.Pretty(expectedAction), dump.Pretty(action))
   478  			}
   479  		}
   480  
   481  		if len(test.expectedActions) > len(actions) {
   482  			t.Errorf("Test %q: %d additional expected actions", test.name, len(test.expectedActions)-len(actions))
   483  			for _, a := range test.expectedActions[len(actions):] {
   484  				t.Logf("    %+v", a)
   485  			}
   486  		}
   487  
   488  	}
   489  }