mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-13 22:05:59 +00:00
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%.
This commit is contained in:
parent
1193ff1271
commit
a1b8e9d3a7
@ -22,6 +22,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -232,24 +233,41 @@ func TestNewDRAPluginClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeUnprepareResources(t *testing.T) {
|
func TestGRPCMethods(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
description string
|
description string
|
||||||
serverSetup func(string) (string, tearDown, error)
|
serverSetup func(string) (string, tearDown, error)
|
||||||
service string
|
service string
|
||||||
request *drapbv1beta1.NodeUnprepareResourcesRequest
|
chosenService string
|
||||||
|
expectError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
description: "server supports v1alpha4",
|
description: "v1alpha4",
|
||||||
serverSetup: setupFakeGRPCServer,
|
serverSetup: setupFakeGRPCServer,
|
||||||
service: drapbv1alpha4.NodeService,
|
service: drapbv1alpha4.NodeService,
|
||||||
request: &drapbv1beta1.NodeUnprepareResourcesRequest{},
|
chosenService: drapbv1alpha4.NodeService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "server supports v1beta1",
|
description: "v1beta1",
|
||||||
serverSetup: setupFakeGRPCServer,
|
serverSetup: setupFakeGRPCServer,
|
||||||
service: drapbv1beta1.DRAPluginService,
|
service: drapbv1beta1.DRAPluginService,
|
||||||
request: &drapbv1beta1.NodeUnprepareResourcesRequest{},
|
chosenService: drapbv1beta1.DRAPluginService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// In practice, such a mismatch between plugin and kubelet should not happen.
|
||||||
|
description: "mismatch",
|
||||||
|
serverSetup: setupFakeGRPCServer,
|
||||||
|
service: drapbv1beta1.DRAPluginService,
|
||||||
|
chosenService: drapbv1alpha4.NodeService,
|
||||||
|
expectError: "unknown service v1alpha3.Node",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// In practice, kubelet wouldn't choose an invalid service.
|
||||||
|
description: "internal-error",
|
||||||
|
serverSetup: setupFakeGRPCServer,
|
||||||
|
service: drapbv1beta1.DRAPluginService,
|
||||||
|
chosenService: "some-other-service",
|
||||||
|
expectError: "unsupported chosen service",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
@ -265,7 +283,7 @@ func TestNodeUnprepareResources(t *testing.T) {
|
|||||||
name: pluginName,
|
name: pluginName,
|
||||||
backgroundCtx: tCtx,
|
backgroundCtx: tCtx,
|
||||||
endpoint: addr,
|
endpoint: addr,
|
||||||
chosenService: test.service,
|
chosenService: test.chosenService,
|
||||||
clientCallTimeout: defaultClientCallTimeout,
|
clientCallTimeout: defaultClientCallTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,10 +306,23 @@ func TestNodeUnprepareResources(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.NodeUnprepareResources(tCtx, test.request)
|
_, err = client.NodePrepareResources(tCtx, &drapbv1beta1.NodePrepareResourcesRequest{})
|
||||||
if err != nil {
|
assertError(t, test.expectError, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
_, err = client.NodeUnprepareResources(tCtx, &drapbv1beta1.NodeUnprepareResourcesRequest{})
|
||||||
|
assertError(t, test.expectError, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertError(t *testing.T, expectError string, err error) {
|
||||||
|
t.Helper()
|
||||||
|
switch {
|
||||||
|
case err != nil && expectError == "":
|
||||||
|
t.Errorf("Expected no error, got: %v", err)
|
||||||
|
case err == nil && expectError != "":
|
||||||
|
t.Errorf("Expected error %q, got none", expectError)
|
||||||
|
case err != nil && !strings.Contains(err.Error(), expectError):
|
||||||
|
t.Errorf("Expected error %q, got: %v", expectError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,57 +17,207 @@ limitations under the License.
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
resourceapi "k8s.io/api/resource/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
cgotesting "k8s.io/client-go/testing"
|
||||||
|
drapbv1alpha4 "k8s.io/kubelet/pkg/apis/dra/v1alpha4"
|
||||||
drapb "k8s.io/kubelet/pkg/apis/dra/v1beta1"
|
drapb "k8s.io/kubelet/pkg/apis/dra/v1beta1"
|
||||||
|
"k8s.io/kubernetes/test/utils/ktesting"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeName = "worker"
|
||||||
|
pluginA = "pluginA"
|
||||||
|
endpointA = "endpointA"
|
||||||
|
pluginB = "pluginB"
|
||||||
|
endpointB = "endpointB"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getFakeNode() (*v1.Node, error) {
|
func getFakeNode() (*v1.Node, error) {
|
||||||
return &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "worker"}}, nil
|
return &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegistrationHandler_ValidatePlugin(t *testing.T) {
|
func TestRegistrationHandler(t *testing.T) {
|
||||||
newRegistrationHandler := func() *RegistrationHandler {
|
slice := &resourceapi.ResourceSlice{
|
||||||
return NewRegistrationHandler(nil, getFakeNode)
|
ObjectMeta: metav1.ObjectMeta{Name: "test-slice"},
|
||||||
|
Spec: resourceapi.ResourceSliceSpec{
|
||||||
|
NodeName: nodeName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
description string
|
description string
|
||||||
handler func() *RegistrationHandler
|
|
||||||
pluginName string
|
pluginName string
|
||||||
endpoint string
|
endpoint string
|
||||||
|
withClient bool
|
||||||
supportedServices []string
|
supportedServices []string
|
||||||
shouldError bool
|
shouldError bool
|
||||||
|
chosenService string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
description: "no versions provided",
|
description: "no-services-provided",
|
||||||
handler: newRegistrationHandler,
|
pluginName: pluginB,
|
||||||
|
endpoint: endpointB,
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "should validate the plugin",
|
description: "current-service",
|
||||||
handler: newRegistrationHandler,
|
pluginName: pluginB,
|
||||||
pluginName: "this-is-a-dummy-plugin-with-a-long-name-so-it-doesnt-collide",
|
endpoint: endpointB,
|
||||||
supportedServices: []string{drapb.DRAPluginService},
|
supportedServices: []string{drapb.DRAPluginService},
|
||||||
|
chosenService: drapb.DRAPluginService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "two-services",
|
||||||
|
pluginName: pluginB,
|
||||||
|
endpoint: endpointB,
|
||||||
|
supportedServices: []string{drapbv1alpha4.NodeService, drapb.DRAPluginService},
|
||||||
|
chosenService: drapb.DRAPluginService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "old-service",
|
||||||
|
pluginName: pluginB,
|
||||||
|
endpoint: endpointB,
|
||||||
|
supportedServices: []string{drapbv1alpha4.NodeService},
|
||||||
|
chosenService: drapbv1alpha4.NodeService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Legacy behavior.
|
||||||
|
description: "version",
|
||||||
|
pluginName: pluginB,
|
||||||
|
endpoint: endpointB,
|
||||||
|
supportedServices: []string{"1.0.0"},
|
||||||
|
chosenService: drapbv1alpha4.NodeService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "replace",
|
||||||
|
pluginName: pluginA,
|
||||||
|
endpoint: endpointB,
|
||||||
|
supportedServices: []string{drapb.DRAPluginService},
|
||||||
|
chosenService: drapb.DRAPluginService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "manage-resource-slices",
|
||||||
|
withClient: true,
|
||||||
|
pluginName: pluginB,
|
||||||
|
endpoint: endpointB,
|
||||||
|
supportedServices: []string{drapb.DRAPluginService},
|
||||||
|
chosenService: drapb.DRAPluginService,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
handler := test.handler()
|
tCtx := ktesting.Init(t)
|
||||||
err := handler.ValidatePlugin(test.pluginName, test.endpoint, test.supportedServices)
|
|
||||||
if test.shouldError {
|
// Stand-alone kubelet has no connection to an
|
||||||
assert.Error(t, err)
|
// apiserver, so faking one is optional.
|
||||||
} else {
|
var client kubernetes.Interface
|
||||||
assert.NoError(t, err)
|
var deleteCollectionForDriver atomic.Pointer[string]
|
||||||
|
if test.withClient {
|
||||||
|
fakeClient := fake.NewClientset(slice)
|
||||||
|
fakeClient.AddReactor("delete-collection", "resourceslices", func(action cgotesting.Action) (bool, runtime.Object, error) {
|
||||||
|
deleteAction := action.(cgotesting.DeleteCollectionAction)
|
||||||
|
restrictions := deleteAction.GetListRestrictions()
|
||||||
|
sliceFields := fields.Set{"spec.nodeName": nodeName}
|
||||||
|
forDriver := deleteCollectionForDriver.Load()
|
||||||
|
if forDriver != nil {
|
||||||
|
sliceFields["spec.driver"] = *forDriver
|
||||||
}
|
}
|
||||||
|
fieldsSelector := fields.SelectorFromSet(sliceFields)
|
||||||
|
// The order of field requirements is random because it comes
|
||||||
|
// from a map. We need to sort.
|
||||||
|
normalize := func(selector string) string {
|
||||||
|
requirements := strings.Split(selector, ",")
|
||||||
|
sort.Strings(requirements)
|
||||||
|
return strings.Join(requirements, ",")
|
||||||
|
}
|
||||||
|
assert.Equal(t, "", restrictions.Labels.String(), "label selector in DeleteCollection")
|
||||||
|
assert.Equal(t, normalize(fieldsSelector.String()), normalize(restrictions.Fields.String()), "field selector in DeleteCollection")
|
||||||
|
|
||||||
|
// There's only one object that could get matched, so delete it.
|
||||||
|
// Delete doesn't return an error if already deleted, which is what
|
||||||
|
// we need here (no error when nothing to delete).
|
||||||
|
err := fakeClient.Tracker().Delete(resourceapi.SchemeGroupVersion.WithResource("resourceslices"), "", slice.Name)
|
||||||
|
return true, nil, err
|
||||||
})
|
})
|
||||||
|
client = fakeClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The handler wipes all slices at startup.
|
||||||
|
handler := NewRegistrationHandler(client, getFakeNode)
|
||||||
|
requireNoSlices := func() {
|
||||||
|
t.Helper()
|
||||||
|
if client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||||
|
slices, err := client.ResourceV1beta1().ResourceSlices().List(tCtx, metav1.ListOptions{})
|
||||||
|
if !assert.NoError(t, err, "list slices") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Empty(t, slices.Items, "slices")
|
||||||
|
}, time.Minute, time.Second)
|
||||||
|
}
|
||||||
|
requireNoSlices()
|
||||||
|
|
||||||
|
// Simulate one existing plugin A.
|
||||||
|
err := handler.RegisterPlugin(pluginA, endpointA, []string{drapb.DRAPluginService}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = handler.ValidatePlugin(test.pluginName, test.endpoint, test.supportedServices)
|
||||||
|
if test.shouldError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if test.pluginName != pluginA {
|
||||||
|
require.Nil(t, draPlugins.get(test.pluginName), "not registered yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add plugin for the first time.
|
||||||
|
err = handler.RegisterPlugin(test.pluginName, test.endpoint, test.supportedServices, nil)
|
||||||
|
if test.shouldError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
plugin := draPlugins.get(test.pluginName)
|
||||||
|
assert.NotNil(t, plugin, "plugin should be registered")
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
handler := newRegistrationHandler()
|
if client != nil {
|
||||||
handler.DeRegisterPlugin("this-plugin-already-exists-and-has-a-long-name-so-it-doesnt-collide")
|
// Create the slice as if the plugin had done that while it runs.
|
||||||
handler.DeRegisterPlugin("this-is-a-dummy-plugin-with-a-long-name-so-it-doesnt-collide")
|
_, err := client.ResourceV1beta1().ResourceSlices().Create(tCtx, slice, metav1.CreateOptions{})
|
||||||
|
assert.NoError(t, err, "recreate slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.DeRegisterPlugin(test.pluginName)
|
||||||
|
// Nop.
|
||||||
|
handler.DeRegisterPlugin(test.pluginName)
|
||||||
|
|
||||||
|
// Deleted by the kubelet after deregistration, now specifically
|
||||||
|
// for that plugin (checked by the fake client reactor).
|
||||||
|
deleteCollectionForDriver.Store(ptr.To(test.pluginName))
|
||||||
|
requireNoSlices()
|
||||||
|
})
|
||||||
|
assert.Equal(t, test.endpoint, plugin.endpoint, "plugin endpoint")
|
||||||
|
assert.Equal(t, test.chosenService, plugin.chosenService, "chosen service")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user