Skip to content

Commit a1b8e9d

Browse files
committed
DRA kubelet: increase plugin test coverage
Deleting slices was not covered to begin with and the recent registration changes also could have been covered better. Now coverage is at 91%.
1 parent 1193ff1 commit a1b8e9d

File tree

2 files changed

+219
-38
lines changed

2 files changed

+219
-38
lines changed

pkg/kubelet/cm/dra/plugin/plugin_test.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net"
2323
"os"
2424
"path/filepath"
25+
"strings"
2526
"sync"
2627
"testing"
2728

@@ -232,24 +233,41 @@ func TestNewDRAPluginClient(t *testing.T) {
232233
}
233234
}
234235

235-
func TestNodeUnprepareResources(t *testing.T) {
236+
func TestGRPCMethods(t *testing.T) {
236237
for _, test := range []struct {
237-
description string
238-
serverSetup func(string) (string, tearDown, error)
239-
service string
240-
request *drapbv1beta1.NodeUnprepareResourcesRequest
238+
description string
239+
serverSetup func(string) (string, tearDown, error)
240+
service string
241+
chosenService string
242+
expectError string
241243
}{
242244
{
243-
description: "server supports v1alpha4",
244-
serverSetup: setupFakeGRPCServer,
245-
service: drapbv1alpha4.NodeService,
246-
request: &drapbv1beta1.NodeUnprepareResourcesRequest{},
245+
description: "v1alpha4",
246+
serverSetup: setupFakeGRPCServer,
247+
service: drapbv1alpha4.NodeService,
248+
chosenService: drapbv1alpha4.NodeService,
249+
},
250+
{
251+
description: "v1beta1",
252+
serverSetup: setupFakeGRPCServer,
253+
service: drapbv1beta1.DRAPluginService,
254+
chosenService: drapbv1beta1.DRAPluginService,
247255
},
248256
{
249-
description: "server supports v1beta1",
250-
serverSetup: setupFakeGRPCServer,
251-
service: drapbv1beta1.DRAPluginService,
252-
request: &drapbv1beta1.NodeUnprepareResourcesRequest{},
257+
// In practice, such a mismatch between plugin and kubelet should not happen.
258+
description: "mismatch",
259+
serverSetup: setupFakeGRPCServer,
260+
service: drapbv1beta1.DRAPluginService,
261+
chosenService: drapbv1alpha4.NodeService,
262+
expectError: "unknown service v1alpha3.Node",
263+
},
264+
{
265+
// In practice, kubelet wouldn't choose an invalid service.
266+
description: "internal-error",
267+
serverSetup: setupFakeGRPCServer,
268+
service: drapbv1beta1.DRAPluginService,
269+
chosenService: "some-other-service",
270+
expectError: "unsupported chosen service",
253271
},
254272
} {
255273
t.Run(test.description, func(t *testing.T) {
@@ -265,7 +283,7 @@ func TestNodeUnprepareResources(t *testing.T) {
265283
name: pluginName,
266284
backgroundCtx: tCtx,
267285
endpoint: addr,
268-
chosenService: test.service,
286+
chosenService: test.chosenService,
269287
clientCallTimeout: defaultClientCallTimeout,
270288
}
271289

@@ -288,10 +306,23 @@ func TestNodeUnprepareResources(t *testing.T) {
288306
t.Fatal(err)
289307
}
290308

291-
_, err = client.NodeUnprepareResources(tCtx, test.request)
292-
if err != nil {
293-
t.Fatal(err)
294-
}
309+
_, err = client.NodePrepareResources(tCtx, &drapbv1beta1.NodePrepareResourcesRequest{})
310+
assertError(t, test.expectError, err)
311+
312+
_, err = client.NodeUnprepareResources(tCtx, &drapbv1beta1.NodeUnprepareResourcesRequest{})
313+
assertError(t, test.expectError, err)
295314
})
296315
}
297316
}
317+
318+
func assertError(t *testing.T, expectError string, err error) {
319+
t.Helper()
320+
switch {
321+
case err != nil && expectError == "":
322+
t.Errorf("Expected no error, got: %v", err)
323+
case err == nil && expectError != "":
324+
t.Errorf("Expected error %q, got none", expectError)
325+
case err != nil && !strings.Contains(err.Error(), expectError):
326+
t.Errorf("Expected error %q, got: %v", expectError, err)
327+
}
328+
}

pkg/kubelet/cm/dra/plugin/registration_test.go

Lines changed: 170 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,207 @@ limitations under the License.
1717
package plugin
1818

1919
import (
20+
"sort"
21+
"strings"
22+
"sync/atomic"
2023
"testing"
24+
"time"
2125

2226
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
2329
v1 "k8s.io/api/core/v1"
30+
resourceapi "k8s.io/api/resource/v1beta1"
2431
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/fields"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/client-go/kubernetes"
35+
"k8s.io/client-go/kubernetes/fake"
36+
cgotesting "k8s.io/client-go/testing"
37+
drapbv1alpha4 "k8s.io/kubelet/pkg/apis/dra/v1alpha4"
2538
drapb "k8s.io/kubelet/pkg/apis/dra/v1beta1"
39+
"k8s.io/kubernetes/test/utils/ktesting"
40+
"k8s.io/utils/ptr"
41+
)
42+
43+
const (
44+
nodeName = "worker"
45+
pluginA = "pluginA"
46+
endpointA = "endpointA"
47+
pluginB = "pluginB"
48+
endpointB = "endpointB"
2649
)
2750

2851
func getFakeNode() (*v1.Node, error) {
29-
return &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "worker"}}, nil
52+
return &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}, nil
3053
}
3154

32-
func TestRegistrationHandler_ValidatePlugin(t *testing.T) {
33-
newRegistrationHandler := func() *RegistrationHandler {
34-
return NewRegistrationHandler(nil, getFakeNode)
55+
func TestRegistrationHandler(t *testing.T) {
56+
slice := &resourceapi.ResourceSlice{
57+
ObjectMeta: metav1.ObjectMeta{Name: "test-slice"},
58+
Spec: resourceapi.ResourceSliceSpec{
59+
NodeName: nodeName,
60+
},
3561
}
3662

3763
for _, test := range []struct {
3864
description string
39-
handler func() *RegistrationHandler
4065
pluginName string
4166
endpoint string
67+
withClient bool
4268
supportedServices []string
4369
shouldError bool
70+
chosenService string
4471
}{
4572
{
46-
description: "no versions provided",
47-
handler: newRegistrationHandler,
73+
description: "no-services-provided",
74+
pluginName: pluginB,
75+
endpoint: endpointB,
4876
shouldError: true,
4977
},
5078
{
51-
description: "should validate the plugin",
52-
handler: newRegistrationHandler,
53-
pluginName: "this-is-a-dummy-plugin-with-a-long-name-so-it-doesnt-collide",
79+
description: "current-service",
80+
pluginName: pluginB,
81+
endpoint: endpointB,
82+
supportedServices: []string{drapb.DRAPluginService},
83+
chosenService: drapb.DRAPluginService,
84+
},
85+
{
86+
description: "two-services",
87+
pluginName: pluginB,
88+
endpoint: endpointB,
89+
supportedServices: []string{drapbv1alpha4.NodeService, drapb.DRAPluginService},
90+
chosenService: drapb.DRAPluginService,
91+
},
92+
{
93+
description: "old-service",
94+
pluginName: pluginB,
95+
endpoint: endpointB,
96+
supportedServices: []string{drapbv1alpha4.NodeService},
97+
chosenService: drapbv1alpha4.NodeService,
98+
},
99+
{
100+
// Legacy behavior.
101+
description: "version",
102+
pluginName: pluginB,
103+
endpoint: endpointB,
104+
supportedServices: []string{"1.0.0"},
105+
chosenService: drapbv1alpha4.NodeService,
106+
},
107+
{
108+
description: "replace",
109+
pluginName: pluginA,
110+
endpoint: endpointB,
111+
supportedServices: []string{drapb.DRAPluginService},
112+
chosenService: drapb.DRAPluginService,
113+
},
114+
{
115+
description: "manage-resource-slices",
116+
withClient: true,
117+
pluginName: pluginB,
118+
endpoint: endpointB,
54119
supportedServices: []string{drapb.DRAPluginService},
120+
chosenService: drapb.DRAPluginService,
55121
},
56122
} {
57123
t.Run(test.description, func(t *testing.T) {
58-
handler := test.handler()
59-
err := handler.ValidatePlugin(test.pluginName, test.endpoint, test.supportedServices)
124+
tCtx := ktesting.Init(t)
125+
126+
// Stand-alone kubelet has no connection to an
127+
// apiserver, so faking one is optional.
128+
var client kubernetes.Interface
129+
var deleteCollectionForDriver atomic.Pointer[string]
130+
if test.withClient {
131+
fakeClient := fake.NewClientset(slice)
132+
fakeClient.AddReactor("delete-collection", "resourceslices", func(action cgotesting.Action) (bool, runtime.Object, error) {
133+
deleteAction := action.(cgotesting.DeleteCollectionAction)
134+
restrictions := deleteAction.GetListRestrictions()
135+
sliceFields := fields.Set{"spec.nodeName": nodeName}
136+
forDriver := deleteCollectionForDriver.Load()
137+
if forDriver != nil {
138+
sliceFields["spec.driver"] = *forDriver
139+
}
140+
fieldsSelector := fields.SelectorFromSet(sliceFields)
141+
// The order of field requirements is random because it comes
142+
// from a map. We need to sort.
143+
normalize := func(selector string) string {
144+
requirements := strings.Split(selector, ",")
145+
sort.Strings(requirements)
146+
return strings.Join(requirements, ",")
147+
}
148+
assert.Equal(t, "", restrictions.Labels.String(), "label selector in DeleteCollection")
149+
assert.Equal(t, normalize(fieldsSelector.String()), normalize(restrictions.Fields.String()), "field selector in DeleteCollection")
150+
151+
// There's only one object that could get matched, so delete it.
152+
// Delete doesn't return an error if already deleted, which is what
153+
// we need here (no error when nothing to delete).
154+
err := fakeClient.Tracker().Delete(resourceapi.SchemeGroupVersion.WithResource("resourceslices"), "", slice.Name)
155+
return true, nil, err
156+
})
157+
client = fakeClient
158+
}
159+
160+
// The handler wipes all slices at startup.
161+
handler := NewRegistrationHandler(client, getFakeNode)
162+
requireNoSlices := func() {
163+
t.Helper()
164+
if client == nil {
165+
return
166+
}
167+
require.EventuallyWithT(t, func(t *assert.CollectT) {
168+
slices, err := client.ResourceV1beta1().ResourceSlices().List(tCtx, metav1.ListOptions{})
169+
if !assert.NoError(t, err, "list slices") {
170+
return
171+
}
172+
assert.Empty(t, slices.Items, "slices")
173+
}, time.Minute, time.Second)
174+
}
175+
requireNoSlices()
176+
177+
// Simulate one existing plugin A.
178+
err := handler.RegisterPlugin(pluginA, endpointA, []string{drapb.DRAPluginService}, nil)
179+
require.NoError(t, err)
180+
181+
err = handler.ValidatePlugin(test.pluginName, test.endpoint, test.supportedServices)
182+
if test.shouldError {
183+
require.Error(t, err)
184+
} else {
185+
require.NoError(t, err)
186+
}
187+
if err != nil {
188+
return
189+
}
190+
if test.pluginName != pluginA {
191+
require.Nil(t, draPlugins.get(test.pluginName), "not registered yet")
192+
}
193+
194+
// Add plugin for the first time.
195+
err = handler.RegisterPlugin(test.pluginName, test.endpoint, test.supportedServices, nil)
60196
if test.shouldError {
61-
assert.Error(t, err)
197+
require.Error(t, err)
62198
} else {
63-
assert.NoError(t, err)
199+
require.NoError(t, err)
64200
}
201+
plugin := draPlugins.get(test.pluginName)
202+
assert.NotNil(t, plugin, "plugin should be registered")
203+
t.Cleanup(func() {
204+
if client != nil {
205+
// Create the slice as if the plugin had done that while it runs.
206+
_, err := client.ResourceV1beta1().ResourceSlices().Create(tCtx, slice, metav1.CreateOptions{})
207+
assert.NoError(t, err, "recreate slice")
208+
}
209+
210+
handler.DeRegisterPlugin(test.pluginName)
211+
// Nop.
212+
handler.DeRegisterPlugin(test.pluginName)
213+
214+
// Deleted by the kubelet after deregistration, now specifically
215+
// for that plugin (checked by the fake client reactor).
216+
deleteCollectionForDriver.Store(ptr.To(test.pluginName))
217+
requireNoSlices()
218+
})
219+
assert.Equal(t, test.endpoint, plugin.endpoint, "plugin endpoint")
220+
assert.Equal(t, test.chosenService, plugin.chosenService, "chosen service")
65221
})
66222
}
67-
68-
t.Cleanup(func() {
69-
handler := newRegistrationHandler()
70-
handler.DeRegisterPlugin("this-plugin-already-exists-and-has-a-long-name-so-it-doesnt-collide")
71-
handler.DeRegisterPlugin("this-is-a-dummy-plugin-with-a-long-name-so-it-doesnt-collide")
72-
})
73223
}

0 commit comments

Comments
 (0)