Skip to content

Commit d4be9d6

Browse files
committed
test(binder): cover extended-resource claim rollback in UnAllocate
Add unit coverage for the DRA extended-resource rollback path: after a successful bind creates the synthetic annotated ResourceClaim, a later bind failure triggers UnAllocate, which must delete the claim and clear pod.Status.ExtendedResourceClaimStatus. Includes idempotency cases for repeated rollback and rollback with nothing to clean up. Signed-off-by: gshaibi <gshaibi@nvidia.com>
1 parent 02a9b30 commit d4be9d6

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

  • pkg/binder/plugins/k8s-plugins/dynamicresources
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2025 NVIDIA CORPORATION
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package dynamicresources
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
13+
corev1 "k8s.io/api/core/v1"
14+
resourceapi "k8s.io/api/resource/v1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/runtime"
17+
"k8s.io/apimachinery/pkg/types"
18+
"k8s.io/client-go/kubernetes/fake"
19+
k8stesting "k8s.io/client-go/testing"
20+
21+
"github.com/kai-scheduler/KAI-scheduler/pkg/apis/scheduling/v1alpha2"
22+
)
23+
24+
func TestDynamicResources(t *testing.T) {
25+
RegisterFailHandler(Fail)
26+
RunSpecs(t, "Binder DynamicResources Suite")
27+
}
28+
29+
var _ = Describe("dynamicResourcesPlugin extended-resource rollback", func() {
30+
const (
31+
namespace = "test-ns"
32+
podName = "test-pod"
33+
podUID = "pod-uid-123"
34+
)
35+
36+
var (
37+
ctx context.Context
38+
client *fake.Clientset
39+
plugin *dynamicResourcesPlugin
40+
pod *corev1.Pod
41+
request *v1alpha2.BindRequest
42+
)
43+
44+
listClaims := func() []resourceapi.ResourceClaim {
45+
list, err := client.ResourceV1().ResourceClaims(namespace).List(ctx, metav1.ListOptions{})
46+
Expect(err).NotTo(HaveOccurred())
47+
return list.Items
48+
}
49+
50+
getPod := func() *corev1.Pod {
51+
p, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
52+
Expect(err).NotTo(HaveOccurred())
53+
return p
54+
}
55+
56+
BeforeEach(func() {
57+
ctx = context.Background()
58+
pod = &corev1.Pod{
59+
ObjectMeta: metav1.ObjectMeta{
60+
Name: podName,
61+
Namespace: namespace,
62+
UID: types.UID(podUID),
63+
},
64+
}
65+
client = fake.NewSimpleClientset(pod)
66+
// The fake tracker does not populate Name from GenerateName; do it here so the
67+
// claim created during Bind is retrievable by the rollback path.
68+
client.PrependReactor("create", "resourceclaims", func(action k8stesting.Action) (bool, runtime.Object, error) {
69+
claim := action.(k8stesting.CreateAction).GetObject().(*resourceapi.ResourceClaim)
70+
if claim.Name == "" && claim.GenerateName != "" {
71+
claim.Name = claim.GenerateName + "generated"
72+
}
73+
return false, claim, nil
74+
})
75+
76+
plugin = &dynamicResourcesPlugin{client: client, bindTimeout: 5}
77+
request = &v1alpha2.BindRequest{
78+
Spec: v1alpha2.BindRequestSpec{
79+
ExtendedResourceClaimAllocation: &v1alpha2.ExtendedResourceClaimAllocation{
80+
Allocation: &resourceapi.AllocationResult{},
81+
DeviceRequests: []resourceapi.DeviceRequest{{Name: "req0"}},
82+
ContainerMappings: []corev1.ContainerExtendedResourceRequest{
83+
{ContainerName: "main", ResourceName: "nvidia.com/gpu", RequestName: "req0"},
84+
},
85+
},
86+
},
87+
}
88+
})
89+
90+
It("rolls back the created claim and clears pod status after a later bind failure", func() {
91+
Expect(plugin.Bind(ctx, pod, request, nil)).To(Succeed())
92+
93+
claims := listClaims()
94+
Expect(claims).To(HaveLen(1))
95+
Expect(claims[0].Annotations).To(HaveKeyWithValue(resourceapi.ExtendedResourceClaimAnnotation, "true"))
96+
Expect(getPod().Status.ExtendedResourceClaimStatus).NotTo(BeNil())
97+
98+
// A later plugin's Bind fails: the framework rolls back by calling UnAllocate.
99+
plugin.UnAllocate(ctx, pod, "node", nil)
100+
101+
Expect(listClaims()).To(BeEmpty())
102+
Expect(getPod().Status.ExtendedResourceClaimStatus).To(BeNil())
103+
})
104+
105+
It("is idempotent when rollback runs more than once", func() {
106+
Expect(plugin.Bind(ctx, pod, request, nil)).To(Succeed())
107+
Expect(listClaims()).To(HaveLen(1))
108+
109+
plugin.UnAllocate(ctx, pod, "node", nil)
110+
Expect(listClaims()).To(BeEmpty())
111+
Expect(getPod().Status.ExtendedResourceClaimStatus).To(BeNil())
112+
113+
// Second rollback must be a no-op: the claim is already gone and status already nil.
114+
Expect(func() { plugin.UnAllocate(ctx, pod, "node", nil) }).NotTo(Panic())
115+
Expect(listClaims()).To(BeEmpty())
116+
Expect(getPod().Status.ExtendedResourceClaimStatus).To(BeNil())
117+
})
118+
119+
It("does nothing when there is no extended-resource claim to roll back", func() {
120+
plugin.UnAllocate(ctx, pod, "node", nil)
121+
Expect(listClaims()).To(BeEmpty())
122+
Expect(getPod().Status.ExtendedResourceClaimStatus).To(BeNil())
123+
})
124+
})

0 commit comments

Comments
 (0)