mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-11-06 13:28:22 +00:00
This moves the existing code unchanged, updates package and module name, and updates all import statements.
2419 lines
93 KiB
Go
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
|
|
}
|