@@ -17,57 +17,207 @@ limitations under the License.
17
17
package plugin
18
18
19
19
import (
20
+ "sort"
21
+ "strings"
22
+ "sync/atomic"
20
23
"testing"
24
+ "time"
21
25
22
26
"github.com/stretchr/testify/assert"
27
+ "github.com/stretchr/testify/require"
28
+
23
29
v1 "k8s.io/api/core/v1"
30
+ resourceapi "k8s.io/api/resource/v1beta1"
24
31
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"
25
38
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"
26
49
)
27
50
28
51
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
30
53
}
31
54
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
+ },
35
61
}
36
62
37
63
for _ , test := range []struct {
38
64
description string
39
- handler func () * RegistrationHandler
40
65
pluginName string
41
66
endpoint string
67
+ withClient bool
42
68
supportedServices []string
43
69
shouldError bool
70
+ chosenService string
44
71
}{
45
72
{
46
- description : "no versions provided" ,
47
- handler : newRegistrationHandler ,
73
+ description : "no-services-provided" ,
74
+ pluginName : pluginB ,
75
+ endpoint : endpointB ,
48
76
shouldError : true ,
49
77
},
50
78
{
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 ,
54
119
supportedServices : []string {drapb .DRAPluginService },
120
+ chosenService : drapb .DRAPluginService ,
55
121
},
56
122
} {
57
123
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 )
60
196
if test .shouldError {
61
- assert .Error (t , err )
197
+ require .Error (t , err )
62
198
} else {
63
- assert .NoError (t , err )
199
+ require .NoError (t , err )
64
200
}
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" )
65
221
})
66
222
}
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
- })
73
223
}
0 commit comments