k8s.io/kubernetes@v1.29.3/pkg/controller/volume/ephemeral/controller_test.go (about)

     1  /*
     2  Copyright 2020 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 ephemeral
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"sort"
    23  	"testing"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/client-go/informers"
    31  	"k8s.io/client-go/kubernetes/fake"
    32  	k8stesting "k8s.io/client-go/testing"
    33  	"k8s.io/client-go/tools/cache"
    34  	"k8s.io/component-base/metrics/testutil"
    35  	"k8s.io/klog/v2"
    36  	"k8s.io/kubernetes/pkg/controller"
    37  	ephemeralvolumemetrics "k8s.io/kubernetes/pkg/controller/volume/ephemeral/metrics"
    38  
    39  	"github.com/stretchr/testify/assert"
    40  )
    41  
    42  var (
    43  	testPodName         = "test-pod"
    44  	testNamespace       = "my-namespace"
    45  	testPodUID          = types.UID("uidpod1")
    46  	otherNamespace      = "not-my-namespace"
    47  	ephemeralVolumeName = "ephemeral-volume"
    48  
    49  	testPod               = makePod(testPodName, testNamespace, testPodUID)
    50  	testPodWithEphemeral  = makePod(testPodName, testNamespace, testPodUID, *makeEphemeralVolume(ephemeralVolumeName))
    51  	testPodEphemeralClaim = makePVC(testPodName+"-"+ephemeralVolumeName, testNamespace, makeOwnerReference(testPodWithEphemeral, true))
    52  	conflictingClaim      = makePVC(testPodName+"-"+ephemeralVolumeName, testNamespace, nil)
    53  	otherNamespaceClaim   = makePVC(testPodName+"-"+ephemeralVolumeName, otherNamespace, nil)
    54  )
    55  
    56  func init() {
    57  	klog.InitFlags(nil)
    58  }
    59  
    60  func TestSyncHandler(t *testing.T) {
    61  	tests := []struct {
    62  		name            string
    63  		podKey          string
    64  		pvcs            []*v1.PersistentVolumeClaim
    65  		pods            []*v1.Pod
    66  		expectedPVCs    []v1.PersistentVolumeClaim
    67  		expectedError   bool
    68  		expectedMetrics expectedMetrics
    69  	}{
    70  		{
    71  			name:            "create",
    72  			pods:            []*v1.Pod{testPodWithEphemeral},
    73  			podKey:          podKey(testPodWithEphemeral),
    74  			expectedPVCs:    []v1.PersistentVolumeClaim{*testPodEphemeralClaim},
    75  			expectedMetrics: expectedMetrics{1, 0},
    76  		},
    77  		{
    78  			name:   "no-such-pod",
    79  			podKey: podKey(testPodWithEphemeral),
    80  		},
    81  		{
    82  			name: "pod-deleted",
    83  			pods: func() []*v1.Pod {
    84  				deleted := metav1.Now()
    85  				pods := []*v1.Pod{testPodWithEphemeral.DeepCopy()}
    86  				pods[0].DeletionTimestamp = &deleted
    87  				return pods
    88  			}(),
    89  			podKey: podKey(testPodWithEphemeral),
    90  		},
    91  		{
    92  			name:   "no-volumes",
    93  			pods:   []*v1.Pod{testPod},
    94  			podKey: podKey(testPod),
    95  		},
    96  		{
    97  			name:            "create-with-other-PVC",
    98  			pods:            []*v1.Pod{testPodWithEphemeral},
    99  			podKey:          podKey(testPodWithEphemeral),
   100  			pvcs:            []*v1.PersistentVolumeClaim{otherNamespaceClaim},
   101  			expectedPVCs:    []v1.PersistentVolumeClaim{*otherNamespaceClaim, *testPodEphemeralClaim},
   102  			expectedMetrics: expectedMetrics{1, 0},
   103  		},
   104  		{
   105  			name:          "wrong-PVC-owner",
   106  			pods:          []*v1.Pod{testPodWithEphemeral},
   107  			podKey:        podKey(testPodWithEphemeral),
   108  			pvcs:          []*v1.PersistentVolumeClaim{conflictingClaim},
   109  			expectedPVCs:  []v1.PersistentVolumeClaim{*conflictingClaim},
   110  			expectedError: true,
   111  		},
   112  		{
   113  			name:            "create-conflict",
   114  			pods:            []*v1.Pod{testPodWithEphemeral},
   115  			podKey:          podKey(testPodWithEphemeral),
   116  			expectedMetrics: expectedMetrics{1, 1},
   117  			expectedError:   true,
   118  		},
   119  	}
   120  
   121  	for _, tc := range tests {
   122  		// Run sequentially because of global logging and global metrics.
   123  		t.Run(tc.name, func(t *testing.T) {
   124  			// There is no good way to shut down the informers. They spawn
   125  			// various goroutines and some of them (in particular shared informer)
   126  			// become very unhappy ("close on closed channel") when using a context
   127  			// that gets cancelled. Therefore we just keep everything running.
   128  			ctx := context.Background()
   129  
   130  			var objects []runtime.Object
   131  			for _, pod := range tc.pods {
   132  				objects = append(objects, pod)
   133  			}
   134  			for _, pvc := range tc.pvcs {
   135  				objects = append(objects, pvc)
   136  			}
   137  
   138  			fakeKubeClient := createTestClient(objects...)
   139  			if tc.expectedMetrics.numFailures > 0 {
   140  				fakeKubeClient.PrependReactor("create", "persistentvolumeclaims", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
   141  					return true, nil, apierrors.NewConflict(action.GetResource().GroupResource(), "fake name", errors.New("fake conflict"))
   142  				})
   143  			}
   144  			setupMetrics()
   145  			informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
   146  			podInformer := informerFactory.Core().V1().Pods()
   147  			pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
   148  
   149  			c, err := NewController(fakeKubeClient, podInformer, pvcInformer)
   150  			if err != nil {
   151  				t.Fatalf("error creating ephemeral controller : %v", err)
   152  			}
   153  			ec, _ := c.(*ephemeralController)
   154  
   155  			// Ensure informers are up-to-date.
   156  			go informerFactory.Start(ctx.Done())
   157  			informerFactory.WaitForCacheSync(ctx.Done())
   158  			cache.WaitForCacheSync(ctx.Done(), podInformer.Informer().HasSynced, pvcInformer.Informer().HasSynced)
   159  
   160  			err = ec.syncHandler(context.TODO(), tc.podKey)
   161  			if err != nil && !tc.expectedError {
   162  				t.Fatalf("unexpected error while running handler: %v", err)
   163  			}
   164  			if err == nil && tc.expectedError {
   165  				t.Fatalf("unexpected success")
   166  			}
   167  
   168  			pvcs, err := fakeKubeClient.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{})
   169  			if err != nil {
   170  				t.Fatalf("unexpected error while listing PVCs: %v", err)
   171  			}
   172  			assert.Equal(t, sortPVCs(tc.expectedPVCs), sortPVCs(pvcs.Items))
   173  			expectMetrics(t, tc.expectedMetrics)
   174  		})
   175  	}
   176  }
   177  
   178  func makePVC(name, namespace string, owner *metav1.OwnerReference) *v1.PersistentVolumeClaim {
   179  	pvc := &v1.PersistentVolumeClaim{
   180  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
   181  		Spec:       v1.PersistentVolumeClaimSpec{},
   182  	}
   183  	if owner != nil {
   184  		pvc.OwnerReferences = []metav1.OwnerReference{*owner}
   185  	}
   186  
   187  	return pvc
   188  }
   189  
   190  func makeEphemeralVolume(name string) *v1.Volume {
   191  	return &v1.Volume{
   192  		Name: name,
   193  		VolumeSource: v1.VolumeSource{
   194  			Ephemeral: &v1.EphemeralVolumeSource{
   195  				VolumeClaimTemplate: &v1.PersistentVolumeClaimTemplate{},
   196  			},
   197  		},
   198  	}
   199  }
   200  
   201  func makePod(name, namespace string, uid types.UID, volumes ...v1.Volume) *v1.Pod {
   202  	pvc := &v1.Pod{
   203  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: uid},
   204  		Spec: v1.PodSpec{
   205  			Volumes: volumes,
   206  		},
   207  	}
   208  
   209  	return pvc
   210  }
   211  
   212  func podKey(pod *v1.Pod) string {
   213  	key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(testPodWithEphemeral)
   214  	return key
   215  }
   216  
   217  func makeOwnerReference(pod *v1.Pod, isController bool) *metav1.OwnerReference {
   218  	isTrue := true
   219  	return &metav1.OwnerReference{
   220  		APIVersion:         "v1",
   221  		Kind:               "Pod",
   222  		Name:               pod.Name,
   223  		UID:                pod.UID,
   224  		Controller:         &isController,
   225  		BlockOwnerDeletion: &isTrue,
   226  	}
   227  }
   228  
   229  func sortPVCs(pvcs []v1.PersistentVolumeClaim) []v1.PersistentVolumeClaim {
   230  	sort.Slice(pvcs, func(i, j int) bool {
   231  		return pvcs[i].Namespace < pvcs[j].Namespace ||
   232  			pvcs[i].Name < pvcs[j].Name
   233  	})
   234  	return pvcs
   235  }
   236  
   237  func createTestClient(objects ...runtime.Object) *fake.Clientset {
   238  	fakeClient := fake.NewSimpleClientset(objects...)
   239  	return fakeClient
   240  }
   241  
   242  // Metrics helpers
   243  
   244  type expectedMetrics struct {
   245  	numCreated  int
   246  	numFailures int
   247  }
   248  
   249  func expectMetrics(t *testing.T, em expectedMetrics) {
   250  	t.Helper()
   251  
   252  	actualCreated, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.EphemeralVolumeCreateAttempts)
   253  	handleErr(t, err, "ephemeralVolumeCreate")
   254  	if actualCreated != float64(em.numCreated) {
   255  		t.Errorf("Expected PVCs to be created %d, got %v", em.numCreated, actualCreated)
   256  	}
   257  	actualConflicts, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.EphemeralVolumeCreateFailures)
   258  	handleErr(t, err, "ephemeralVolumeCreate/Conflict")
   259  	if actualConflicts != float64(em.numFailures) {
   260  		t.Errorf("Expected PVCs to have conflicts %d, got %v", em.numFailures, actualConflicts)
   261  	}
   262  }
   263  
   264  func handleErr(t *testing.T, err error, metricName string) {
   265  	if err != nil {
   266  		t.Errorf("Failed to get %s value, err: %v", metricName, err)
   267  	}
   268  }
   269  
   270  func setupMetrics() {
   271  	ephemeralvolumemetrics.RegisterMetrics()
   272  	ephemeralvolumemetrics.EphemeralVolumeCreateAttempts.Reset()
   273  	ephemeralvolumemetrics.EphemeralVolumeCreateFailures.Reset()
   274  }