k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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  	drapbv1alpha3 "k8s.io/kubelet/pkg/apis/dra/v1alpha3"
    31  )
    32  
    33  type fakeV1alpha3GRPCServer struct {
    34  	drapbv1alpha3.UnimplementedNodeServer
    35  }
    36  
    37  var _ drapbv1alpha3.NodeServer = &fakeV1alpha3GRPCServer{}
    38  
    39  func (f *fakeV1alpha3GRPCServer) NodePrepareResources(ctx context.Context, in *drapbv1alpha3.NodePrepareResourcesRequest) (*drapbv1alpha3.NodePrepareResourcesResponse, error) {
    40  	return &drapbv1alpha3.NodePrepareResourcesResponse{Claims: map[string]*drapbv1alpha3.NodePrepareResourceResponse{"dummy": {CDIDevices: []string{"dummy"}}}}, nil
    41  }
    42  
    43  func (f *fakeV1alpha3GRPCServer) NodeUnprepareResources(ctx context.Context, in *drapbv1alpha3.NodeUnprepareResourcesRequest) (*drapbv1alpha3.NodeUnprepareResourcesResponse, error) {
    44  
    45  	return &drapbv1alpha3.NodeUnprepareResourcesResponse{}, nil
    46  }
    47  
    48  func (f *fakeV1alpha3GRPCServer) NodeListAndWatchResources(req *drapbv1alpha3.NodeListAndWatchResourcesRequest, srv drapbv1alpha3.Node_NodeListAndWatchResourcesServer) error {
    49  	if err := srv.Send(&drapbv1alpha3.NodeListAndWatchResourcesResponse{}); err != nil {
    50  		return err
    51  	}
    52  	if err := srv.Send(&drapbv1alpha3.NodeListAndWatchResourcesResponse{}); err != nil {
    53  		return err
    54  	}
    55  	return 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 v1alpha3Version:
    82  		fakeGRPCServer := &fakeV1alpha3GRPCServer{}
    83  		drapbv1alpha3.RegisterNodeServer(s, fakeGRPCServer)
    84  	default:
    85  		return "", nil, fmt.Errorf("unsupported version: %s", version)
    86  	}
    87  
    88  	go func() {
    89  		go s.Serve(listener)
    90  		<-closeCh
    91  		s.GracefulStop()
    92  	}()
    93  
    94  	return addr, teardown, nil
    95  }
    96  
    97  func TestGRPCConnIsReused(t *testing.T) {
    98  	addr, teardown, err := setupFakeGRPCServer(v1alpha3Version)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  	defer teardown()
   103  
   104  	reusedConns := make(map[*grpc.ClientConn]int)
   105  	wg := sync.WaitGroup{}
   106  	m := sync.Mutex{}
   107  
   108  	p := &plugin{
   109  		endpoint: addr,
   110  	}
   111  
   112  	conn, err := p.getOrCreateGRPCConn()
   113  	defer func() {
   114  		err := conn.Close()
   115  		if err != nil {
   116  			t.Error(err)
   117  		}
   118  	}()
   119  	if err != nil {
   120  		t.Fatal(err)
   121  	}
   122  
   123  	// ensure the plugin we are using is registered
   124  	draPlugins.add("dummy-plugin", p)
   125  	defer draPlugins.delete("dummy-plugin")
   126  
   127  	// we call `NodePrepareResource` 2 times and check whether a new connection is created or the same is reused
   128  	for i := 0; i < 2; i++ {
   129  		wg.Add(1)
   130  		go func() {
   131  			defer wg.Done()
   132  			client, err := NewDRAPluginClient("dummy-plugin")
   133  			if err != nil {
   134  				t.Error(err)
   135  				return
   136  			}
   137  
   138  			req := &drapbv1alpha3.NodePrepareResourcesRequest{
   139  				Claims: []*drapbv1alpha3.Claim{
   140  					{
   141  						Namespace:      "dummy-namespace",
   142  						Uid:            "dummy-uid",
   143  						Name:           "dummy-claim",
   144  						ResourceHandle: "dummy-resource",
   145  					},
   146  				},
   147  			}
   148  			client.NodePrepareResources(context.TODO(), req)
   149  
   150  			client.(*plugin).Lock()
   151  			conn := client.(*plugin).conn
   152  			client.(*plugin).Unlock()
   153  
   154  			m.Lock()
   155  			defer m.Unlock()
   156  			reusedConns[conn]++
   157  		}()
   158  	}
   159  
   160  	wg.Wait()
   161  	// We should have only one entry otherwise it means another gRPC connection has been created
   162  	if len(reusedConns) != 1 {
   163  		t.Errorf("expected length to be 1 but got %d", len(reusedConns))
   164  	}
   165  	if counter, ok := reusedConns[conn]; ok && counter != 2 {
   166  		t.Errorf("expected counter to be 2 but got %d", counter)
   167  	}
   168  }
   169  
   170  func TestNewDRAPluginClient(t *testing.T) {
   171  	for _, test := range []struct {
   172  		description string
   173  		setup       func(string) tearDown
   174  		pluginName  string
   175  		shouldError bool
   176  	}{
   177  		{
   178  			description: "plugin name is empty",
   179  			setup: func(_ string) tearDown {
   180  				return func() {}
   181  			},
   182  			pluginName:  "",
   183  			shouldError: true,
   184  		},
   185  		{
   186  			description: "plugin name not found in the list",
   187  			setup: func(_ string) tearDown {
   188  				return func() {}
   189  			},
   190  			pluginName:  "plugin-name-not-found-in-the-list",
   191  			shouldError: true,
   192  		},
   193  		{
   194  			description: "plugin exists",
   195  			setup: func(name string) tearDown {
   196  				draPlugins.add(name, &plugin{})
   197  				return func() {
   198  					draPlugins.delete(name)
   199  				}
   200  			},
   201  			pluginName: "dummy-plugin",
   202  		},
   203  	} {
   204  		t.Run(test.description, func(t *testing.T) {
   205  			teardown := test.setup(test.pluginName)
   206  			defer teardown()
   207  
   208  			client, err := NewDRAPluginClient(test.pluginName)
   209  			if test.shouldError {
   210  				assert.Nil(t, client)
   211  				assert.Error(t, err)
   212  			} else {
   213  				assert.NotNil(t, client)
   214  				assert.Nil(t, err)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestNodeUnprepareResources(t *testing.T) {
   221  	for _, test := range []struct {
   222  		description   string
   223  		serverSetup   func(string) (string, tearDown, error)
   224  		serverVersion string
   225  		request       *drapbv1alpha3.NodeUnprepareResourcesRequest
   226  	}{
   227  		{
   228  			description:   "server supports v1alpha3",
   229  			serverSetup:   setupFakeGRPCServer,
   230  			serverVersion: v1alpha3Version,
   231  			request:       &drapbv1alpha3.NodeUnprepareResourcesRequest{},
   232  		},
   233  	} {
   234  		t.Run(test.description, func(t *testing.T) {
   235  			addr, teardown, err := setupFakeGRPCServer(test.serverVersion)
   236  			if err != nil {
   237  				t.Fatal(err)
   238  			}
   239  			defer teardown()
   240  
   241  			p := &plugin{
   242  				endpoint:      addr,
   243  				clientTimeout: PluginClientTimeout,
   244  			}
   245  
   246  			conn, err := p.getOrCreateGRPCConn()
   247  			defer func() {
   248  				err := conn.Close()
   249  				if err != nil {
   250  					t.Error(err)
   251  				}
   252  			}()
   253  			if err != nil {
   254  				t.Fatal(err)
   255  			}
   256  
   257  			draPlugins.add("dummy-plugin", p)
   258  			defer draPlugins.delete("dummy-plugin")
   259  
   260  			client, err := NewDRAPluginClient("dummy-plugin")
   261  			if err != nil {
   262  				t.Fatal(err)
   263  			}
   264  
   265  			_, err = client.NodeUnprepareResources(context.TODO(), test.request)
   266  			if err != nil {
   267  				t.Fatal(err)
   268  			}
   269  		})
   270  	}
   271  }
   272  
   273  func TestListAndWatchResources(t *testing.T) {
   274  	for _, test := range []struct {
   275  		description   string
   276  		serverSetup   func(string) (string, tearDown, error)
   277  		serverVersion string
   278  		request       *drapbv1alpha3.NodeListAndWatchResourcesRequest
   279  		responses     []*drapbv1alpha3.NodeListAndWatchResourcesResponse
   280  		expectError   string
   281  	}{
   282  		{
   283  			description:   "server supports NodeResources API",
   284  			serverSetup:   setupFakeGRPCServer,
   285  			serverVersion: v1alpha3Version,
   286  			request:       &drapbv1alpha3.NodeListAndWatchResourcesRequest{},
   287  			responses: []*drapbv1alpha3.NodeListAndWatchResourcesResponse{
   288  				{},
   289  				{},
   290  			},
   291  			expectError: "EOF",
   292  		},
   293  	} {
   294  		t.Run(test.description, func(t *testing.T) {
   295  			addr, teardown, err := setupFakeGRPCServer(test.serverVersion)
   296  			if err != nil {
   297  				t.Fatal(err)
   298  			}
   299  			defer teardown()
   300  
   301  			p := &plugin{
   302  				endpoint: addr,
   303  			}
   304  
   305  			conn, err := p.getOrCreateGRPCConn()
   306  			defer func() {
   307  				err := conn.Close()
   308  				if err != nil {
   309  					t.Error(err)
   310  				}
   311  			}()
   312  			if err != nil {
   313  				t.Fatal(err)
   314  			}
   315  
   316  			draPlugins.add("dummy-plugin", p)
   317  			defer draPlugins.delete("dummy-plugin")
   318  
   319  			client, err := NewDRAPluginClient("dummy-plugin")
   320  			if err != nil {
   321  				t.Fatal(err)
   322  			}
   323  
   324  			stream, err := client.NodeListAndWatchResources(context.Background(), test.request)
   325  			if err != nil {
   326  				t.Fatal(err)
   327  			}
   328  			var actualResponses []*drapbv1alpha3.NodeListAndWatchResourcesResponse
   329  			var actualErr error
   330  			for {
   331  				resp, err := stream.Recv()
   332  				if err != nil {
   333  					actualErr = err
   334  					break
   335  				}
   336  				actualResponses = append(actualResponses, resp)
   337  			}
   338  			assert.Equal(t, test.responses, actualResponses)
   339  			assert.Contains(t, actualErr.Error(), test.expectError)
   340  		})
   341  	}
   342  }