k8s.io/kubernetes@v1.29.3/pkg/kubelet/cm/dra/plugin/client_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package plugin
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net"
    23  	"os"
    24  	"path/filepath"
    25  	"sync"
    26  	"testing"
    27  
    28  	"github.com/stretchr/testify/assert"
    29  	"google.golang.org/grpc"
    30  	drapbv1alpha2 "k8s.io/kubelet/pkg/apis/dra/v1alpha2"
    31  	drapbv1alpha3 "k8s.io/kubelet/pkg/apis/dra/v1alpha3"
    32  )
    33  
    34  type fakeV1alpha3GRPCServer struct {
    35  	drapbv1alpha3.UnimplementedNodeServer
    36  }
    37  
    38  func (f *fakeV1alpha3GRPCServer) NodePrepareResource(ctx context.Context, in *drapbv1alpha3.NodePrepareResourcesRequest) (*drapbv1alpha3.NodePrepareResourcesResponse, error) {
    39  	return &drapbv1alpha3.NodePrepareResourcesResponse{Claims: map[string]*drapbv1alpha3.NodePrepareResourceResponse{"dummy": {CDIDevices: []string{"dummy"}}}}, nil
    40  }
    41  
    42  func (f *fakeV1alpha3GRPCServer) NodeUnprepareResource(ctx context.Context, in *drapbv1alpha3.NodeUnprepareResourcesRequest) (*drapbv1alpha3.NodeUnprepareResourcesResponse, error) {
    43  	return &drapbv1alpha3.NodeUnprepareResourcesResponse{}, nil
    44  }
    45  
    46  type fakeV1alpha2GRPCServer struct {
    47  	drapbv1alpha2.UnimplementedNodeServer
    48  }
    49  
    50  func (f *fakeV1alpha2GRPCServer) NodePrepareResource(ctx context.Context, in *drapbv1alpha2.NodePrepareResourceRequest) (*drapbv1alpha2.NodePrepareResourceResponse, error) {
    51  	return &drapbv1alpha2.NodePrepareResourceResponse{CdiDevices: []string{"dummy"}}, nil
    52  }
    53  
    54  func (f *fakeV1alpha2GRPCServer) NodeUnprepareResource(ctx context.Context, in *drapbv1alpha2.NodeUnprepareResourceRequest) (*drapbv1alpha2.NodeUnprepareResourceResponse, error) {
    55  	return &drapbv1alpha2.NodeUnprepareResourceResponse{}, nil
    56  }
    57  
    58  type tearDown func()
    59  
    60  func setupFakeGRPCServer(version string) (string, tearDown, error) {
    61  	p, err := os.MkdirTemp("", "dra_plugin")
    62  	if err != nil {
    63  		return "", nil, err
    64  	}
    65  
    66  	closeCh := make(chan struct{})
    67  	addr := filepath.Join(p, "server.sock")
    68  	teardown := func() {
    69  		close(closeCh)
    70  		os.RemoveAll(addr)
    71  	}
    72  
    73  	listener, err := net.Listen("unix", addr)
    74  	if err != nil {
    75  		teardown()
    76  		return "", nil, err
    77  	}
    78  
    79  	s := grpc.NewServer()
    80  	switch version {
    81  	case v1alpha2Version:
    82  		fakeGRPCServer := &fakeV1alpha2GRPCServer{}
    83  		drapbv1alpha2.RegisterNodeServer(s, fakeGRPCServer)
    84  	case v1alpha3Version:
    85  		fakeGRPCServer := &fakeV1alpha3GRPCServer{}
    86  		drapbv1alpha3.RegisterNodeServer(s, fakeGRPCServer)
    87  	default:
    88  		return "", nil, fmt.Errorf("unsupported version: %s", version)
    89  	}
    90  
    91  	go func() {
    92  		go s.Serve(listener)
    93  		<-closeCh
    94  		s.GracefulStop()
    95  	}()
    96  
    97  	return addr, teardown, nil
    98  }
    99  
   100  func TestGRPCConnIsReused(t *testing.T) {
   101  	addr, teardown, err := setupFakeGRPCServer(v1alpha3Version)
   102  	if err != nil {
   103  		t.Fatal(err)
   104  	}
   105  	defer teardown()
   106  
   107  	reusedConns := make(map[*grpc.ClientConn]int)
   108  	wg := sync.WaitGroup{}
   109  	m := sync.Mutex{}
   110  
   111  	p := &plugin{
   112  		endpoint: addr,
   113  		version:  v1alpha3Version,
   114  	}
   115  
   116  	conn, err := p.getOrCreateGRPCConn()
   117  	defer func() {
   118  		err := conn.Close()
   119  		if err != nil {
   120  			t.Error(err)
   121  		}
   122  	}()
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	// ensure the plugin we are using is registered
   128  	draPlugins.add("dummy-plugin", p)
   129  	defer draPlugins.delete("dummy-plugin")
   130  
   131  	// we call `NodePrepareResource` 2 times and check whether a new connection is created or the same is reused
   132  	for i := 0; i < 2; i++ {
   133  		wg.Add(1)
   134  		go func() {
   135  			defer wg.Done()
   136  			client, err := NewDRAPluginClient("dummy-plugin")
   137  			if err != nil {
   138  				t.Error(err)
   139  				return
   140  			}
   141  
   142  			req := &drapbv1alpha3.NodePrepareResourcesRequest{
   143  				Claims: []*drapbv1alpha3.Claim{
   144  					{
   145  						Namespace:      "dummy-namespace",
   146  						Uid:            "dummy-uid",
   147  						Name:           "dummy-claim",
   148  						ResourceHandle: "dummy-resource",
   149  					},
   150  				},
   151  			}
   152  			client.NodePrepareResources(context.TODO(), req)
   153  
   154  			client.(*plugin).Lock()
   155  			conn := client.(*plugin).conn
   156  			client.(*plugin).Unlock()
   157  
   158  			m.Lock()
   159  			defer m.Unlock()
   160  			reusedConns[conn]++
   161  		}()
   162  	}
   163  
   164  	wg.Wait()
   165  	// We should have only one entry otherwise it means another gRPC connection has been created
   166  	if len(reusedConns) != 1 {
   167  		t.Errorf("expected length to be 1 but got %d", len(reusedConns))
   168  	}
   169  	if counter, ok := reusedConns[conn]; ok && counter != 2 {
   170  		t.Errorf("expected counter to be 2 but got %d", counter)
   171  	}
   172  }
   173  
   174  func TestNewDRAPluginClient(t *testing.T) {
   175  	for _, test := range []struct {
   176  		description string
   177  		setup       func(string) tearDown
   178  		pluginName  string
   179  		shouldError bool
   180  	}{
   181  		{
   182  			description: "plugin name is empty",
   183  			setup: func(_ string) tearDown {
   184  				return func() {}
   185  			},
   186  			pluginName:  "",
   187  			shouldError: true,
   188  		},
   189  		{
   190  			description: "plugin name not found in the list",
   191  			setup: func(_ string) tearDown {
   192  				return func() {}
   193  			},
   194  			pluginName:  "plugin-name-not-found-in-the-list",
   195  			shouldError: true,
   196  		},
   197  		{
   198  			description: "plugin exists",
   199  			setup: func(name string) tearDown {
   200  				draPlugins.add(name, &plugin{})
   201  				return func() {
   202  					draPlugins.delete(name)
   203  				}
   204  			},
   205  			pluginName: "dummy-plugin",
   206  		},
   207  	} {
   208  		t.Run(test.description, func(t *testing.T) {
   209  			teardown := test.setup(test.pluginName)
   210  			defer teardown()
   211  
   212  			client, err := NewDRAPluginClient(test.pluginName)
   213  			if test.shouldError {
   214  				assert.Nil(t, client)
   215  				assert.Error(t, err)
   216  			} else {
   217  				assert.NotNil(t, client)
   218  				assert.Nil(t, err)
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestNodeUnprepareResource(t *testing.T) {
   225  	for _, test := range []struct {
   226  		description   string
   227  		serverSetup   func(string) (string, tearDown, error)
   228  		serverVersion string
   229  		request       *drapbv1alpha3.NodeUnprepareResourcesRequest
   230  	}{
   231  		{
   232  			description:   "server supports v1alpha3",
   233  			serverSetup:   setupFakeGRPCServer,
   234  			serverVersion: v1alpha3Version,
   235  			request:       &drapbv1alpha3.NodeUnprepareResourcesRequest{},
   236  		},
   237  		{
   238  			description:   "server supports v1alpha2, plugin client should fallback",
   239  			serverSetup:   setupFakeGRPCServer,
   240  			serverVersion: v1alpha2Version,
   241  			request: &drapbv1alpha3.NodeUnprepareResourcesRequest{
   242  				Claims: []*drapbv1alpha3.Claim{
   243  					{
   244  						Namespace:      "dummy-namespace",
   245  						Uid:            "dummy-uid",
   246  						Name:           "dummy-claim",
   247  						ResourceHandle: "dummy-resource",
   248  					},
   249  				},
   250  			},
   251  		},
   252  	} {
   253  		t.Run(test.description, func(t *testing.T) {
   254  			addr, teardown, err := setupFakeGRPCServer(test.serverVersion)
   255  			if err != nil {
   256  				t.Fatal(err)
   257  			}
   258  			defer teardown()
   259  
   260  			p := &plugin{
   261  				endpoint: addr,
   262  				version:  v1alpha3Version,
   263  			}
   264  
   265  			conn, err := p.getOrCreateGRPCConn()
   266  			defer func() {
   267  				err := conn.Close()
   268  				if err != nil {
   269  					t.Error(err)
   270  				}
   271  			}()
   272  			if err != nil {
   273  				t.Fatal(err)
   274  			}
   275  
   276  			draPlugins.add("dummy-plugin", p)
   277  			defer draPlugins.delete("dummy-plugin")
   278  
   279  			client, err := NewDRAPluginClient("dummy-plugin")
   280  			if err != nil {
   281  				t.Fatal(err)
   282  			}
   283  
   284  			_, err = client.NodeUnprepareResources(context.TODO(), test.request)
   285  			if err != nil {
   286  				t.Fatal(err)
   287  			}
   288  		})
   289  	}
   290  }