mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 12:07:47 +00:00
e2e dra: collect and check GRPC calls
If kubelet plugin registration fails, it would be good to know more about the communication with kubelet. Capturing the GRPC calls and then checking that makes the failure messages more informative. Here's an example where a failure was triggered by temporarily modifying the check so that it didn't find the call: [FAILED] Timed out after 30.000s. Expected: <[]app.GRPCCall | len:2, cap:2>: [ { FullMethod: "/pluginregistration.Registration/GetInfo", Request: {}, Response: endpoint: /var/lib/kubelet/plugins/test-driver/dra.sock name: test-driver.cdi.k8s.io supported_versions: - 1.0.0 type: DRAPlugin, Err: nil, }, { FullMethod: "/pluginregistration.Registration/NotifyRegistrationStatus", Request: plugin_registered: true, Response: {}, Err: nil, }, ] to contain successful NotifyRegistrationStatus call
This commit is contained in:
parent
1c9f08a1c5
commit
d0a64739e2
@ -135,10 +135,11 @@ func KubeletPluginSocketPath(path string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRPCInterceptor is called for each incoming gRPC method call.
|
// GRPCInterceptor is called for each incoming gRPC method call. This option
|
||||||
|
// may be used more than once and each interceptor will get called.
|
||||||
func GRPCInterceptor(interceptor grpc.UnaryServerInterceptor) Option {
|
func GRPCInterceptor(interceptor grpc.UnaryServerInterceptor) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.interceptor = interceptor
|
o.interceptors = append(o.interceptors, interceptor)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,7 +151,7 @@ type options struct {
|
|||||||
draEndpoint endpoint
|
draEndpoint endpoint
|
||||||
draAddress string
|
draAddress string
|
||||||
pluginRegistrationEndpoint endpoint
|
pluginRegistrationEndpoint endpoint
|
||||||
interceptor grpc.UnaryServerInterceptor
|
interceptors []grpc.UnaryServerInterceptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// draPlugin combines the kubelet registration service and the DRA node plugin
|
// draPlugin combines the kubelet registration service and the DRA node plugin
|
||||||
@ -190,7 +191,7 @@ func Start(nodeServer drapbv1.NodeServer, opts ...Option) (result DRAPlugin, fin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the node plugin gRPC server first to ensure that it is ready.
|
// Run the node plugin gRPC server first to ensure that it is ready.
|
||||||
plugin, err := startGRPCServer(klog.LoggerWithName(o.logger, "dra"), o.grpcVerbosity, o.interceptor, o.draEndpoint, func(grpcServer *grpc.Server) {
|
plugin, err := startGRPCServer(klog.LoggerWithName(o.logger, "dra"), o.grpcVerbosity, o.interceptors, o.draEndpoint, func(grpcServer *grpc.Server) {
|
||||||
drapbv1.RegisterNodeServer(grpcServer, nodeServer)
|
drapbv1.RegisterNodeServer(grpcServer, nodeServer)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -209,7 +210,7 @@ func Start(nodeServer drapbv1.NodeServer, opts ...Option) (result DRAPlugin, fin
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Now make it available to kubelet.
|
// Now make it available to kubelet.
|
||||||
registrar, err := startRegistrar(klog.LoggerWithName(o.logger, "registrar"), o.grpcVerbosity, o.interceptor, o.driverName, o.draAddress, o.pluginRegistrationEndpoint)
|
registrar, err := startRegistrar(klog.LoggerWithName(o.logger, "registrar"), o.grpcVerbosity, o.interceptors, o.driverName, o.draAddress, o.pluginRegistrationEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("start registrar: %v", err)
|
return nil, fmt.Errorf("start registrar: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ type nodeRegistrar struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startRegistrar returns a running instance.
|
// startRegistrar returns a running instance.
|
||||||
func startRegistrar(logger klog.Logger, grpcVerbosity int, interceptor grpc.UnaryServerInterceptor, driverName string, endpoint string, pluginRegistrationEndpoint endpoint) (*nodeRegistrar, error) {
|
func startRegistrar(logger klog.Logger, grpcVerbosity int, interceptors []grpc.UnaryServerInterceptor, driverName string, endpoint string, pluginRegistrationEndpoint endpoint) (*nodeRegistrar, error) {
|
||||||
n := &nodeRegistrar{
|
n := &nodeRegistrar{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
registrationServer: registrationServer{
|
registrationServer: registrationServer{
|
||||||
@ -40,7 +40,7 @@ func startRegistrar(logger klog.Logger, grpcVerbosity int, interceptor grpc.Unar
|
|||||||
supportedVersions: []string{"1.0.0"}, // TODO: is this correct?
|
supportedVersions: []string{"1.0.0"}, // TODO: is this correct?
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
s, err := startGRPCServer(logger, grpcVerbosity, interceptor, pluginRegistrationEndpoint, func(grpcServer *grpc.Server) {
|
s, err := startGRPCServer(logger, grpcVerbosity, interceptors, pluginRegistrationEndpoint, func(grpcServer *grpc.Server) {
|
||||||
registerapi.RegisterRegistrationServer(grpcServer, n)
|
registerapi.RegisterRegistrationServer(grpcServer, n)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -54,7 +54,7 @@ type endpoint struct {
|
|||||||
|
|
||||||
// startGRPCServer sets up the GRPC server on a Unix domain socket and spawns a goroutine
|
// startGRPCServer sets up the GRPC server on a Unix domain socket and spawns a goroutine
|
||||||
// which handles requests for arbitrary services.
|
// which handles requests for arbitrary services.
|
||||||
func startGRPCServer(logger klog.Logger, grpcVerbosity int, interceptor grpc.UnaryServerInterceptor, endpoint endpoint, services ...registerService) (*grpcServer, error) {
|
func startGRPCServer(logger klog.Logger, grpcVerbosity int, interceptors []grpc.UnaryServerInterceptor, endpoint endpoint, services ...registerService) (*grpcServer, error) {
|
||||||
s := &grpcServer{
|
s := &grpcServer{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
@ -79,15 +79,13 @@ func startGRPCServer(logger klog.Logger, grpcVerbosity int, interceptor grpc.Una
|
|||||||
// Run a gRPC server. It will close the listening socket when
|
// Run a gRPC server. It will close the listening socket when
|
||||||
// shutting down, so we don't need to do that.
|
// shutting down, so we don't need to do that.
|
||||||
var opts []grpc.ServerOption
|
var opts []grpc.ServerOption
|
||||||
var interceptors []grpc.UnaryServerInterceptor
|
var finalInterceptors []grpc.UnaryServerInterceptor
|
||||||
if grpcVerbosity >= 0 {
|
if grpcVerbosity >= 0 {
|
||||||
interceptors = append(interceptors, s.interceptor)
|
finalInterceptors = append(finalInterceptors, s.interceptor)
|
||||||
}
|
}
|
||||||
if interceptor != nil {
|
finalInterceptors = append(finalInterceptors, interceptors...)
|
||||||
interceptors = append(interceptors, interceptor)
|
if len(finalInterceptors) >= 0 {
|
||||||
}
|
opts = append(opts, grpc.ChainUnaryInterceptor(finalInterceptors...))
|
||||||
if len(interceptors) >= 0 {
|
|
||||||
opts = append(opts, grpc.ChainUnaryInterceptor(interceptors...))
|
|
||||||
}
|
}
|
||||||
s.server = grpc.NewServer(opts...)
|
s.server = grpc.NewServer(opts...)
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
|
@ -240,14 +240,14 @@ func (d *Driver) SetUp(nodes *Nodes, resources app.Resources) {
|
|||||||
|
|
||||||
// Wait for registration.
|
// Wait for registration.
|
||||||
ginkgo.By("wait for plugin registration")
|
ginkgo.By("wait for plugin registration")
|
||||||
gomega.Eventually(func() []string {
|
gomega.Eventually(func() map[string][]app.GRPCCall {
|
||||||
var notRegistered []string
|
notRegistered := make(map[string][]app.GRPCCall)
|
||||||
for nodename, plugin := range d.Nodes {
|
for nodename, plugin := range d.Nodes {
|
||||||
if !plugin.IsRegistered() {
|
calls := plugin.GetGRPCCalls()
|
||||||
notRegistered = append(notRegistered, nodename)
|
if contains, err := app.BeRegistered.Match(calls); err != nil || !contains {
|
||||||
|
notRegistered[nodename] = calls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(notRegistered)
|
|
||||||
return notRegistered
|
return notRegistered
|
||||||
}).WithTimeout(time.Minute).Should(gomega.BeEmpty(), "hosts where the plugin has not been registered yet")
|
}).WithTimeout(time.Minute).Should(gomega.BeEmpty(), "hosts where the plugin has not been registered yet")
|
||||||
}
|
}
|
||||||
|
32
test/e2e/dra/test-driver/app/gomega.go
Normal file
32
test/e2e/dra/test-driver/app/gomega.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/onsi/gomega/gcustom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BeRegistered checks that plugin registration has completed.
|
||||||
|
var BeRegistered = gcustom.MakeMatcher(func(actualCalls []GRPCCall) (bool, error) {
|
||||||
|
for _, call := range actualCalls {
|
||||||
|
if call.FullMethod == "/pluginregistration.Registration/NotifyRegistrationStatus" &&
|
||||||
|
call.Err == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}).WithMessage("contain successful NotifyRegistrationStatus call")
|
@ -24,6 +24,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"k8s.io/dynamic-resource-allocation/kubeletplugin"
|
"k8s.io/dynamic-resource-allocation/kubeletplugin"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1alpha2"
|
drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1alpha2"
|
||||||
@ -38,8 +40,23 @@ type ExamplePlugin struct {
|
|||||||
driverName string
|
driverName string
|
||||||
nodeName string
|
nodeName string
|
||||||
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
prepared map[ClaimID]bool
|
prepared map[ClaimID]bool
|
||||||
|
gRPCCalls []GRPCCall
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCCall struct {
|
||||||
|
// FullMethod is the fully qualified, e.g. /package.service/method.
|
||||||
|
FullMethod string
|
||||||
|
|
||||||
|
// Request contains the parameters of the call.
|
||||||
|
Request interface{}
|
||||||
|
|
||||||
|
// Response contains the reply of the plugin. It is nil for calls that are in progress.
|
||||||
|
Response interface{}
|
||||||
|
|
||||||
|
// Err contains the error return value of the plugin. It is nil for calls that are in progress or succeeded.
|
||||||
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaimID contains both claim name and UID to simplify debugging. The
|
// ClaimID contains both claim name and UID to simplify debugging. The
|
||||||
@ -94,6 +111,7 @@ func StartPlugin(logger klog.Logger, cdiDir, driverName string, nodeName string,
|
|||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
kubeletplugin.Logger(logger),
|
kubeletplugin.Logger(logger),
|
||||||
kubeletplugin.DriverName(driverName),
|
kubeletplugin.DriverName(driverName),
|
||||||
|
kubeletplugin.GRPCInterceptor(ex.recordGRPCCall),
|
||||||
)
|
)
|
||||||
d, err := kubeletplugin.Start(ex, opts...)
|
d, err := kubeletplugin.Start(ex, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -206,3 +224,35 @@ func (ex *ExamplePlugin) GetPreparedResources() []ClaimID {
|
|||||||
}
|
}
|
||||||
return prepared
|
return prepared
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ex *ExamplePlugin) recordGRPCCall(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||||
|
call := GRPCCall{
|
||||||
|
FullMethod: info.FullMethod,
|
||||||
|
Request: req,
|
||||||
|
}
|
||||||
|
ex.mutex.Lock()
|
||||||
|
ex.gRPCCalls = append(ex.gRPCCalls, call)
|
||||||
|
index := len(ex.gRPCCalls) - 1
|
||||||
|
ex.mutex.Unlock()
|
||||||
|
|
||||||
|
// We don't hold the mutex here to allow concurrent calls.
|
||||||
|
call.Response, call.Err = handler(ctx, req)
|
||||||
|
|
||||||
|
ex.mutex.Lock()
|
||||||
|
ex.gRPCCalls[index] = call
|
||||||
|
ex.mutex.Unlock()
|
||||||
|
|
||||||
|
return call.Response, call.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ex *ExamplePlugin) GetGRPCCalls() []GRPCCall {
|
||||||
|
ex.mutex.Lock()
|
||||||
|
defer ex.mutex.Unlock()
|
||||||
|
|
||||||
|
// We must return a new slice, otherwise adding new calls would become
|
||||||
|
// visible to the caller. We also need to copy the entries because
|
||||||
|
// they get mutated by recordGRPCCall.
|
||||||
|
calls := make([]GRPCCall, 0, len(ex.gRPCCalls))
|
||||||
|
calls = append(calls, ex.gRPCCalls...)
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user