...

Source file src/k8s.io/kubernetes/pkg/controller/resourceclaim/controller_test.go

Documentation: k8s.io/kubernetes/pkg/controller/resourceclaim

     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 resourceclaim
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"sync"
    25  	"testing"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  
    29  	v1 "k8s.io/api/core/v1"
    30  	resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
    31  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/types"
    35  	"k8s.io/client-go/informers"
    36  	"k8s.io/client-go/kubernetes/fake"
    37  	k8stesting "k8s.io/client-go/testing"
    38  	"k8s.io/client-go/tools/cache"
    39  	"k8s.io/component-base/metrics/testutil"
    40  	"k8s.io/klog/v2"
    41  	"k8s.io/kubernetes/pkg/controller"
    42  	ephemeralvolumemetrics "k8s.io/kubernetes/pkg/controller/resourceclaim/metrics"
    43  	"k8s.io/utils/pointer"
    44  )
    45  
    46  var (
    47  	testPodName          = "test-pod"
    48  	testNamespace        = "my-namespace"
    49  	testPodUID           = types.UID("uidpod1")
    50  	otherNamespace       = "not-my-namespace"
    51  	podResourceClaimName = "acme-resource"
    52  	templateName         = "my-template"
    53  	className            = "my-resource-class"
    54  	nodeName             = "worker"
    55  
    56  	testPod             = makePod(testPodName, testNamespace, testPodUID)
    57  	testPodWithResource = makePod(testPodName, testNamespace, testPodUID, *makePodResourceClaim(podResourceClaimName, templateName))
    58  	otherTestPod        = makePod(testPodName+"-II", testNamespace, testPodUID+"-II")
    59  
    60  	testClaim              = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, className, makeOwnerReference(testPodWithResource, true))
    61  	testClaimAllocated     = allocateClaim(testClaim)
    62  	testClaimReserved      = reserveClaim(testClaimAllocated, testPodWithResource)
    63  	testClaimReservedTwice = reserveClaim(testClaimReserved, otherTestPod)
    64  
    65  	generatedTestClaim          = makeGeneratedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, className, 1, makeOwnerReference(testPodWithResource, true))
    66  	generatedTestClaimAllocated = allocateClaim(generatedTestClaim)
    67  	generatedTestClaimReserved  = reserveClaim(generatedTestClaimAllocated, testPodWithResource)
    68  
    69  	conflictingClaim    = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, className, nil)
    70  	otherNamespaceClaim = makeClaim(testPodName+"-"+podResourceClaimName, otherNamespace, className, nil)
    71  	template            = makeTemplate(templateName, testNamespace, className)
    72  
    73  	testPodWithNodeName = func() *v1.Pod {
    74  		pod := testPodWithResource.DeepCopy()
    75  		pod.Spec.NodeName = nodeName
    76  		pod.Status.ResourceClaimStatuses = append(pod.Status.ResourceClaimStatuses, v1.PodResourceClaimStatus{
    77  			Name:              pod.Spec.ResourceClaims[0].Name,
    78  			ResourceClaimName: &generatedTestClaim.Name,
    79  		})
    80  		return pod
    81  	}()
    82  
    83  	podSchedulingContext = resourcev1alpha2.PodSchedulingContext{
    84  		ObjectMeta: metav1.ObjectMeta{
    85  			Name:      testPodName,
    86  			Namespace: testNamespace,
    87  			OwnerReferences: []metav1.OwnerReference{
    88  				{
    89  					APIVersion: "v1",
    90  					Kind:       "Pod",
    91  					Name:       testPodName,
    92  					UID:        testPodUID,
    93  					Controller: pointer.Bool(true),
    94  				},
    95  			},
    96  		},
    97  		Spec: resourcev1alpha2.PodSchedulingContextSpec{
    98  			SelectedNode: nodeName,
    99  		},
   100  	}
   101  )
   102  
   103  func init() {
   104  	klog.InitFlags(nil)
   105  }
   106  
   107  func TestSyncHandler(t *testing.T) {
   108  	tests := []struct {
   109  		name                          string
   110  		key                           string
   111  		claims                        []*resourcev1alpha2.ResourceClaim
   112  		claimsInCache                 []*resourcev1alpha2.ResourceClaim
   113  		pods                          []*v1.Pod
   114  		podsLater                     []*v1.Pod
   115  		templates                     []*resourcev1alpha2.ResourceClaimTemplate
   116  		expectedClaims                []resourcev1alpha2.ResourceClaim
   117  		expectedPodSchedulingContexts []resourcev1alpha2.PodSchedulingContext
   118  		expectedStatuses              map[string][]v1.PodResourceClaimStatus
   119  		expectedError                 bool
   120  		expectedMetrics               expectedMetrics
   121  	}{
   122  		{
   123  			name:           "create",
   124  			pods:           []*v1.Pod{testPodWithResource},
   125  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   126  			key:            podKey(testPodWithResource),
   127  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaim},
   128  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   129  				testPodWithResource.Name: {
   130  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   131  				},
   132  			},
   133  			expectedMetrics: expectedMetrics{1, 0},
   134  		},
   135  		{
   136  			name: "nop",
   137  			pods: []*v1.Pod{func() *v1.Pod {
   138  				pod := testPodWithResource.DeepCopy()
   139  				pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
   140  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   141  				}
   142  				return pod
   143  			}()},
   144  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   145  			key:            podKey(testPodWithResource),
   146  			claims:         []*resourcev1alpha2.ResourceClaim{generatedTestClaim},
   147  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaim},
   148  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   149  				testPodWithResource.Name: {
   150  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   151  				},
   152  			},
   153  			expectedMetrics: expectedMetrics{0, 0},
   154  		},
   155  		{
   156  			name: "recreate",
   157  			pods: []*v1.Pod{func() *v1.Pod {
   158  				pod := testPodWithResource.DeepCopy()
   159  				pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
   160  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   161  				}
   162  				return pod
   163  			}()},
   164  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   165  			key:            podKey(testPodWithResource),
   166  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaim},
   167  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   168  				testPodWithResource.Name: {
   169  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   170  				},
   171  			},
   172  			expectedMetrics: expectedMetrics{1, 0},
   173  		},
   174  		{
   175  			name:          "missing-template",
   176  			pods:          []*v1.Pod{testPodWithResource},
   177  			templates:     nil,
   178  			key:           podKey(testPodWithResource),
   179  			expectedError: true,
   180  		},
   181  		{
   182  			name:           "find-existing-claim-by-label",
   183  			pods:           []*v1.Pod{testPodWithResource},
   184  			key:            podKey(testPodWithResource),
   185  			claims:         []*resourcev1alpha2.ResourceClaim{generatedTestClaim},
   186  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaim},
   187  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   188  				testPodWithResource.Name: {
   189  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   190  				},
   191  			},
   192  			expectedMetrics: expectedMetrics{0, 0},
   193  		},
   194  		{
   195  			name:           "find-existing-claim-by-name",
   196  			pods:           []*v1.Pod{testPodWithResource},
   197  			key:            podKey(testPodWithResource),
   198  			claims:         []*resourcev1alpha2.ResourceClaim{testClaim},
   199  			expectedClaims: []resourcev1alpha2.ResourceClaim{*testClaim},
   200  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   201  				testPodWithResource.Name: {
   202  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &testClaim.Name},
   203  				},
   204  			},
   205  			expectedMetrics: expectedMetrics{0, 0},
   206  		},
   207  		{
   208  			name:          "find-created-claim-in-cache",
   209  			pods:          []*v1.Pod{testPodWithResource},
   210  			key:           podKey(testPodWithResource),
   211  			claimsInCache: []*resourcev1alpha2.ResourceClaim{generatedTestClaim},
   212  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   213  				testPodWithResource.Name: {
   214  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   215  				},
   216  			},
   217  			expectedMetrics: expectedMetrics{0, 0},
   218  		},
   219  		{
   220  			name: "no-such-pod",
   221  			key:  podKey(testPodWithResource),
   222  		},
   223  		{
   224  			name: "pod-deleted",
   225  			pods: func() []*v1.Pod {
   226  				deleted := metav1.Now()
   227  				pods := []*v1.Pod{testPodWithResource.DeepCopy()}
   228  				pods[0].DeletionTimestamp = &deleted
   229  				return pods
   230  			}(),
   231  			key: podKey(testPodWithResource),
   232  		},
   233  		{
   234  			name: "no-volumes",
   235  			pods: []*v1.Pod{testPod},
   236  			key:  podKey(testPod),
   237  		},
   238  		{
   239  			name:           "create-with-other-claim",
   240  			pods:           []*v1.Pod{testPodWithResource},
   241  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   242  			key:            podKey(testPodWithResource),
   243  			claims:         []*resourcev1alpha2.ResourceClaim{otherNamespaceClaim},
   244  			expectedClaims: []resourcev1alpha2.ResourceClaim{*otherNamespaceClaim, *generatedTestClaim},
   245  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   246  				testPodWithResource.Name: {
   247  					{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   248  				},
   249  			},
   250  			expectedMetrics: expectedMetrics{1, 0},
   251  		},
   252  		{
   253  			name:           "wrong-claim-owner",
   254  			pods:           []*v1.Pod{testPodWithResource},
   255  			key:            podKey(testPodWithResource),
   256  			claims:         []*resourcev1alpha2.ResourceClaim{conflictingClaim},
   257  			expectedClaims: []resourcev1alpha2.ResourceClaim{*conflictingClaim},
   258  			expectedError:  true,
   259  		},
   260  		{
   261  			name:            "create-conflict",
   262  			pods:            []*v1.Pod{testPodWithResource},
   263  			templates:       []*resourcev1alpha2.ResourceClaimTemplate{template},
   264  			key:             podKey(testPodWithResource),
   265  			expectedMetrics: expectedMetrics{1, 1},
   266  			expectedError:   true,
   267  		},
   268  		{
   269  			name:            "stay-reserved-seen",
   270  			pods:            []*v1.Pod{testPodWithResource},
   271  			key:             claimKey(testClaimReserved),
   272  			claims:          []*resourcev1alpha2.ResourceClaim{testClaimReserved},
   273  			expectedClaims:  []resourcev1alpha2.ResourceClaim{*testClaimReserved},
   274  			expectedMetrics: expectedMetrics{0, 0},
   275  		},
   276  		{
   277  			name:            "stay-reserved-not-seen",
   278  			podsLater:       []*v1.Pod{testPodWithResource},
   279  			key:             claimKey(testClaimReserved),
   280  			claims:          []*resourcev1alpha2.ResourceClaim{testClaimReserved},
   281  			expectedClaims:  []resourcev1alpha2.ResourceClaim{*testClaimReserved},
   282  			expectedMetrics: expectedMetrics{0, 0},
   283  		},
   284  		{
   285  			name:   "clear-reserved-delayed-allocation",
   286  			pods:   []*v1.Pod{},
   287  			key:    claimKey(testClaimReserved),
   288  			claims: []*resourcev1alpha2.ResourceClaim{testClaimReserved},
   289  			expectedClaims: func() []resourcev1alpha2.ResourceClaim {
   290  				claim := testClaimAllocated.DeepCopy()
   291  				claim.Status.DeallocationRequested = true
   292  				return []resourcev1alpha2.ResourceClaim{*claim}
   293  			}(),
   294  			expectedMetrics: expectedMetrics{0, 0},
   295  		},
   296  		{
   297  			name: "clear-reserved-immediate-allocation",
   298  			pods: []*v1.Pod{},
   299  			key:  claimKey(testClaimReserved),
   300  			claims: func() []*resourcev1alpha2.ResourceClaim {
   301  				claim := testClaimReserved.DeepCopy()
   302  				claim.Spec.AllocationMode = resourcev1alpha2.AllocationModeImmediate
   303  				return []*resourcev1alpha2.ResourceClaim{claim}
   304  			}(),
   305  			expectedClaims: func() []resourcev1alpha2.ResourceClaim {
   306  				claim := testClaimAllocated.DeepCopy()
   307  				claim.Spec.AllocationMode = resourcev1alpha2.AllocationModeImmediate
   308  				return []resourcev1alpha2.ResourceClaim{*claim}
   309  			}(),
   310  			expectedMetrics: expectedMetrics{0, 0},
   311  		},
   312  		{
   313  			name: "clear-reserved-when-done-delayed-allocation",
   314  			pods: func() []*v1.Pod {
   315  				pods := []*v1.Pod{testPodWithResource.DeepCopy()}
   316  				pods[0].Status.Phase = v1.PodSucceeded
   317  				return pods
   318  			}(),
   319  			key: claimKey(testClaimReserved),
   320  			claims: func() []*resourcev1alpha2.ResourceClaim {
   321  				claims := []*resourcev1alpha2.ResourceClaim{testClaimReserved.DeepCopy()}
   322  				claims[0].OwnerReferences = nil
   323  				return claims
   324  			}(),
   325  			expectedClaims: func() []resourcev1alpha2.ResourceClaim {
   326  				claims := []resourcev1alpha2.ResourceClaim{*testClaimAllocated.DeepCopy()}
   327  				claims[0].OwnerReferences = nil
   328  				claims[0].Status.DeallocationRequested = true
   329  				return claims
   330  			}(),
   331  			expectedMetrics: expectedMetrics{0, 0},
   332  		},
   333  		{
   334  			name: "clear-reserved-when-done-immediate-allocation",
   335  			pods: func() []*v1.Pod {
   336  				pods := []*v1.Pod{testPodWithResource.DeepCopy()}
   337  				pods[0].Status.Phase = v1.PodSucceeded
   338  				return pods
   339  			}(),
   340  			key: claimKey(testClaimReserved),
   341  			claims: func() []*resourcev1alpha2.ResourceClaim {
   342  				claims := []*resourcev1alpha2.ResourceClaim{testClaimReserved.DeepCopy()}
   343  				claims[0].OwnerReferences = nil
   344  				claims[0].Spec.AllocationMode = resourcev1alpha2.AllocationModeImmediate
   345  				return claims
   346  			}(),
   347  			expectedClaims: func() []resourcev1alpha2.ResourceClaim {
   348  				claims := []resourcev1alpha2.ResourceClaim{*testClaimAllocated.DeepCopy()}
   349  				claims[0].OwnerReferences = nil
   350  				claims[0].Spec.AllocationMode = resourcev1alpha2.AllocationModeImmediate
   351  				return claims
   352  			}(),
   353  			expectedMetrics: expectedMetrics{0, 0},
   354  		},
   355  		{
   356  			name:            "remove-reserved",
   357  			pods:            []*v1.Pod{testPod},
   358  			key:             claimKey(testClaimReservedTwice),
   359  			claims:          []*resourcev1alpha2.ResourceClaim{testClaimReservedTwice},
   360  			expectedClaims:  []resourcev1alpha2.ResourceClaim{*testClaimReserved},
   361  			expectedMetrics: expectedMetrics{0, 0},
   362  		},
   363  		{
   364  			name: "delete-claim-when-done",
   365  			pods: func() []*v1.Pod {
   366  				pods := []*v1.Pod{testPodWithResource.DeepCopy()}
   367  				pods[0].Status.Phase = v1.PodSucceeded
   368  				return pods
   369  			}(),
   370  			key:             claimKey(testClaimReserved),
   371  			claims:          []*resourcev1alpha2.ResourceClaim{testClaimReserved},
   372  			expectedClaims:  nil,
   373  			expectedMetrics: expectedMetrics{0, 0},
   374  		},
   375  		{
   376  			name:           "trigger-allocation",
   377  			pods:           []*v1.Pod{testPodWithNodeName},
   378  			key:            podKey(testPodWithNodeName),
   379  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   380  			claims:         []*resourcev1alpha2.ResourceClaim{generatedTestClaim},
   381  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaim},
   382  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   383  				testPodWithNodeName.Name: {
   384  					{Name: testPodWithNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   385  				},
   386  			},
   387  			expectedPodSchedulingContexts: []resourcev1alpha2.PodSchedulingContext{podSchedulingContext},
   388  			expectedMetrics:               expectedMetrics{0, 0},
   389  		},
   390  		{
   391  			name:           "add-reserved",
   392  			pods:           []*v1.Pod{testPodWithNodeName},
   393  			key:            podKey(testPodWithNodeName),
   394  			templates:      []*resourcev1alpha2.ResourceClaimTemplate{template},
   395  			claims:         []*resourcev1alpha2.ResourceClaim{generatedTestClaimAllocated},
   396  			expectedClaims: []resourcev1alpha2.ResourceClaim{*generatedTestClaimReserved},
   397  			expectedStatuses: map[string][]v1.PodResourceClaimStatus{
   398  				testPodWithNodeName.Name: {
   399  					{Name: testPodWithNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaim.Name},
   400  				},
   401  			},
   402  			expectedMetrics: expectedMetrics{0, 0},
   403  		},
   404  	}
   405  
   406  	for _, tc := range tests {
   407  		// Run sequentially because of global logging and global metrics.
   408  		t.Run(tc.name, func(t *testing.T) {
   409  			ctx, cancel := context.WithCancel(context.Background())
   410  			defer cancel()
   411  
   412  			var objects []runtime.Object
   413  			for _, pod := range tc.pods {
   414  				objects = append(objects, pod)
   415  			}
   416  			for _, claim := range tc.claims {
   417  				objects = append(objects, claim)
   418  			}
   419  			for _, template := range tc.templates {
   420  				objects = append(objects, template)
   421  			}
   422  
   423  			fakeKubeClient := createTestClient(objects...)
   424  			if tc.expectedMetrics.numFailures > 0 {
   425  				fakeKubeClient.PrependReactor("create", "resourceclaims", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
   426  					return true, nil, apierrors.NewConflict(action.GetResource().GroupResource(), "fake name", errors.New("fake conflict"))
   427  				})
   428  			}
   429  			setupMetrics()
   430  			informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
   431  			podInformer := informerFactory.Core().V1().Pods()
   432  			podSchedulingInformer := informerFactory.Resource().V1alpha2().PodSchedulingContexts()
   433  			claimInformer := informerFactory.Resource().V1alpha2().ResourceClaims()
   434  			templateInformer := informerFactory.Resource().V1alpha2().ResourceClaimTemplates()
   435  
   436  			ec, err := NewController(klog.FromContext(ctx), fakeKubeClient, podInformer, podSchedulingInformer, claimInformer, templateInformer)
   437  			if err != nil {
   438  				t.Fatalf("error creating ephemeral controller : %v", err)
   439  			}
   440  
   441  			// Ensure informers are up-to-date.
   442  			go informerFactory.Start(ctx.Done())
   443  			stopInformers := func() {
   444  				cancel()
   445  				informerFactory.Shutdown()
   446  			}
   447  			defer stopInformers()
   448  			informerFactory.WaitForCacheSync(ctx.Done())
   449  			cache.WaitForCacheSync(ctx.Done(), podInformer.Informer().HasSynced, claimInformer.Informer().HasSynced, templateInformer.Informer().HasSynced)
   450  
   451  			// Add claims that only exist in the mutation cache.
   452  			for _, claim := range tc.claimsInCache {
   453  				ec.claimCache.Mutation(claim)
   454  			}
   455  
   456  			// Simulate race: stop informers, add more pods that the controller doesn't know about.
   457  			stopInformers()
   458  			for _, pod := range tc.podsLater {
   459  				_, err := fakeKubeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
   460  				if err != nil {
   461  					t.Fatalf("unexpected error while creating pod: %v", err)
   462  				}
   463  			}
   464  
   465  			err = ec.syncHandler(context.TODO(), tc.key)
   466  			if err != nil && !tc.expectedError {
   467  				t.Fatalf("unexpected error while running handler: %v", err)
   468  			}
   469  			if err == nil && tc.expectedError {
   470  				t.Fatalf("unexpected success")
   471  			}
   472  
   473  			claims, err := fakeKubeClient.ResourceV1alpha2().ResourceClaims("").List(ctx, metav1.ListOptions{})
   474  			if err != nil {
   475  				t.Fatalf("unexpected error while listing claims: %v", err)
   476  			}
   477  			assert.Equal(t, normalizeClaims(tc.expectedClaims), normalizeClaims(claims.Items))
   478  
   479  			pods, err := fakeKubeClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
   480  			if err != nil {
   481  				t.Fatalf("unexpected error while listing pods: %v", err)
   482  			}
   483  			var actualStatuses map[string][]v1.PodResourceClaimStatus
   484  			for _, pod := range pods.Items {
   485  				if len(pod.Status.ResourceClaimStatuses) == 0 {
   486  					continue
   487  				}
   488  				if actualStatuses == nil {
   489  					actualStatuses = make(map[string][]v1.PodResourceClaimStatus)
   490  				}
   491  				actualStatuses[pod.Name] = pod.Status.ResourceClaimStatuses
   492  			}
   493  			assert.Equal(t, tc.expectedStatuses, actualStatuses, "pod resource claim statuses")
   494  
   495  			scheduling, err := fakeKubeClient.ResourceV1alpha2().PodSchedulingContexts("").List(ctx, metav1.ListOptions{})
   496  			if err != nil {
   497  				t.Fatalf("unexpected error while listing claims: %v", err)
   498  			}
   499  			assert.Equal(t, normalizeScheduling(tc.expectedPodSchedulingContexts), normalizeScheduling(scheduling.Items))
   500  
   501  			expectMetrics(t, tc.expectedMetrics)
   502  		})
   503  	}
   504  }
   505  
   506  func makeClaim(name, namespace, classname string, owner *metav1.OwnerReference) *resourcev1alpha2.ResourceClaim {
   507  	claim := &resourcev1alpha2.ResourceClaim{
   508  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
   509  		Spec: resourcev1alpha2.ResourceClaimSpec{
   510  			ResourceClassName: classname,
   511  			AllocationMode:    resourcev1alpha2.AllocationModeWaitForFirstConsumer,
   512  		},
   513  	}
   514  	if owner != nil {
   515  		claim.OwnerReferences = []metav1.OwnerReference{*owner}
   516  	}
   517  
   518  	return claim
   519  }
   520  
   521  func makeGeneratedClaim(podClaimName, generateName, namespace, classname string, createCounter int, owner *metav1.OwnerReference) *resourcev1alpha2.ResourceClaim {
   522  	claim := &resourcev1alpha2.ResourceClaim{
   523  		ObjectMeta: metav1.ObjectMeta{
   524  			Name:         fmt.Sprintf("%s-%d", generateName, createCounter),
   525  			GenerateName: generateName,
   526  			Namespace:    namespace,
   527  			Annotations:  map[string]string{"resource.kubernetes.io/pod-claim-name": podClaimName},
   528  		},
   529  		Spec: resourcev1alpha2.ResourceClaimSpec{
   530  			ResourceClassName: classname,
   531  			AllocationMode:    resourcev1alpha2.AllocationModeWaitForFirstConsumer,
   532  		},
   533  	}
   534  	if owner != nil {
   535  		claim.OwnerReferences = []metav1.OwnerReference{*owner}
   536  	}
   537  
   538  	return claim
   539  }
   540  
   541  func allocateClaim(claim *resourcev1alpha2.ResourceClaim) *resourcev1alpha2.ResourceClaim {
   542  	claim = claim.DeepCopy()
   543  	claim.Status.Allocation = &resourcev1alpha2.AllocationResult{
   544  		Shareable: true,
   545  	}
   546  	return claim
   547  }
   548  
   549  func reserveClaim(claim *resourcev1alpha2.ResourceClaim, pod *v1.Pod) *resourcev1alpha2.ResourceClaim {
   550  	claim = claim.DeepCopy()
   551  	claim.Status.ReservedFor = append(claim.Status.ReservedFor,
   552  		resourcev1alpha2.ResourceClaimConsumerReference{
   553  			Resource: "pods",
   554  			Name:     pod.Name,
   555  			UID:      pod.UID,
   556  		},
   557  	)
   558  	return claim
   559  }
   560  
   561  func makePodResourceClaim(name, templateName string) *v1.PodResourceClaim {
   562  	return &v1.PodResourceClaim{
   563  		Name: name,
   564  		Source: v1.ClaimSource{
   565  			ResourceClaimTemplateName: &templateName,
   566  		},
   567  	}
   568  }
   569  
   570  func makePod(name, namespace string, uid types.UID, podClaims ...v1.PodResourceClaim) *v1.Pod {
   571  	pod := &v1.Pod{
   572  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: uid},
   573  		Spec: v1.PodSpec{
   574  			ResourceClaims: podClaims,
   575  		},
   576  	}
   577  
   578  	return pod
   579  }
   580  
   581  func makeTemplate(name, namespace, classname string) *resourcev1alpha2.ResourceClaimTemplate {
   582  	template := &resourcev1alpha2.ResourceClaimTemplate{
   583  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
   584  		Spec: resourcev1alpha2.ResourceClaimTemplateSpec{
   585  			Spec: resourcev1alpha2.ResourceClaimSpec{
   586  				ResourceClassName: classname,
   587  			},
   588  		},
   589  	}
   590  	return template
   591  }
   592  
   593  func podKey(pod *v1.Pod) string {
   594  	return podKeyPrefix + pod.Namespace + "/" + pod.Name
   595  }
   596  
   597  func claimKey(claim *resourcev1alpha2.ResourceClaim) string {
   598  	return claimKeyPrefix + claim.Namespace + "/" + claim.Name
   599  }
   600  
   601  func makeOwnerReference(pod *v1.Pod, isController bool) *metav1.OwnerReference {
   602  	isTrue := true
   603  	return &metav1.OwnerReference{
   604  		APIVersion:         "v1",
   605  		Kind:               "Pod",
   606  		Name:               pod.Name,
   607  		UID:                pod.UID,
   608  		Controller:         &isController,
   609  		BlockOwnerDeletion: &isTrue,
   610  	}
   611  }
   612  
   613  func normalizeClaims(claims []resourcev1alpha2.ResourceClaim) []resourcev1alpha2.ResourceClaim {
   614  	sort.Slice(claims, func(i, j int) bool {
   615  		if claims[i].Namespace < claims[j].Namespace {
   616  			return true
   617  		}
   618  		if claims[i].Namespace > claims[j].Namespace {
   619  			return false
   620  		}
   621  		return claims[i].Name < claims[j].Name
   622  	})
   623  	for i := range claims {
   624  		if len(claims[i].Status.ReservedFor) == 0 {
   625  			claims[i].Status.ReservedFor = nil
   626  		}
   627  		if claims[i].Spec.AllocationMode == "" {
   628  			// This emulates defaulting.
   629  			claims[i].Spec.AllocationMode = resourcev1alpha2.AllocationModeWaitForFirstConsumer
   630  		}
   631  	}
   632  	return claims
   633  }
   634  
   635  func normalizeScheduling(scheduling []resourcev1alpha2.PodSchedulingContext) []resourcev1alpha2.PodSchedulingContext {
   636  	sort.Slice(scheduling, func(i, j int) bool {
   637  		return scheduling[i].Namespace < scheduling[j].Namespace ||
   638  			scheduling[i].Name < scheduling[j].Name
   639  	})
   640  	return scheduling
   641  }
   642  
   643  func createTestClient(objects ...runtime.Object) *fake.Clientset {
   644  	fakeClient := fake.NewSimpleClientset(objects...)
   645  	fakeClient.PrependReactor("create", "resourceclaims", createResourceClaimReactor())
   646  	return fakeClient
   647  }
   648  
   649  // createResourceClaimReactor implements the logic required for the GenerateName field to work when using
   650  // the fake client. Add it with client.PrependReactor to your fake client.
   651  func createResourceClaimReactor() func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
   652  	nameCounter := 1
   653  	var mutex sync.Mutex
   654  	return func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
   655  		mutex.Lock()
   656  		defer mutex.Unlock()
   657  		claim := action.(k8stesting.CreateAction).GetObject().(*resourcev1alpha2.ResourceClaim)
   658  		if claim.Name == "" && claim.GenerateName != "" {
   659  			claim.Name = fmt.Sprintf("%s-%d", claim.GenerateName, nameCounter)
   660  		}
   661  		nameCounter++
   662  		return false, nil, nil
   663  	}
   664  }
   665  
   666  // Metrics helpers
   667  
   668  type expectedMetrics struct {
   669  	numCreated  int
   670  	numFailures int
   671  }
   672  
   673  func expectMetrics(t *testing.T, em expectedMetrics) {
   674  	t.Helper()
   675  
   676  	actualCreated, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.ResourceClaimCreateAttempts)
   677  	handleErr(t, err, "ResourceClaimCreate")
   678  	if actualCreated != float64(em.numCreated) {
   679  		t.Errorf("Expected claims to be created %d, got %v", em.numCreated, actualCreated)
   680  	}
   681  	actualConflicts, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.ResourceClaimCreateFailures)
   682  	handleErr(t, err, "ResourceClaimCreate/Conflict")
   683  	if actualConflicts != float64(em.numFailures) {
   684  		t.Errorf("Expected claims to have conflicts %d, got %v", em.numFailures, actualConflicts)
   685  	}
   686  }
   687  
   688  func handleErr(t *testing.T, err error, metricName string) {
   689  	if err != nil {
   690  		t.Errorf("Failed to get %s value, err: %v", metricName, err)
   691  	}
   692  }
   693  
   694  func setupMetrics() {
   695  	ephemeralvolumemetrics.RegisterMetrics()
   696  	ephemeralvolumemetrics.ResourceClaimCreateAttempts.Reset()
   697  	ephemeralvolumemetrics.ResourceClaimCreateFailures.Reset()
   698  }
   699  

View as plain text