Files
Patrick Ohly c13f052503 ktesting: move to staging
This moves the existing code unchanged, updates package and module name, and
updates all import statements.
2026-10-07 11:08:22 +02:00

2419 lines
93 KiB
Go

/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourceclaim
import (
"context"
"errors"
"fmt"
"sort"
"sync"
"testing"
"time"
"github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1"
schedulingapi "k8s.io/api/scheduling/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
resourcelisters "k8s.io/client-go/listers/resource/v1"
k8stesting "k8s.io/client-go/testing"
"k8s.io/client-go/util/workqueue"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
resourceclaimmetrics "k8s.io/dynamic-resource-allocation/resourceclaim/metrics"
"k8s.io/klog/v2"
"k8s.io/ktesting"
"k8s.io/kubernetes/pkg/controller"
controllermetrics "k8s.io/kubernetes/pkg/controller/resourceclaim/metrics"
)
var (
testPodName = "test-pod"
testPodGroupName = "test-podgroup"
testNamespace = "my-namespace"
testPodUID = types.UID("uidpod1")
testPodGroupUID = types.UID("uidpodgroup1")
otherNamespace = "not-my-namespace"
podResourceClaimName = "acme-resource"
templateName = "my-template"
nodeName = "worker"
testPod = makePod(testPodName, testNamespace, testPodUID)
testPodWithResource = makePod(testPodName, testNamespace, testPodUID, *makePodResourceClaim(podResourceClaimName, templateName))
testPodWithPodGroupResource = podInPodGroup(testPodWithResource, testPodName, testPodGroupName)
otherTestPod = makePod(testPodName+"-II", testNamespace, testPodUID+"-II")
testPodGroup = makePodGroup(testPodGroupName, testNamespace, testPodGroupUID)
testPodGroupWithResource = makePodGroup(testPodGroupName, testNamespace, testPodGroupUID, *makePodGroupResourceClaim(podResourceClaimName, templateName))
testPodGroupWithResourceInStatus = func() *schedulingapi.PodGroup {
podGroup := testPodGroupWithResource.DeepCopy()
podGroup.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: podResourceClaimName, ResourceClaimName: &testPodGroupClaim.Name},
}
return podGroup
}()
testClaim = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, makeOwnerReference(testPodWithResource, true))
testPodGroupClaim = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, makeOwnerReference(testPodGroupWithResource, true))
testClaimAllocated = allocateClaim(testClaim)
testClaimReserved = reserveClaim(testClaimAllocated, testPodWithResource)
testClaimReservedTwice = reserveClaim(testClaimReserved, otherTestPod)
testClaimKey = claimKeyPrefix + testClaim.Namespace + "/" + testClaim.Name
testPodKey = podKeyPrefix + testNamespace + "/" + testPodName
testPodGroupKey = podGroupKeyPrefix + testNamespace + "/" + testPodGroupName
testClaimReservedForPodGroup = reserveClaim(testClaimAllocated, testPodGroupWithResource)
templatedTestClaim = makeTemplatedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, 1, makeOwnerReference(testPodWithResource, true), nil)
templatedTestClaimAllocated = allocateClaim(templatedTestClaim)
templatedTestClaimReserved = reserveClaim(templatedTestClaimAllocated, testPodWithResource)
templatedTestClaimReservedForPodGroup = reserveClaim(templatedTestClaimAllocated, testPodGroupWithResource)
templatedTestPodGroupClaim = makeTemplatedClaim(podResourceClaimName, testPodGroupName+"-"+podResourceClaimName+"-", testNamespace, 1, makeOwnerReference(testPodGroupWithResource, true), nil)
templatedTestClaimWithAdmin = makeTemplatedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, 1, makeOwnerReference(testPodWithResource, true), new(true))
templatedTestClaimWithAdminAllocated = allocateClaim(templatedTestClaimWithAdmin)
extendedTestClaim = makeExtendedResourceClaim(testPodName, testNamespace, 1, makeOwnerReference(testPodWithResource, true))
extendedTestClaimAllocated = allocateClaim(extendedTestClaim)
conflictingClaim = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, nil)
conflictingPodGroupClaim = makeClaim(testPodGroupName+"-"+podResourceClaimName, testNamespace, nil)
otherNamespaceClaim = makeClaim(testPodName+"-"+podResourceClaimName, otherNamespace, nil)
template = makeTemplate(templateName, testNamespace, nil)
templateWithAdminAccess = makeTemplate(templateName, testNamespace, new(true))
testPodWithNodeName = func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Spec.NodeName = nodeName
pod.Status.ResourceClaimStatuses = append(pod.Status.ResourceClaimStatuses, v1.PodResourceClaimStatus{
Name: pod.Spec.ResourceClaims[0].Name,
ResourceClaimName: &templatedTestClaim.Name,
})
return pod
}()
testPodWithPodGroupAndNodeName = podInPodGroup(testPodWithNodeName, testPodName, testPodGroupName)
adminAccessFeatureOffError = "admin access is requested, but the feature is disabled"
allPossibleFeatures = expandedControllerFeatures(nil, nil, nil, nil)
adminAccessDisabled = expandedControllerFeatures(new(false), nil, nil, nil)
adminAccessEnabled = expandedControllerFeatures(new(true), nil, nil, nil)
genericWorkloadDisabled = expandedControllerFeatures(nil, new(false), nil, nil)
genericWorkloadEnabled = expandedControllerFeatures(nil, new(true), nil, nil)
workloadResourceClaimsDisabled = expandedControllerFeatures(nil, nil, nil, new(false))
workloadResourceClaimsEnabled = expandedControllerFeatures(nil, nil, nil, new(true))
workloadResourceClaimsDisabledGenericWorkloadEnabled = expandedControllerFeatures(nil, new(true), nil, new(false))
)
func TestSyncHandler(t *testing.T) { testSyncHandler(ktesting.Init(t)) }
// TestCreateClaimDoesNotMutateTemplate verifies that creating a ResourceClaim
// from a ResourceClaimTemplate with pre-existing annotations copies the map and
// leaves the shared informer-cached template unmodified, on both claim paths.
func TestCreateClaimDoesNotMutateTemplate(t *testing.T) {
for _, tc := range []struct {
name string
trigger runtime.Object
key string
features controllerFeatures
}{
{"pod", testPodWithResource.DeepCopy(), podKey(testPodWithResource), controllerFeatures{}},
{"podGroup", testPodGroupWithResource.DeepCopy(), testPodGroupKey, controllerFeatures{GenericWorkload: true, WorkloadResourceClaims: true}},
} {
t.Run(tc.name, func(t *testing.T) {
tCtx := ktesting.Init(t)
template := makeTemplate(templateName, testNamespace, nil)
template.Spec.ObjectMeta.Annotations = map[string]string{"example.com/color": "blue"}
fakeKubeClient := createTestClient(tc.trigger, template)
informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
podInformer := informerFactory.Core().V1().Pods()
podGroupInformer := informerFactory.Scheduling().V1beta1().PodGroups()
claimInformer := informerFactory.Resource().V1().ResourceClaims()
templateInformer := informerFactory.Resource().V1().ResourceClaimTemplates()
setupMetrics()
ec, err := newControllerWithFeatures(tCtx.Logger(), fakeKubeClient, podInformer, podGroupInformer, claimInformer, templateInformer, tc.features)
if err != nil {
tCtx.Fatalf("error creating controller: %v", err)
}
informerFactory.StartWithContext(tCtx)
defer func() {
tCtx.Cancel("stopping informers")
informerFactory.Shutdown()
}()
informerFactory.WaitForCacheSyncWithContext(tCtx)
// cachedTemplate is the object the controller reads from the shared informer
// cache; snapshot it so any in-place mutation is detectable.
cachedTemplate, err := templateInformer.Lister().ResourceClaimTemplates(testNamespace).Get(templateName)
if err != nil {
tCtx.Fatalf("template not in informer cache: %v", err)
}
templateBefore := cachedTemplate.DeepCopy()
if err := ec.syncHandler(tCtx, tc.key); err != nil {
tCtx.Fatalf("syncHandler: %v", err)
}
assert.Equal(tCtx, templateBefore, cachedTemplate, "controller must not mutate the shared informer-cached ResourceClaimTemplate")
if _, leaked := cachedTemplate.Spec.ObjectMeta.Annotations[resourceapi.PodResourceClaimAnnotation]; leaked {
tCtx.Errorf("PodResourceClaimAnnotation leaked into the shared template's annotations")
}
// The generated claim must still carry both the template's annotation and
// the controller-added PodResourceClaimAnnotation.
claims, err := fakeKubeClient.ResourceV1().ResourceClaims(testNamespace).List(tCtx, metav1.ListOptions{})
if err != nil {
tCtx.Fatalf("listing claims: %v", err)
}
if assert.Len(tCtx, claims.Items, 1, "exactly one claim should be created") {
created := claims.Items[0]
assert.Equal(tCtx, "blue", created.Annotations["example.com/color"], "template annotation should be copied to the claim")
assert.Equal(tCtx, podResourceClaimName, created.Annotations[resourceapi.PodResourceClaimAnnotation], "claim should carry the PodResourceClaimAnnotation")
}
})
}
}
func testSyncHandler(tCtx ktesting.TContext) {
tests := []struct {
name string
key string
featureCombinations []controllerFeatures
claims []*resourceapi.ResourceClaim
claimsInCache []*resourceapi.ResourceClaim
pods []*v1.Pod
podsLater []*v1.Pod
claimsLater []*resourceapi.ResourceClaim
podGroups []*schedulingapi.PodGroup
templates []*resourceapi.ResourceClaimTemplate
expectedClaims []resourceapi.ResourceClaim
expectedStatuses map[string][]v1.PodResourceClaimStatus
expectedPodGroupStatuses map[string][]schedulingapi.PodGroupResourceClaimStatus
expectedError string
expectedMetrics claimCreateMetrics
}{
{
name: "create",
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "create-adminaccess-feature-disabled",
featureCombinations: adminAccessDisabled,
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{templateWithAdminAccess},
key: podKey(testPodWithResource),
expectedError: adminAccessFeatureOffError,
expectedMetrics: claimCreateMetrics{},
},
{
name: "create-adminaccess-feature-enabled",
featureCombinations: adminAccessEnabled,
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{templateWithAdminAccess},
key: podKey(testPodWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaimWithAdmin},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaimWithAdmin.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "true", ownerKind: "Pod"}: 1,
},
},
{
name: "create-for-grouped-pod",
featureCombinations: workloadResourceClaimsEnabled,
pods: []*v1.Pod{podInPodGroup(testPodWithResource, testPodName, testPodGroupName)},
podGroups: []*schedulingapi.PodGroup{testPodGroup},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "skip-create-for-pod-podgroup-does-not-exist",
featureCombinations: genericWorkloadEnabled,
pods: []*v1.Pod{podInPodGroup(testPodWithResource, testPodName, testPodGroupName)},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedError: `podgroup.scheduling.k8s.io "test-podgroup" not found`,
expectedMetrics: claimCreateMetrics{},
},
{
name: "create-for-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerGroup: "scheduling.k8s.io", ownerKind: "PodGroup"}: 1,
},
},
{
name: "skip-create-podgroup-claim-for-pod",
featureCombinations: workloadResourceClaimsEnabled,
pods: []*v1.Pod{testPodWithPodGroupResource},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithPodGroupResource),
expectedClaims: nil,
expectedMetrics: claimCreateMetrics{},
},
{
name: "update-pod-status-with-podgroup-claim",
featureCombinations: workloadResourceClaimsEnabled,
pods: []*v1.Pod{testPodWithPodGroupResource},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
claims: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithPodGroupResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithPodGroupResource.Name: {
{Name: testPodWithPodGroupResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "skip-create-for-pod-with-podgroup-claim-genericworkload-disabled",
featureCombinations: genericWorkloadDisabled,
pods: []*v1.Pod{testPodWithPodGroupResource},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithPodGroupResource),
expectedError: "GenericWorkload feature is disabled",
expectedMetrics: claimCreateMetrics{},
},
{
name: "skip-create-for-pod-with-podgroup-claim-workloadresourceclaims-disabled",
featureCombinations: workloadResourceClaimsDisabledGenericWorkloadEnabled,
pods: []*v1.Pod{testPodWithPodGroupResource},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithPodGroupResource),
expectedError: "DRAWorkloadResourceClaims feature is disabled",
expectedMetrics: claimCreateMetrics{},
},
{
name: "create-for-grouped-pod-with-claim-workloadresourceclaims-disabled",
featureCombinations: workloadResourceClaimsDisabledGenericWorkloadEnabled,
pods: []*v1.Pod{podInPodGroup(testPodWithResource, testPodName, testPodGroupName)},
podGroups: []*schedulingapi.PodGroup{testPodGroup},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodName: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "nop",
pods: []*v1.Pod{func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
claims: []*resourceapi.ResourceClaim{templatedTestClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "nop-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{func() *schedulingapi.PodGroup {
podGroup := testPodGroupWithResource.DeepCopy()
podGroup.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
}
return podGroup
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
claims: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "nop-claim-in-mutation-cache-only",
pods: []*v1.Pod{func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
claimsInCache: []*resourceapi.ResourceClaim{templatedTestClaim},
expectedClaims: nil, // the claim only lives in the mutation cache, nothing exists in the apiserver
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "nop-claim-in-mutation-cache-only-for-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{func() *schedulingapi.PodGroup {
podGroup := testPodGroupWithResource.DeepCopy()
podGroup.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
}
return podGroup
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
claimsInCache: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
expectedClaims: nil, // the claim only lives in the mutation cache, nothing exists in the apiserver
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "nop-claim-in-apiserver-only",
pods: []*v1.Pod{func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
claimsLater: []*resourceapi.ResourceClaim{templatedTestClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "nop-claim-in-apiserver-only-for-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{func() *schedulingapi.PodGroup {
podGroup := testPodGroupWithResource.DeepCopy()
podGroup.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
}
return podGroup
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
claimsLater: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "recreate",
pods: []*v1.Pod{func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "recreate-for-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{func() *schedulingapi.PodGroup {
pod := testPodGroupWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerGroup: "scheduling.k8s.io", ownerKind: "PodGroup"}: 1,
},
},
{
name: "missing-template",
pods: []*v1.Pod{testPodWithResource},
templates: nil,
key: podKey(testPodWithResource),
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
expectedMetrics: claimCreateMetrics{},
},
{
name: "missing-template-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: nil,
key: podGroupKey(testPodGroupWithResource),
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
expectedMetrics: claimCreateMetrics{},
},
{
name: "find-existing-claim-by-label",
pods: []*v1.Pod{testPodWithResource},
key: podKey(testPodWithResource),
claims: []*resourceapi.ResourceClaim{templatedTestClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "find-existing-claim-by-label-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podGroupKey(testPodGroupWithResource),
claims: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "find-created-claim-in-cache",
pods: []*v1.Pod{testPodWithResource},
key: podKey(testPodWithResource),
claimsInCache: []*resourceapi.ResourceClaim{templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "find-created-claim-in-cache-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podGroupKey(testPodGroupWithResource),
claimsInCache: []*resourceapi.ResourceClaim{templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "recreate-wrong-owner",
pods: []*v1.Pod{func() *v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &conflictingClaim.Name},
}
return pod
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
claims: []*resourceapi.ResourceClaim{conflictingClaim},
expectedClaims: []resourceapi.ResourceClaim{*conflictingClaim, *templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "recreate-wrong-owner-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{func() *schedulingapi.PodGroup {
podGroup := testPodGroupWithResource.DeepCopy()
podGroup.Status.ResourceClaimStatuses = []schedulingapi.PodGroupResourceClaimStatus{
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &conflictingPodGroupClaim.Name},
}
return podGroup
}()},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
claims: []*resourceapi.ResourceClaim{conflictingPodGroupClaim},
expectedClaims: []resourceapi.ResourceClaim{*conflictingPodGroupClaim, *templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerGroup: "scheduling.k8s.io", ownerKind: "PodGroup"}: 1,
},
},
{
name: "no-such-pod",
key: podKey(testPodWithResource),
expectedMetrics: claimCreateMetrics{},
},
{
name: "no-such-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
key: podGroupKey(testPodGroupWithResource),
expectedMetrics: claimCreateMetrics{},
},
{
name: "pod-deleted",
pods: func() []*v1.Pod {
deleted := metav1.Now()
pods := []*v1.Pod{testPodWithResource.DeepCopy()}
pods[0].DeletionTimestamp = &deleted
return pods
}(),
key: podKey(testPodWithResource),
expectedMetrics: claimCreateMetrics{},
},
{
name: "podgroup-deleted",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: func() []*schedulingapi.PodGroup {
deleted := metav1.Now()
podGroups := []*schedulingapi.PodGroup{testPodGroupWithResource.DeepCopy()}
podGroups[0].DeletionTimestamp = &deleted
return podGroups
}(),
key: podGroupKey(testPodGroupWithResource),
expectedMetrics: claimCreateMetrics{},
},
{
name: "no-volumes",
pods: []*v1.Pod{testPod},
key: podKey(testPod),
expectedMetrics: claimCreateMetrics{},
},
{
name: "create-with-other-claim",
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
claims: []*resourceapi.ResourceClaim{otherNamespaceClaim},
expectedClaims: []resourceapi.ResourceClaim{*otherNamespaceClaim, *templatedTestClaim},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithResource.Name: {
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
{
name: "create-with-other-claim-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
claims: []*resourceapi.ResourceClaim{otherNamespaceClaim},
expectedClaims: []resourceapi.ResourceClaim{*otherNamespaceClaim, *templatedTestPodGroupClaim},
expectedPodGroupStatuses: map[string][]schedulingapi.PodGroupResourceClaimStatus{
testPodGroupWithResource.Name: {
{Name: testPodGroupWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestPodGroupClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerGroup: "scheduling.k8s.io", ownerKind: "PodGroup"}: 1,
},
},
{
name: "wrong-claim-owner",
pods: []*v1.Pod{testPodWithResource},
key: podKey(testPodWithResource),
claims: []*resourceapi.ResourceClaim{conflictingClaim},
expectedClaims: []resourceapi.ResourceClaim{*conflictingClaim},
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
expectedMetrics: claimCreateMetrics{},
},
{
name: "wrong-claim-owner-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podGroupKey(testPodGroupWithResource),
claims: []*resourceapi.ResourceClaim{conflictingPodGroupClaim},
expectedClaims: []resourceapi.ResourceClaim{*conflictingPodGroupClaim},
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
expectedMetrics: claimCreateMetrics{},
},
{
name: "create-conflict",
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podKey(testPodWithResource),
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "failure", adminAccess: "false", ownerKind: "Pod"}: 1,
},
expectedError: "create ResourceClaim : Operation cannot be fulfilled on resourceclaims.resource.k8s.io \"fake name\": fake conflict",
},
{
name: "create-conflict-admin-access",
featureCombinations: adminAccessEnabled,
pods: []*v1.Pod{testPodWithResource},
templates: []*resourceapi.ResourceClaimTemplate{templateWithAdminAccess},
key: podKey(testPodWithResource),
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "failure", adminAccess: "true", ownerKind: "Pod"}: 1,
},
expectedError: "create ResourceClaim : Operation cannot be fulfilled on resourceclaims.resource.k8s.io \"fake name\": fake conflict",
},
{
name: "create-conflict-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
templates: []*resourceapi.ResourceClaimTemplate{template},
key: podGroupKey(testPodGroupWithResource),
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "failure", adminAccess: "false", ownerGroup: "scheduling.k8s.io", ownerKind: "PodGroup"}: 1,
},
expectedError: "create ResourceClaim : Operation cannot be fulfilled on resourceclaims.resource.k8s.io \"fake name\": fake conflict",
},
{
name: "stay-reserved-seen",
pods: []*v1.Pod{testPodWithResource},
key: claimKey(testClaimReserved),
claims: []*resourceapi.ResourceClaim{testClaimReserved},
expectedClaims: []resourceapi.ResourceClaim{*testClaimReserved},
expectedMetrics: claimCreateMetrics{},
},
{
name: "stay-reserved-not-seen",
podsLater: []*v1.Pod{testPodWithResource},
key: claimKey(testClaimReserved),
claims: []*resourceapi.ResourceClaim{testClaimReserved},
expectedClaims: []resourceapi.ResourceClaim{*testClaimReserved},
expectedMetrics: claimCreateMetrics{},
},
{
name: "clear-reserved-structured",
pods: []*v1.Pod{},
key: claimKey(testClaimReserved),
claims: []*resourceapi.ResourceClaim{structuredParameters(testClaimReserved)},
expectedClaims: func() []resourceapi.ResourceClaim {
claim := testClaimAllocated.DeepCopy()
claim.Finalizers = []string{}
claim.Status.Allocation = nil
return []resourceapi.ResourceClaim{*claim}
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "clear-reserved-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
podGroups: []*schedulingapi.PodGroup{},
key: claimKey(testClaimReservedForPodGroup),
claims: []*resourceapi.ResourceClaim{structuredParameters(testClaimReservedForPodGroup)},
expectedClaims: func() []resourceapi.ResourceClaim {
claim := testClaimAllocated.DeepCopy()
claim.Finalizers = []string{}
claim.Status.Allocation = nil
return []resourceapi.ResourceClaim{*claim}
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "dont-clear-reserved-podgroup-feature-disabled",
featureCombinations: workloadResourceClaimsDisabled,
podGroups: []*schedulingapi.PodGroup{},
key: claimKey(testClaimReservedForPodGroup),
claims: []*resourceapi.ResourceClaim{structuredParameters(testClaimReservedForPodGroup)},
expectedClaims: []resourceapi.ResourceClaim{*structuredParameters(testClaimReservedForPodGroup)},
expectedMetrics: claimCreateMetrics{},
},
{
name: "dont-clear-reserved-structured",
pods: []*v1.Pod{testPodWithResource},
key: claimKey(testClaimReserved),
claims: func() []*resourceapi.ResourceClaim {
claim := structuredParameters(testClaimReserved)
claim = reserveClaim(claim, otherTestPod)
return []*resourceapi.ResourceClaim{claim}
}(),
expectedClaims: []resourceapi.ResourceClaim{*structuredParameters(testClaimReserved)},
expectedMetrics: claimCreateMetrics{},
},
{
name: "clear-reserved-structured-deleted",
pods: []*v1.Pod{},
key: claimKey(testClaimReserved),
claims: func() []*resourceapi.ResourceClaim {
claim := structuredParameters(testClaimReserved.DeepCopy())
claim.DeletionTimestamp = &metav1.Time{}
return []*resourceapi.ResourceClaim{claim}
}(),
expectedClaims: func() []resourceapi.ResourceClaim {
claim := structuredParameters(testClaimAllocated.DeepCopy())
claim.DeletionTimestamp = &metav1.Time{}
claim.Finalizers = []string{}
claim.Status.Allocation = nil
return []resourceapi.ResourceClaim{*claim}
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "structured-deleted",
pods: []*v1.Pod{},
key: claimKey(testClaimReserved),
claims: func() []*resourceapi.ResourceClaim {
claim := structuredParameters(testClaimAllocated.DeepCopy())
claim.DeletionTimestamp = &metav1.Time{}
return []*resourceapi.ResourceClaim{claim}
}(),
expectedClaims: func() []resourceapi.ResourceClaim {
claim := structuredParameters(testClaimAllocated.DeepCopy())
claim.DeletionTimestamp = &metav1.Time{}
claim.Finalizers = []string{}
claim.Status.Allocation = nil
return []resourceapi.ResourceClaim{*claim}
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "clear-reserved-when-done",
pods: func() []*v1.Pod {
pods := []*v1.Pod{testPodWithResource.DeepCopy()}
pods[0].Status.Phase = v1.PodSucceeded
return pods
}(),
key: claimKey(testClaimReserved),
claims: func() []*resourceapi.ResourceClaim {
claims := []*resourceapi.ResourceClaim{testClaimReserved.DeepCopy()}
claims[0].OwnerReferences = nil
return claims
}(),
expectedClaims: func() []resourceapi.ResourceClaim {
claims := []resourceapi.ResourceClaim{*testClaimAllocated.DeepCopy()}
claims[0].OwnerReferences = nil
return claims
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "clear-reserved-when-deleted-before-scheduling",
pods: func() []*v1.Pod {
pod := testPodWithResource.DeepCopy()
deleted := metav1.Now()
pod.DeletionTimestamp = &deleted
pod.Spec.NodeName = ""
return []*v1.Pod{pod}
}(),
key: claimKey(testClaimReserved),
claims: func() []*resourceapi.ResourceClaim {
claims := []*resourceapi.ResourceClaim{testClaimReserved.DeepCopy()}
claims[0].OwnerReferences = nil
return claims
}(),
expectedClaims: func() []resourceapi.ResourceClaim {
claims := []resourceapi.ResourceClaim{*testClaimAllocated.DeepCopy()}
claims[0].OwnerReferences = nil
return claims
}(),
expectedMetrics: claimCreateMetrics{},
},
{
name: "remove-reserved",
pods: []*v1.Pod{testPod},
key: claimKey(testClaimReservedTwice),
claims: []*resourceapi.ResourceClaim{testClaimReservedTwice},
expectedClaims: []resourceapi.ResourceClaim{*testClaimReserved},
expectedMetrics: claimCreateMetrics{},
},
{
name: "do not reserve static claim for terminal pre-scheduled pod",
pods: func() []*v1.Pod {
pod := makePod(testPodName, testNamespace, testPodUID, v1.PodResourceClaim{
Name: podResourceClaimName,
ResourceClaimName: &testClaim.Name,
})
pod.Spec.NodeName = nodeName
pod.Status.Phase = v1.PodSucceeded
return []*v1.Pod{pod}
}(),
key: podKey(testPodWithNodeName),
claims: func() []*resourceapi.ResourceClaim {
claim := reserveClaim(testClaimAllocated, otherTestPod)
claim.OwnerReferences = nil
return []*resourceapi.ResourceClaim{claim}
}(),
expectedClaims: []resourceapi.ResourceClaim{func() resourceapi.ResourceClaim {
claim := reserveClaim(testClaimAllocated, otherTestPod)
claim.OwnerReferences = nil
return *claim
}()},
expectedMetrics: claimCreateMetrics{},
},
{
name: "delete-claim-when-done",
pods: func() []*v1.Pod {
pods := []*v1.Pod{testPodWithResource.DeepCopy()}
pods[0].Status.Phase = v1.PodSucceeded
return pods
}(),
key: claimKey(testClaimReserved),
claims: []*resourceapi.ResourceClaim{testClaimReserved},
expectedClaims: nil,
expectedMetrics: claimCreateMetrics{},
},
{
name: "add-reserved",
pods: []*v1.Pod{testPodWithNodeName},
key: podKey(testPodWithNodeName),
templates: []*resourceapi.ResourceClaimTemplate{template},
claims: []*resourceapi.ResourceClaim{templatedTestClaimAllocated},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaimReserved},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithNodeName.Name: {
{Name: testPodWithNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "add-reserved-podgroup",
featureCombinations: workloadResourceClaimsEnabled,
pods: []*v1.Pod{testPodWithPodGroupAndNodeName},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podKey(testPodWithPodGroupAndNodeName),
templates: []*resourceapi.ResourceClaimTemplate{template},
claims: []*resourceapi.ResourceClaim{templatedTestClaimAllocated},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaimReservedForPodGroup},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithPodGroupAndNodeName.Name: {
{Name: testPodWithPodGroupAndNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "skip-add-reserved-podgroup-genericworkload-disabled",
featureCombinations: genericWorkloadDisabled,
pods: []*v1.Pod{testPodWithPodGroupAndNodeName},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podKey(testPodWithPodGroupAndNodeName),
templates: []*resourceapi.ResourceClaimTemplate{template},
claims: []*resourceapi.ResourceClaim{templatedTestClaimAllocated},
expectedError: "GenericWorkload feature is disabled",
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaimAllocated},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithPodGroupAndNodeName.Name: {
{Name: testPodWithPodGroupAndNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "skip-add-reserved-podgroup-workloadresourceclaims-disabled",
featureCombinations: workloadResourceClaimsDisabledGenericWorkloadEnabled,
pods: []*v1.Pod{testPodWithPodGroupAndNodeName},
podGroups: []*schedulingapi.PodGroup{testPodGroupWithResource},
key: podKey(testPodWithPodGroupAndNodeName),
templates: []*resourceapi.ResourceClaimTemplate{template},
claims: []*resourceapi.ResourceClaim{templatedTestClaimAllocated},
expectedClaims: []resourceapi.ResourceClaim{*templatedTestClaimAllocated},
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodWithPodGroupAndNodeName.Name: {
{Name: testPodWithPodGroupAndNodeName.Spec.ResourceClaims[0].Name, ResourceClaimName: &templatedTestClaim.Name},
},
},
expectedMetrics: claimCreateMetrics{},
},
{
name: "remove-pod-reservation-with-non-pod-reservation-present",
pods: func() []*v1.Pod {
pod := testPodWithResource.DeepCopy()
pod.Status.Phase = v1.PodSucceeded
return []*v1.Pod{pod}
}(),
claims: func() []*resourceapi.ResourceClaim {
claim := testClaimReserved.DeepCopy()
nonPodRef := resourceapi.ResourceClaimConsumerReference{
APIGroup: "foo.com",
Resource: "foo",
Name: "foo",
UID: "123",
}
claim.Status.ReservedFor = append(claim.Status.ReservedFor, nonPodRef)
return []*resourceapi.ResourceClaim{claim}
}(),
key: testClaimKey,
expectedClaims: []resourceapi.ResourceClaim{func() resourceapi.ResourceClaim {
claim := testClaimReserved.DeepCopy()
nonPodRef := resourceapi.ResourceClaimConsumerReference{
APIGroup: "foo.com",
Resource: "foo",
Name: "foo",
UID: "123",
}
claim.Status.ReservedFor = []resourceapi.ResourceClaimConsumerReference{nonPodRef}
return *claim
}()},
expectedMetrics: claimCreateMetrics{},
},
{
name: "flapping-resourceclaim-statuses",
pods: func() []*v1.Pod {
pod := makePod(testPodName, testNamespace, testPodUID,
*makePodResourceClaim("claimA", templateName),
*makePodResourceClaim("claimB", templateName),
)
// Initially only claimA is in status
pod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
{Name: "claimA", ResourceClaimName: new("claimA-object")},
}
return []*v1.Pod{pod}
}(),
templates: []*resourceapi.ResourceClaimTemplate{template},
claims: []*resourceapi.ResourceClaim{
makeClaim("claimA-object", testNamespace, makeOwnerReference(testPod, true)),
},
key: podKeyPrefix + testNamespace + "/" + testPodName,
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
testPodName: {
{Name: "claimA", ResourceClaimName: new("claimA-object")},
{Name: "claimB", ResourceClaimName: new("test-pod-claimB--1")},
},
},
expectedClaims: []resourceapi.ResourceClaim{
*makeClaim("claimA-object", testNamespace, makeOwnerReference(testPod, true)),
*makeTemplatedClaim("claimB", testPodName+"-claimB-", testNamespace, 1, makeOwnerReference(testPod, true), nil),
},
expectedMetrics: claimCreateMetrics{
claimCreateMetricLabels{status: "success", adminAccess: "false", ownerKind: "Pod"}: 1,
},
},
}
for _, tc := range tests {
// Run sequentially because of global logging and global metrics.
run := func(tCtx ktesting.TContext, features controllerFeatures) {
var objects []runtime.Object
for _, pod := range tc.pods {
objects = append(objects, pod)
}
for _, podGroup := range tc.podGroups {
objects = append(objects, podGroup)
}
for _, claim := range tc.claims {
objects = append(objects, claim)
}
for _, template := range tc.templates {
objects = append(objects, template)
}
fakeKubeClient := createTestClient(objects...)
for labels, expected := range tc.expectedMetrics {
if expected > 0 && labels.status == "failure" {
fakeKubeClient.PrependReactor("create", "resourceclaims", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, apierrors.NewConflict(action.GetResource().GroupResource(), "fake name", errors.New("fake conflict"))
})
break
}
}
var appliedPatches []string
fakeKubeClient.PrependReactor("patch", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
patchAction := action.(k8stesting.PatchAction)
if patchAction.GetSubresource() == "status" {
appliedPatches = append(appliedPatches, string(patchAction.GetPatch()))
}
return false, nil, nil
})
informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
podInformer := informerFactory.Core().V1().Pods()
podGroupInformer := informerFactory.Scheduling().V1beta1().PodGroups()
claimInformer := informerFactory.Resource().V1().ResourceClaims()
templateInformer := informerFactory.Resource().V1().ResourceClaimTemplates()
setupMetrics()
ec, err := newControllerWithFeatures(tCtx.Logger(), fakeKubeClient, podInformer, podGroupInformer, claimInformer, templateInformer, features)
if err != nil {
tCtx.Fatalf("error creating ephemeral controller : %v", err)
}
// Ensure informers are up-to-date.
informerFactory.StartWithContext(tCtx)
stopInformers := func() {
tCtx.Cancel("stopping informers")
informerFactory.Shutdown()
}
defer stopInformers()
informerFactory.WaitForCacheSyncWithContext(tCtx)
// Add claims that only exist in the mutation cache.
for _, claim := range tc.claimsInCache {
ec.claimCache.Mutation(claim)
}
// Simulate race: stop informers, add more objects that the controller doesn't know about.
stopInformers()
for _, pod := range tc.podsLater {
_, err := fakeKubeClient.CoreV1().Pods(pod.Namespace).Create(tCtx, pod, metav1.CreateOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while creating pod: %v", err)
}
}
for _, claim := range tc.claimsLater {
_, err := fakeKubeClient.ResourceV1().ResourceClaims(claim.Namespace).Create(tCtx, claim, metav1.CreateOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while creating claim: %v", err)
}
}
err = ec.syncHandler(tCtx, tc.key)
if err != nil {
if len(tc.expectedError) == 0 {
assert.NoError(tCtx, err)
} else {
assert.ErrorContains(tCtx, err, tc.expectedError, "the error message should have contained the expected error message")
}
return
}
if tc.expectedError != "" {
assert.ErrorContains(tCtx, err, tc.expectedError, "the error message should have contained the expected error message")
} else if err != nil {
tCtx.Errorf("unexpected sync handler error: %v", err)
}
// keep going to check other side effects
if tc.name == "flapping-resourceclaim-statuses" {
assert.Len(tCtx, appliedPatches, 1, "should have applied status once")
assert.Contains(tCtx, appliedPatches[0], `"name":"claimA"`, "patch should contain claimA")
assert.Contains(tCtx, appliedPatches[0], `"name":"claimB"`, "patch should contain claimB")
}
claims, err := fakeKubeClient.ResourceV1().ResourceClaims("").List(tCtx, metav1.ListOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while listing claims: %v", err)
}
assert.Equal(tCtx, normalizeClaims(tc.expectedClaims), normalizeClaims(claims.Items))
pods, err := fakeKubeClient.CoreV1().Pods("").List(tCtx, metav1.ListOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while listing pods: %v", err)
}
var actualStatuses map[string][]v1.PodResourceClaimStatus
for _, pod := range pods.Items {
if len(pod.Status.ResourceClaimStatuses) == 0 {
continue
}
if actualStatuses == nil {
actualStatuses = make(map[string][]v1.PodResourceClaimStatus)
}
actualStatuses[pod.Name] = pod.Status.ResourceClaimStatuses
}
assert.Equal(tCtx, tc.expectedStatuses, actualStatuses, "pod resource claim statuses")
podGroups, err := fakeKubeClient.SchedulingV1beta1().PodGroups("").List(tCtx, metav1.ListOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while listing podgroups: %v", err)
}
var actualPodGroupStatuses map[string][]schedulingapi.PodGroupResourceClaimStatus
for _, podGroup := range podGroups.Items {
if len(podGroup.Status.ResourceClaimStatuses) == 0 {
continue
}
if actualPodGroupStatuses == nil {
actualPodGroupStatuses = make(map[string][]schedulingapi.PodGroupResourceClaimStatus)
}
actualPodGroupStatuses[podGroup.Name] = podGroup.Status.ResourceClaimStatuses
}
assert.Equal(tCtx, tc.expectedPodGroupStatuses, actualPodGroupStatuses, "podgroup resource claim statuses")
expectMetrics(tCtx, tc.expectedMetrics)
}
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
if len(tc.featureCombinations) == 0 {
tc.featureCombinations = allPossibleFeatures
}
for _, features := range tc.featureCombinations {
tCtx.Run(features.String(), func(tCtx ktesting.TContext) {
run(tCtx, features)
})
}
})
}
}
// TestClaimExists covering the three places it looks for a claim
// (mutation cache, underlay informer store, storeapiserver), and a optional owner check.
func TestClaimExists(t *testing.T) { testClaimExists(ktesting.Init(t)) }
func testClaimExists(tCtx ktesting.TContext) {
alwaysOwned := func(*resourceapi.ResourceClaim) bool { return true }
neverOwned := func(*resourceapi.ResourceClaim) bool { return false }
for _, tc := range []struct {
name string
claimInMutationCache *resourceapi.ResourceClaim
claimInInformerCache *resourceapi.ResourceClaim
claimInAPIServer *resourceapi.ResourceClaim
// reactor injected on GET to simulate a transient apiserver error.
getReactor k8stesting.ReactionFunc
invalidOwner bool
wantExists bool
wantErr bool
}{
{
name: "found in mutation cache",
claimInMutationCache: templatedTestClaim,
wantExists: true,
},
{
name: "found in underlay store",
claimInInformerCache: templatedTestClaim,
wantExists: true,
},
{
name: "found in apiserver fallback",
claimInAPIServer: templatedTestClaim,
wantExists: true,
},
{
name: "not found anywhere",
wantExists: false,
},
{
name: "apiserver returns error",
getReactor: func(k8stesting.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewInternalError(errors.New("apiserver is down"))
},
wantExists: false,
wantErr: true,
},
{
name: "owner check fails",
claimInMutationCache: templatedTestClaim,
invalidOwner: true,
wantExists: false,
},
} {
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
var objects []runtime.Object
if tc.claimInInformerCache != nil {
objects = append(objects, tc.claimInInformerCache)
}
fakeKubeClient := createTestClient(objects...)
if tc.getReactor != nil {
fakeKubeClient.PrependReactor("get", "resourceclaims", tc.getReactor)
}
informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
ec, err := newControllerWithFeatures(tCtx.Logger(), fakeKubeClient,
informerFactory.Core().V1().Pods(),
informerFactory.Scheduling().V1beta1().PodGroups(),
informerFactory.Resource().V1().ResourceClaims(),
informerFactory.Resource().V1().ResourceClaimTemplates(),
controllerFeatures{})
tCtx.ExpectNoError(err, "creating controller")
// Ensure informers are up-to-date.
informerFactory.StartWithContext(tCtx)
stopInformers := func() {
tCtx.Cancel("stopping informers")
informerFactory.Shutdown()
}
defer stopInformers()
informerFactory.WaitForCacheSyncWithContext(tCtx)
// Add claims that only exist in the mutation cache.
if claim := tc.claimInMutationCache; claim != nil {
ec.claimCache.Mutation(claim)
}
// Simulate race: stop informers, add more objects that the controller doesn't know about.
stopInformers()
if claim := tc.claimInAPIServer; claim != nil {
_, err := fakeKubeClient.ResourceV1().ResourceClaims(claim.Namespace).Create(tCtx, claim, metav1.CreateOptions{})
if err != nil {
tCtx.Fatalf("unexpected error while creating claim: %v", err)
}
}
checkOwner := alwaysOwned
if tc.invalidOwner {
checkOwner = neverOwned
}
exists, err := ec.claimExists(tCtx, testNamespace, templatedTestClaim.Name, checkOwner)
if tc.wantErr {
assert.Error(tCtx, err)
} else {
assert.NoError(tCtx, err)
}
assert.Equal(tCtx, tc.wantExists, exists)
})
}
}
func TestEventHandlers(t *testing.T) { testEventHandlers(ktesting.Init(t)) }
func testEventHandlers(tCtx ktesting.TContext) {
type object interface {
runtime.Object
metav1.Object
}
modifiedClaim := testClaim.DeepCopy()
modifiedClaim.Labels = map[string]string{"foo": "bar"}
modifiedClaimAdminAccess := templatedTestClaimWithAdmin.DeepCopy()
modifiedClaim.Labels = map[string]string{"foo": "bar"}
otherClaimAllocated := testClaimAllocated.DeepCopy()
otherClaimAllocated.Name += "2"
otherClaimKey := testClaimKey + "2"
otherClaimAllocatedAdminAccess := templatedTestClaimWithAdminAllocated.DeepCopy()
otherClaimAllocatedAdminAccess.Name += "2"
templatedTestClaimKey := testClaimKey + "--1"
templatedOtherClaimKey := templatedTestClaimKey + "2"
extendedResourceTemplatedClaimKey := claimKeyPrefix + testNamespace + "/" + testPodName + "-extended-resources--1"
otherNSTemplate := makeTemplate(templateName, otherNamespace, nil)
otherNSPod := makePod("fake-1", otherNamespace, "uidpod2", *makePodResourceClaim(podResourceClaimName, templateName))
otherNSPodKey := podKeyPrefix + otherNamespace + "/" + otherNSPod.Name
extendedResourceClaimName := "test-extended-claim"
podWithExtendedResourceClaim := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: testPodName, Namespace: testNamespace},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodRunning,
ExtendedResourceClaimStatus: &v1.PodExtendedResourceClaimStatus{
ResourceClaimName: extendedResourceClaimName,
},
},
}
completedPodWithExtendedResourceClaim := podWithExtendedResourceClaim.DeepCopy()
completedPodWithExtendedResourceClaim.Status.Phase = v1.PodSucceeded
completedPodWithRegularAndExtendedResourceClaim := podWithExtendedResourceClaim.DeepCopy()
completedPodWithRegularAndExtendedResourceClaim.Spec.ResourceClaims = []v1.PodResourceClaim{{Name: "regular-claim"}}
completedPodWithRegularAndExtendedResourceClaim.Status.Phase = v1.PodSucceeded
failedPodWithExtendedResourceClaim := podWithExtendedResourceClaim.DeepCopy()
failedPodWithExtendedResourceClaim.Status.Phase = v1.PodFailed
extendedResourceClaimKey := claimKeyPrefix + testNamespace + "/" + extendedResourceClaimName
tests := map[string]struct {
featureCombinations []controllerFeatures
initialObjects []runtime.Object
createObjects []object
updateObjects []object
deleteObjects []object
expectedKeys []string
expectedMetrics map[controllermetrics.NumResourceClaimLabels]float64
expectedIndexedPodsByResourceClaimTemplate []string
}{
"nothing": {},
"new-claim": {
createObjects: []object{testClaim},
expectedKeys: []string{testClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false"}: 1,
},
},
"update-claim": {
initialObjects: []runtime.Object{testClaim},
updateObjects: []object{modifiedClaim},
expectedKeys: []string{testClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false"}: 1,
},
},
"allocate-claim": {
initialObjects: []runtime.Object{testClaim},
updateObjects: []object{testClaimAllocated},
expectedKeys: []string{testClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "false"}: 1,
},
},
"allocate-another-claim": {
initialObjects: []runtime.Object{testClaimAllocated},
createObjects: []object{otherClaimAllocated},
expectedKeys: []string{otherClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "false"}: 2,
},
},
"deallocate-claim": {
initialObjects: []runtime.Object{testClaimAllocated, otherClaimAllocated},
updateObjects: []object{testClaim},
expectedKeys: []string{testClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "false"}: 1,
{Allocated: "false", AdminAccess: "false"}: 1,
},
},
"delete-deallocated-claim": {
initialObjects: []runtime.Object{testClaim, otherClaimAllocated},
deleteObjects: []object{testClaim},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "false"}: 1,
},
},
"delete-allocated-claim": {
initialObjects: []runtime.Object{otherClaimAllocated},
deleteObjects: []object{otherClaimAllocated},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{},
},
"new-claim-admin-access": {
createObjects: []object{templatedTestClaimWithAdmin},
expectedKeys: []string{templatedTestClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "true", Source: "resource_claim_template"}: 1,
},
},
"update-claim-admin-access": {
initialObjects: []runtime.Object{templatedTestClaimWithAdmin},
updateObjects: []object{modifiedClaimAdminAccess},
expectedKeys: []string{templatedTestClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "true", Source: "resource_claim_template"}: 1,
},
},
"allocate-claim-admin-access": {
initialObjects: []runtime.Object{modifiedClaimAdminAccess},
updateObjects: []object{templatedTestClaimWithAdminAllocated},
expectedKeys: []string{templatedTestClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "true", Source: "resource_claim_template"}: 1,
},
},
"allocate-another-claim-admin-access": {
initialObjects: []runtime.Object{templatedTestClaimWithAdminAllocated},
createObjects: []object{otherClaimAllocatedAdminAccess},
expectedKeys: []string{templatedOtherClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "true", Source: "resource_claim_template"}: 2,
},
},
"deallocate-claim-admin-access": {
initialObjects: []runtime.Object{templatedTestClaimWithAdminAllocated, otherClaimAllocatedAdminAccess},
updateObjects: []object{templatedTestClaimWithAdmin},
expectedKeys: []string{templatedTestClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "true", Source: "resource_claim_template"}: 1,
{Allocated: "false", AdminAccess: "true", Source: "resource_claim_template"}: 1,
},
},
"delete-deallocated-claim-admin-access": {
initialObjects: []runtime.Object{templatedTestClaimWithAdmin, otherClaimAllocatedAdminAccess},
deleteObjects: []object{templatedTestClaimWithAdmin},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "true", Source: "resource_claim_template"}: 1,
},
},
"delete-allocated-claim-admin-access": {
initialObjects: []runtime.Object{otherClaimAllocatedAdminAccess},
deleteObjects: []object{otherClaimAllocatedAdminAccess},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{},
},
"new-claim-extended-resources": {
createObjects: []object{extendedTestClaim},
expectedKeys: []string{extendedResourceTemplatedClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false", Source: "extended_resource"}: 1,
},
},
"allocate-claim-extended-resources": {
initialObjects: []runtime.Object{extendedTestClaim},
updateObjects: []object{extendedTestClaimAllocated},
expectedKeys: []string{extendedResourceTemplatedClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "true", AdminAccess: "false", Source: "extended_resource"}: 1,
},
},
"deallocate-claim-extended-resources": {
initialObjects: []runtime.Object{extendedTestClaimAllocated},
updateObjects: []object{extendedTestClaim},
expectedKeys: []string{extendedResourceTemplatedClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false", Source: "extended_resource"}: 1,
},
},
"delete-claim-extended-resources": {
initialObjects: []runtime.Object{extendedTestClaimAllocated},
deleteObjects: []object{extendedTestClaimAllocated},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{},
},
"new-pods": {
createObjects: []object{testPodWithResource, otherNSPod},
expectedKeys: []string{testPodKey, otherNSPodKey},
expectedIndexedPodsByResourceClaimTemplate: []string{
testNamespace + "/" + templateName,
otherNamespace + "/" + templateName,
},
},
"new-template-for-pod": {
initialObjects: []runtime.Object{testPodWithResource, otherNSPod},
createObjects: []object{template},
expectedKeys: []string{testPodKey},
expectedIndexedPodsByResourceClaimTemplate: []string{
testNamespace + "/" + templateName,
otherNamespace + "/" + templateName,
},
},
"new-template-for-pod-other-namespace": {
initialObjects: []runtime.Object{testPodWithResource, otherNSPod, template},
createObjects: []object{otherNSTemplate},
expectedKeys: []string{otherNSPodKey},
expectedIndexedPodsByResourceClaimTemplate: []string{
testNamespace + "/" + templateName,
otherNamespace + "/" + templateName,
},
},
"pod-without-resource-claims": {
createObjects: []object{testPod},
expectedKeys: []string{},
},
"running-pod-with-extended-resource-claim": {
createObjects: []object{podWithExtendedResourceClaim},
expectedKeys: []string{},
},
"completed-pod-with-extended-resource-claim": {
createObjects: []object{completedPodWithExtendedResourceClaim},
expectedKeys: []string{extendedResourceClaimKey},
},
"faled-pod-with-extended-resource-claim": {
createObjects: []object{failedPodWithExtendedResourceClaim},
expectedKeys: []string{extendedResourceClaimKey},
},
"delete-pod-with-extended-resource-claim": {
initialObjects: []runtime.Object{podWithExtendedResourceClaim},
deleteObjects: []object{podWithExtendedResourceClaim},
expectedKeys: []string{extendedResourceClaimKey},
},
"completed-pod-with-regular-and-extended-resource-claim": {
createObjects: []object{completedPodWithRegularAndExtendedResourceClaim},
expectedKeys: []string{extendedResourceClaimKey},
},
"new-podgroup-feature-disabled": {
featureCombinations: workloadResourceClaimsDisabled,
createObjects: []object{testPodGroupWithResourceInStatus},
expectedKeys: []string{},
},
"new-podgroup": {
featureCombinations: workloadResourceClaimsEnabled,
createObjects: []object{testPodGroupWithResourceInStatus},
expectedKeys: []string{testPodGroupKey},
},
"new-podgroup-templated-claim-already-exists": {
featureCombinations: workloadResourceClaimsEnabled,
initialObjects: []runtime.Object{testPodGroupClaim},
createObjects: []object{testPodGroupWithResourceInStatus},
expectedKeys: []string{},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false"}: 1,
},
},
"new-templated-claim-for-podgroup": {
featureCombinations: workloadResourceClaimsEnabled,
initialObjects: []runtime.Object{testPodGroupWithResourceInStatus},
createObjects: []object{testPodGroupClaim},
expectedKeys: []string{testClaimKey},
expectedMetrics: map[controllermetrics.NumResourceClaimLabels]float64{
{Allocated: "false", AdminAccess: "false"}: 1,
},
},
"new-template-for-podgroup": {
featureCombinations: workloadResourceClaimsEnabled,
initialObjects: []runtime.Object{testPodGroupWithResource},
createObjects: []object{template},
expectedKeys: []string{testPodGroupKey},
},
"podgroup-claim-status-update": {
featureCombinations: workloadResourceClaimsEnabled,
initialObjects: []runtime.Object{
testPodGroupWithResource,
podInPodGroup(testPodWithPodGroupResource, testPodName+"-1", testPodGroupName),
podInPodGroup(testPodWithPodGroupResource, testPodName+"-2", testPodGroupName),
podInPodGroup(testPodWithPodGroupResource, testPodName+"-3", testPodGroupName+"-2"),
podInPodGroup(testPod, testPodName+"-4", testPodGroupName),
},
updateObjects: []object{testPodGroupWithResourceInStatus},
expectedKeys: []string{
testPodGroupKey,
testPodKey + "-1",
testPodKey + "-2",
},
expectedIndexedPodsByResourceClaimTemplate: []string{testNamespace + "/" + templateName},
},
"podgroup-claim-status-update-feature-disabled": {
featureCombinations: workloadResourceClaimsDisabled,
initialObjects: []runtime.Object{
testPodGroupWithResource,
podInPodGroup(testPodWithPodGroupResource, testPodName+"-1", testPodGroupName),
podInPodGroup(testPodWithPodGroupResource, testPodName+"-2", testPodGroupName),
podInPodGroup(testPodWithPodGroupResource, testPodName+"-3", testPodGroupName+"-2"),
podInPodGroup(testPod, testPodName+"-4", testPodGroupName),
},
updateObjects: []object{testPodGroupWithResourceInStatus},
expectedKeys: []string{},
expectedIndexedPodsByResourceClaimTemplate: []string{testNamespace + "/" + templateName},
},
}
for name, test := range tests {
run := func(tCtx ktesting.TContext, features controllerFeatures) {
fakeKubeClient := createTestClient(test.initialObjects...)
informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
podInformer := informerFactory.Core().V1().Pods()
podGroupInformer := informerFactory.Scheduling().V1beta1().PodGroups()
claimInformer := informerFactory.Resource().V1().ResourceClaims()
templateInformer := informerFactory.Resource().V1().ResourceClaimTemplates()
setupMetrics()
ec, err := newControllerWithFeatures(tCtx.Logger(), fakeKubeClient, podInformer, podGroupInformer, claimInformer, templateInformer, features)
tCtx.ExpectNoError(err, "creating ephemeral controller")
tCtx.Cleanup(ec.queue.ShutDown)
informerFactory.StartWithContext(tCtx)
tCtx.Cleanup(informerFactory.Shutdown)
drainQueue := func() []string {
tCtx.Wait()
actualKeys := []string{}
for ec.queue.Len() > 0 {
actual, shuttingDown := ec.queue.Get()
tCtx.Expect(shuttingDown).To(gomega.BeFalseBecause("workqueue should not be shutting down"))
actualKeys = append(actualKeys, actual)
ec.queue.Forget(actual)
ec.queue.Done(actual)
}
return actualKeys
}
gvr := func(obj metav1.Object) schema.GroupVersionResource {
switch obj.(type) {
case *v1.Pod:
return v1.SchemeGroupVersion.WithResource("pods")
case *schedulingapi.PodGroup:
return schedulingapi.SchemeGroupVersion.WithResource("podgroups")
case *resourceapi.ResourceClaim:
return resourceapi.SchemeGroupVersion.WithResource("resourceclaims")
case *resourceapi.ResourceClaimTemplate:
return resourceapi.SchemeGroupVersion.WithResource("resourceclaimtemplates")
}
tCtx.Fatalf("invalid object type %T", obj)
return schema.GroupVersionResource{}
}
// Not checking after initial objects added. Waiting for the
// interesting operations.
_ = drainQueue()
for _, object := range test.createObjects {
err := fakeKubeClient.Tracker().Create(gvr(object), object, object.GetNamespace(), metav1.CreateOptions{})
tCtx.ExpectNoError(err)
}
for _, object := range test.updateObjects {
err := fakeKubeClient.Tracker().Update(gvr(object), object, object.GetNamespace(), metav1.UpdateOptions{})
tCtx.ExpectNoError(err)
}
for _, object := range test.deleteObjects {
err := fakeKubeClient.Tracker().Delete(gvr(object), object.GetNamespace(), object.GetName(), metav1.DeleteOptions{})
tCtx.ExpectNoError(err)
}
actualKeys := drainQueue()
tCtx.Expect(actualKeys).To(gomega.ConsistOf(test.expectedKeys), "Workqueue does not contain expected elements")
em := newNumMetrics(claimInformer.Lister())
for labels, val := range test.expectedMetrics {
em = em.withUpdates(labels, val)
}
em.verify(tCtx)
actualIndexedPodsByResourceClaimTemplate := ec.podIndexer.ListIndexFuncValues(podResourceClaimTemplateIndex)
tCtx.Expect(actualIndexedPodsByResourceClaimTemplate).To(gomega.ConsistOf(test.expectedIndexedPodsByResourceClaimTemplate), "expected Pods were not indexed by ResourceClaimTemplate")
}
if len(test.featureCombinations) == 0 {
test.featureCombinations = allPossibleFeatures
}
tCtx.Run(name, func(tCtx ktesting.TContext) {
for _, features := range test.featureCombinations {
tCtx.SyncTest(features.String(), func(tCtx ktesting.TContext) {
run(tCtx, features)
})
}
})
}
}
func TestWorkqueue(t *testing.T) { testWorkqueue(ktesting.Init(t)) }
func testWorkqueue(tCtx ktesting.TContext) {
tests := map[string]struct {
shutdown bool
syncHandlerErr error
expectStopLoop bool
expectSynced bool
expectRequeue bool
}{
"no-error": {
syncHandlerErr: nil,
expectSynced: true,
},
"retryable-error": {
syncHandlerErr: fmt.Errorf("will retry"),
expectSynced: true,
expectRequeue: true,
},
"nonretryable-error": {
syncHandlerErr: nonRetryableError{fmt.Errorf("will not retry")},
expectSynced: true,
expectRequeue: false,
},
"wrapped-nonretryable-error": {
syncHandlerErr: fmt.Errorf("%w", nonRetryableError{fmt.Errorf("will not retry")}),
expectSynced: true,
expectRequeue: false,
},
"shutdown": {
shutdown: true,
expectSynced: false,
expectStopLoop: true,
},
}
for name, test := range tests {
tCtx.SyncTest(name, func(tCtx ktesting.TContext) {
// The default [utilruntime.ErrorHandlers] use [time] but are
// initialized before the synctest bubble is established. As a
// result, when the error handler is invoked the default rate
// limiting begins a sleep for the duration between *fake* now and
// *actual* now. The workqueue in turn spins through its internal
// periodic accounting for more than 26 years(!) which eventually
// resolves, but wastes actual CPU time.
oldErrorHandlers := utilruntime.ErrorHandlers
tCtx.Cleanup(func() {
utilruntime.ErrorHandlers = oldErrorHandlers
})
utilruntime.ErrorHandlers = []utilruntime.ErrorHandler{
func(ctx context.Context, err error, msg string, keysAndValues ...any) {
klog.FromContext(ctx).Error(err, msg, keysAndValues...)
},
}
rateLimitDelay := 1 * time.Second
ec := &Controller{
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
&staticRateLimiter[string]{delay: rateLimitDelay, requeues: map[string]int{}},
workqueue.TypedRateLimitingQueueConfig[string]{Name: "resource_claim"},
),
}
tCtx.Cleanup(ec.queue.ShutDown)
synced := false
ec.syncHandlerFunc = func(ctx context.Context, s string) error {
synced = true
return test.syncHandlerErr
}
if test.shutdown {
ec.queue.ShutDown()
}
var key string
ec.queue.Add(key)
actual := ec.processNextWorkItem(tCtx)
tCtx.Expect(actual).To(gomega.Equal(!test.expectStopLoop))
if test.expectSynced {
tCtx.Expect(synced).To(gomega.BeTrueBecause("sync handler should have run but did not"))
} else {
tCtx.Expect(synced).To(gomega.BeFalseBecause("sync handler should not have run but did"))
}
time.Sleep(rateLimitDelay)
// The delayed Add races with checking the length of the queue. Wait
// for the Add to finish before checking the length.
tCtx.Wait()
if test.expectRequeue {
tCtx.Expect(ec.queue.Len()).To(gomega.Equal(1), "key should have been queued")
} else {
tCtx.Expect(ec.queue.Len()).To(gomega.Equal(0), "no key should have been queued")
}
})
}
}
func TestGetAdminAccessMetricLabel(t *testing.T) {
tests := []struct {
name string
claim *resourceapi.ResourceClaim
want string
}{
{
name: "nil-claim",
claim: nil,
want: "false",
},
{
name: "no-requests",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: nil,
},
},
},
want: "false",
},
{
name: "admin-access-false",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: new(false),
},
},
},
},
},
},
want: "false",
},
{
name: "admin-access-true",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: new(true),
},
},
},
},
},
},
want: "true",
},
{
name: "prioritized-list",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
FirstAvailable: []resourceapi.DeviceSubRequest{{}},
},
},
},
},
},
want: "false",
},
{
name: "one-of-multiple-requests-with-admin-access-true",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: new(false),
},
},
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: new(true),
},
},
},
},
},
},
want: "true",
},
{
name: "multiple-requests-admin-access-false-or-nil",
claim: &resourceapi.ResourceClaim{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: nil,
},
},
{
Exactly: &resourceapi.ExactDeviceRequest{
AdminAccess: new(false),
},
},
},
},
},
},
want: "false",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getAdminAccessMetricLabel(tt.claim)
if got != tt.want {
t.Errorf("GetAdminAccessMetricLabel() = %v, want %v", got, tt.want)
}
})
}
}
// expandedControllerFeatures generates a list of [controllerFeatures] based on
// the provided constraints. Arguments that are nil mean the corresponding
// feature can be enabled or disabled. Non-nil arguments restrict the
// corresponding feature to only that referenced value.
func expandedControllerFeatures(adminAccess, genericWorkload, prioritizedList, workloadResourceClaims *bool) []controllerFeatures {
var combos []controllerFeatures
bools := []bool{false, true}
for _, adminAccessVal := range bools {
if adminAccess != nil && *adminAccess != adminAccessVal {
continue
}
for _, genericWorkloadVal := range bools {
if genericWorkload != nil && *genericWorkload != genericWorkloadVal {
continue
}
for _, prioritizedListVal := range bools {
if prioritizedList != nil && *prioritizedList != prioritizedListVal {
continue
}
for _, workloadResourceClaimsVal := range bools {
if workloadResourceClaims != nil && *workloadResourceClaims != workloadResourceClaimsVal {
continue
}
// WorkloadResourceClaims depends on GenericWorkload
if workloadResourceClaimsVal && !genericWorkloadVal {
continue
}
combos = append(combos, controllerFeatures{
AdminAccess: adminAccessVal,
GenericWorkload: genericWorkloadVal,
PrioritizedList: prioritizedListVal,
WorkloadResourceClaims: workloadResourceClaimsVal,
})
}
}
}
}
return combos
}
func makeClaim(name, namespace string, owner *metav1.OwnerReference) *resourceapi.ResourceClaim {
claim := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
}
if owner != nil {
claim.OwnerReferences = []metav1.OwnerReference{*owner}
}
return claim
}
func makeTemplatedClaim(podClaimName, generateName, namespace string, createCounter int, owner *metav1.OwnerReference, adminAccess *bool) *resourceapi.ResourceClaim {
claim := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", generateName, createCounter),
GenerateName: generateName,
Namespace: namespace,
Annotations: map[string]string{resourceapi.PodResourceClaimAnnotation: podClaimName},
},
}
if owner != nil {
claim.OwnerReferences = []metav1.OwnerReference{*owner}
}
if adminAccess != nil {
claim.Spec = resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Name: "req-0",
Exactly: &resourceapi.ExactDeviceRequest{
DeviceClassName: "class",
AdminAccess: adminAccess,
},
},
},
},
}
}
return claim
}
func makeExtendedResourceClaim(podName, namespace string, createCounter int, owner *metav1.OwnerReference) *resourceapi.ResourceClaim {
generateName := podName + "-extended-resources-"
claim := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%d", generateName, createCounter),
GenerateName: generateName,
Namespace: namespace,
Annotations: map[string]string{"resource.kubernetes.io/extended-resource-claim": "true"},
},
}
if owner != nil {
claim.OwnerReferences = []metav1.OwnerReference{*owner}
}
return claim
}
func allocateClaim(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
claim = claim.DeepCopy()
claim.Status.Allocation = &resourceapi.AllocationResult{}
return claim
}
func structuredParameters(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
claim = claim.DeepCopy()
// As far the controller is concerned, a claim was allocated by us if it has
// this finalizer. For testing we don't need to update the allocation result.
claim.Finalizers = append(claim.Finalizers, resourceapi.Finalizer)
return claim
}
func reserveClaim(claim *resourceapi.ResourceClaim, obj metav1.Object) *resourceapi.ResourceClaim {
claim = claim.DeepCopy()
var apiGroup, resource string
switch obj.(type) {
case *v1.Pod:
apiGroup = v1.GroupName
resource = "pods"
case *schedulingapi.PodGroup:
apiGroup = schedulingapi.GroupName
resource = "podgroups"
default:
panic(fmt.Sprintf("invalid type: %T", obj))
}
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
resourceapi.ResourceClaimConsumerReference{
APIGroup: apiGroup,
Resource: resource,
Name: obj.GetName(),
UID: obj.GetUID(),
},
)
return claim
}
func makePodResourceClaim(name, templateName string) *v1.PodResourceClaim {
return &v1.PodResourceClaim{
Name: name,
ResourceClaimTemplateName: &templateName,
}
}
func makePod(name, namespace string, uid types.UID, podClaims ...v1.PodResourceClaim) *v1.Pod {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: uid},
Spec: v1.PodSpec{
ResourceClaims: podClaims,
},
}
return pod
}
func makePodGroupResourceClaim(name, templateName string) *schedulingapi.PodGroupResourceClaim {
return &schedulingapi.PodGroupResourceClaim{
Name: name,
ResourceClaimTemplateName: &templateName,
}
}
func makePodGroup(name, namespace string, uid types.UID, podGroupClaims ...schedulingapi.PodGroupResourceClaim) *schedulingapi.PodGroup {
podGroup := &schedulingapi.PodGroup{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: uid},
Spec: schedulingapi.PodGroupSpec{
ResourceClaims: podGroupClaims,
},
}
return podGroup
}
func podInPodGroup(pod *v1.Pod, podName, podGroupName string) *v1.Pod {
pod = pod.DeepCopy()
pod.Name = podName
pod.Spec.SchedulingGroup = &v1.PodSchedulingGroup{
PodGroupName: &podGroupName,
}
return pod
}
func makeTemplate(name, namespace string, adminAccess *bool) *resourceapi.ResourceClaimTemplate {
template := &resourceapi.ResourceClaimTemplate{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
}
if adminAccess != nil {
template.Spec = resourceapi.ResourceClaimTemplateSpec{
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{
{
Name: "req-0",
Exactly: &resourceapi.ExactDeviceRequest{
DeviceClassName: "class",
AdminAccess: adminAccess,
},
},
},
},
},
}
}
return template
}
func podKey(pod *v1.Pod) string {
return podKeyPrefix + pod.Namespace + "/" + pod.Name
}
func podGroupKey(podGroup *schedulingapi.PodGroup) string {
return podGroupKeyPrefix + podGroup.Namespace + "/" + podGroup.Name
}
func claimKey(claim *resourceapi.ResourceClaim) string {
return claimKeyPrefix + claim.Namespace + "/" + claim.Name
}
func makeOwnerReference(obj metav1.Object, isController bool) *metav1.OwnerReference {
var apiVersion, kind string
switch obj.(type) {
case *v1.Pod:
apiVersion = v1.SchemeGroupVersion.String()
kind = "Pod"
case *schedulingapi.PodGroup:
apiVersion = schedulingapi.SchemeGroupVersion.String()
kind = "PodGroup"
default:
panic(fmt.Sprintf("invalid type %T", obj))
}
return &metav1.OwnerReference{
APIVersion: apiVersion,
Kind: kind,
Name: obj.GetName(),
UID: obj.GetUID(),
Controller: &isController,
}
}
func normalizeClaims(claims []resourceapi.ResourceClaim) []resourceapi.ResourceClaim {
sort.Slice(claims, func(i, j int) bool {
if claims[i].Namespace < claims[j].Namespace {
return true
}
if claims[i].Namespace > claims[j].Namespace {
return false
}
return claims[i].Name < claims[j].Name
})
for i := range claims {
if len(claims[i].Status.ReservedFor) == 0 {
claims[i].Status.ReservedFor = nil
}
}
return claims
}
func createTestClient(objects ...runtime.Object) *fake.Clientset {
fakeClient := fake.NewSimpleClientset(objects...)
fakeClient.PrependReactor("create", "resourceclaims", createResourceClaimReactor())
return fakeClient
}
// createResourceClaimReactor implements the logic required for the GenerateName field to work when using
// the fake client. Add it with client.PrependReactor to your fake client.
func createResourceClaimReactor() func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
nameCounter := 1
var mutex sync.Mutex
return func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
mutex.Lock()
defer mutex.Unlock()
claim := action.(k8stesting.CreateAction).GetObject().(*resourceapi.ResourceClaim)
if claim.Name == "" && claim.GenerateName != "" {
claim.Name = fmt.Sprintf("%s-%d", claim.GenerateName, nameCounter)
}
nameCounter++
return false, nil, nil
}
}
type numMetrics struct {
metrics map[controllermetrics.NumResourceClaimLabels]float64
lister resourcelisters.ResourceClaimLister
}
func getNumMetric(lister resourcelisters.ResourceClaimLister, logger klog.Logger) (em numMetrics, err error) {
if lister == nil {
return numMetrics{}, nil
}
// Create a fresh collector instance for each call to avoid registration conflicts
freshCollector := newCustomCollector(lister, getAdminAccessMetricLabel, logger)
testRegistry := metrics.NewKubeRegistry()
testRegistry.CustomMustRegister(freshCollector)
gatheredMetrics, err := testRegistry.Gather()
if err != nil {
return numMetrics{}, fmt.Errorf("failed to gather metrics: %w", err)
}
metricName := "dynamic_resource_allocation_resource_claims"
em = newNumMetrics(lister)
for _, mf := range gatheredMetrics {
if mf.GetName() != metricName {
continue
}
for _, metric := range mf.GetMetric() {
labels := make(map[string]string)
for _, labelPair := range metric.GetLabel() {
labels[labelPair.GetName()] = labelPair.GetValue()
}
allocated := labels["allocated"]
adminAccess := labels["admin_access"]
source := labels["source"]
value := metric.GetGauge().GetValue()
em.metrics[controllermetrics.NumResourceClaimLabels{
Allocated: allocated,
AdminAccess: adminAccess,
Source: source,
}] = value
}
}
return em, nil
}
func (em numMetrics) verify(tCtx ktesting.TContext) {
tCtx.Helper()
result, err := getNumMetric(em.lister, tCtx.Logger())
tCtx.ExpectNoError(err)
tCtx.Expect(result.metrics).To(gomega.Equal(em.metrics))
}
type claimCreateMetricLabels struct {
status string
adminAccess string
ownerGroup string
ownerKind string
}
type claimCreateMetrics map[claimCreateMetricLabels]float64
func expectMetrics(tCtx ktesting.TContext, em claimCreateMetrics) {
tCtx.Helper()
gatheredMetrics, err := legacyregistry.DefaultGatherer.Gather()
if err != nil {
tCtx.Errorf("failed to gather metrics: %v", err)
return
}
actualMetrics := claimCreateMetrics{}
for _, mf := range gatheredMetrics {
if mf.GetName() != "dynamic_resource_allocation_resourceclaim_creates_total" {
continue
}
for _, metric := range mf.GetMetric() {
labels := make(map[string]string)
for _, labelPair := range metric.GetLabel() {
labels[labelPair.GetName()] = labelPair.GetValue()
}
actualMetrics[claimCreateMetricLabels{
status: labels["status"],
adminAccess: labels["admin_access"],
ownerGroup: labels["owner_api_group"],
ownerKind: labels["owner_api_kind"],
}] = metric.GetCounter().GetValue()
}
}
tCtx.Expect(actualMetrics).To(gomega.Equal(em))
}
func setupMetrics() {
// Enable test mode to prevent global custom collector registration
controllermetrics.SetTestMode(true)
// Reset counter metrics for each test (they are registered by the controller itself)
resourceclaimmetrics.ResourceClaimCreate.Reset()
}
func newNumMetrics(lister resourcelisters.ResourceClaimLister) numMetrics {
metrics := make(map[controllermetrics.NumResourceClaimLabels]float64)
for _, allocated := range []string{"false", "true"} {
for _, adminAccess := range []string{"false", "true"} {
for _, source := range []string{"", "extended_resource", "resource_claim_template"} {
metrics[controllermetrics.NumResourceClaimLabels{
Allocated: allocated,
AdminAccess: adminAccess,
Source: source,
}] = 0
}
}
}
return numMetrics{
metrics: metrics,
lister: lister,
}
}
func (em numMetrics) withUpdates(rcLabels controllermetrics.NumResourceClaimLabels, n float64) numMetrics {
em.metrics[rcLabels] += n
return numMetrics{
metrics: em.metrics,
lister: em.lister,
}
}
// staticRateLimiter delays any item by a fixed amount of time. It allows a test
// to know exactly how long to wait before rate-limited items are added to the
// queue.
type staticRateLimiter[T comparable] struct {
delay time.Duration
requeuesLock sync.Mutex
requeues map[T]int
}
// Forget implements [workqueue.TypedRateLimiter].
func (s *staticRateLimiter[T]) Forget(item T) {
s.requeuesLock.Lock()
defer s.requeuesLock.Unlock()
delete(s.requeues, item)
}
// NumRequeues implements [workqueue.TypedRateLimiter].
func (s *staticRateLimiter[T]) NumRequeues(item T) int {
s.requeuesLock.Lock()
defer s.requeuesLock.Unlock()
return s.requeues[item]
}
// When implements [workqueue.TypedRateLimiter].
func (s *staticRateLimiter[T]) When(item T) time.Duration {
s.requeuesLock.Lock()
defer s.requeuesLock.Unlock()
s.requeues[item]++
return s.delay
}