Commit 6330cee9 authored by Hannes Hörl's avatar Hannes Hörl Committed by Maria Ntalla

Add tests for `csiDriverClient`

As #69219 outlines the unit tests in `csi_client_test.go` where not testing the actual implementation of the `csiDriverClient` but was testing the fake. To fix this, we changed the `csiDriverClient` to use a `nodeClientCreator` which is responsible for creating a new `NodeClient`, a real one in prod and a fake one in the tests. The setup of the gRPC connection has been pushed into that creator. The node client uses that connection; that's transparent to the driver client. It's the responsibility of the driver client to close the connection when it is done with the node client. To achieve this, we have the node client creator return a closer which handles the connection teardown. In the tests we now also check if the driver client actually calls this closer, thus closing the gRPC connection. Closes: #69219 Co-authored-by: 's avatarRosie Bloxsom <rbloxsom@pivotal.io> Co-authored-by: 's avatarMaria Ntalla <mntalla@pivotal.io>
parent db1d1c86
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"time" "time"
...@@ -69,14 +70,39 @@ type csiClient interface { ...@@ -69,14 +70,39 @@ type csiClient interface {
// csiClient encapsulates all csi-plugin methods // csiClient encapsulates all csi-plugin methods
type csiDriverClient struct { type csiDriverClient struct {
driverName string driverName string
nodeClient csipb.NodeClient nodeClientCreator nodeClientCreator
} }
var _ csiClient = &csiDriverClient{} var _ csiClient = &csiDriverClient{}
type nodeClientCreator func(driverName string) (
nodeClient csipb.NodeClient,
closer io.Closer,
err error,
)
// newNodeClient creates a new NodeClient with the internally used gRPC
// connection set up. It also returns a closer which must to be called to close
// the gRPC connection when the NodeClient is not used anymore.
// This is the default implementation for the nodeClientCreator, used in
// newCsiDriverClient.
func newNodeClient(driverName string) (nodeClient csipb.NodeClient, closer io.Closer, err error) {
var conn *grpc.ClientConn
conn, err = newGrpcConn(driverName)
if err != nil {
return nil, nil, err
}
nodeClient = csipb.NewNodeClient(conn)
return nodeClient, conn, nil
}
func newCsiDriverClient(driverName string) *csiDriverClient { func newCsiDriverClient(driverName string) *csiDriverClient {
c := &csiDriverClient{driverName: driverName} c := &csiDriverClient{
driverName: driverName,
nodeClientCreator: newNodeClient,
}
return c return c
} }
...@@ -87,12 +113,11 @@ func (c *csiDriverClient) NodeGetInfo(ctx context.Context) ( ...@@ -87,12 +113,11 @@ func (c *csiDriverClient) NodeGetInfo(ctx context.Context) (
err error) { err error) {
glog.V(4).Info(log("calling NodeGetInfo rpc")) glog.V(4).Info(log("calling NodeGetInfo rpc"))
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return "", 0, nil, err return "", 0, nil, err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
res, err := nodeClient.NodeGetInfo(ctx, &csipb.NodeGetInfoRequest{}) res, err := nodeClient.NodeGetInfo(ctx, &csipb.NodeGetInfoRequest{})
if err != nil { if err != nil {
...@@ -122,12 +147,11 @@ func (c *csiDriverClient) NodePublishVolume( ...@@ -122,12 +147,11 @@ func (c *csiDriverClient) NodePublishVolume(
return errors.New("missing target path") return errors.New("missing target path")
} }
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
req := &csipb.NodePublishVolumeRequest{ req := &csipb.NodePublishVolumeRequest{
VolumeId: volID, VolumeId: volID,
...@@ -171,12 +195,11 @@ func (c *csiDriverClient) NodeUnpublishVolume(ctx context.Context, volID string, ...@@ -171,12 +195,11 @@ func (c *csiDriverClient) NodeUnpublishVolume(ctx context.Context, volID string,
return errors.New("missing target path") return errors.New("missing target path")
} }
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
req := &csipb.NodeUnpublishVolumeRequest{ req := &csipb.NodeUnpublishVolumeRequest{
VolumeId: volID, VolumeId: volID,
...@@ -204,12 +227,11 @@ func (c *csiDriverClient) NodeStageVolume(ctx context.Context, ...@@ -204,12 +227,11 @@ func (c *csiDriverClient) NodeStageVolume(ctx context.Context,
return errors.New("missing staging target path") return errors.New("missing staging target path")
} }
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
req := &csipb.NodeStageVolumeRequest{ req := &csipb.NodeStageVolumeRequest{
VolumeId: volID, VolumeId: volID,
...@@ -249,12 +271,11 @@ func (c *csiDriverClient) NodeUnstageVolume(ctx context.Context, volID, stagingT ...@@ -249,12 +271,11 @@ func (c *csiDriverClient) NodeUnstageVolume(ctx context.Context, volID, stagingT
return errors.New("missing staging target path") return errors.New("missing staging target path")
} }
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
req := &csipb.NodeUnstageVolumeRequest{ req := &csipb.NodeUnstageVolumeRequest{
VolumeId: volID, VolumeId: volID,
...@@ -267,12 +288,11 @@ func (c *csiDriverClient) NodeUnstageVolume(ctx context.Context, volID, stagingT ...@@ -267,12 +288,11 @@ func (c *csiDriverClient) NodeUnstageVolume(ctx context.Context, volID, stagingT
func (c *csiDriverClient) NodeGetCapabilities(ctx context.Context) ([]*csipb.NodeServiceCapability, error) { func (c *csiDriverClient) NodeGetCapabilities(ctx context.Context) ([]*csipb.NodeServiceCapability, error) {
glog.V(4).Info(log("calling NodeGetCapabilities rpc")) glog.V(4).Info(log("calling NodeGetCapabilities rpc"))
conn, err := newGrpcConn(c.driverName) nodeClient, closer, err := c.nodeClientCreator(c.driverName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() defer closer.Close()
nodeClient := csipb.NewNodeClient(conn)
req := &csipb.NodeGetCapabilitiesRequest{} req := &csipb.NodeGetCapabilitiesRequest{}
resp, err := nodeClient.NodeGetCapabilities(ctx, req) resp, err := nodeClient.NodeGetCapabilities(ctx, req)
......
...@@ -19,12 +19,13 @@ package csi ...@@ -19,12 +19,13 @@ package csi
import ( import (
"context" "context"
"errors" "errors"
"io"
"reflect"
"testing" "testing"
csipb "github.com/container-storage-interface/spec/lib/go/csi/v0" csipb "github.com/container-storage-interface/spec/lib/go/csi/v0"
api "k8s.io/api/core/v1" api "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/volume/csi/fake" "k8s.io/kubernetes/pkg/volume/csi/fake"
"reflect"
) )
type fakeCsiDriverClient struct { type fakeCsiDriverClient struct {
...@@ -151,6 +152,20 @@ func setupClient(t *testing.T, stageUnstageSet bool) csiClient { ...@@ -151,6 +152,20 @@ func setupClient(t *testing.T, stageUnstageSet bool) csiClient {
return newFakeCsiDriverClient(t, stageUnstageSet) return newFakeCsiDriverClient(t, stageUnstageSet)
} }
func checkErr(t *testing.T, expectedAnError bool, actualError error) {
t.Helper()
errOccurred := actualError != nil
if expectedAnError && !errOccurred {
t.Error("expected an error")
}
if !expectedAnError && errOccurred {
t.Errorf("expected no error, got: %v", actualError)
}
}
func TestClientNodeGetInfo(t *testing.T) { func TestClientNodeGetInfo(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
...@@ -168,28 +183,33 @@ func TestClientNodeGetInfo(t *testing.T) { ...@@ -168,28 +183,33 @@ func TestClientNodeGetInfo(t *testing.T) {
Segments: map[string]string{"com.example.csi-topology/zone": "zone1"}, Segments: map[string]string{"com.example.csi-topology/zone": "zone1"},
}, },
}, },
{name: "grpc error", mustFail: true, err: errors.New("grpc error")}, {
name: "grpc error",
mustFail: true,
err: errors.New("grpc error"),
},
} }
client := setupClient(t, false /* stageUnstageSet */)
for _, tc := range testCases { for _, tc := range testCases {
t.Logf("test case: %s", tc.name) t.Logf("test case: %s", tc.name)
client.(*fakeCsiDriverClient).nodeClient.SetNextError(tc.err)
client.(*fakeCsiDriverClient).nodeClient.SetNodeGetInfoResp(&csipb.NodeGetInfoResponse{
NodeId: tc.expectedNodeID,
MaxVolumesPerNode: tc.expectedMaxVolumePerNode,
AccessibleTopology: tc.expectedAccessibleTopology,
})
nodeID, maxVolumePerNode, accessibleTopology, err := client.NodeGetInfo(context.Background())
if tc.mustFail && err == nil { fakeCloser := fake.NewCloser(t)
t.Error("expected an error but got none") client := &csiDriverClient{
driverName: "Fake Driver Name",
nodeClientCreator: func(driverName string) (csipb.NodeClient, io.Closer, error) {
nodeClient := fake.NewNodeClient(false /* stagingCapable */)
nodeClient.SetNextError(tc.err)
nodeClient.SetNodeGetInfoResp(&csipb.NodeGetInfoResponse{
NodeId: tc.expectedNodeID,
MaxVolumesPerNode: tc.expectedMaxVolumePerNode,
AccessibleTopology: tc.expectedAccessibleTopology,
})
return nodeClient, fakeCloser, nil
},
} }
if !tc.mustFail && err != nil { nodeID, maxVolumePerNode, accessibleTopology, err := client.NodeGetInfo(context.Background())
t.Errorf("expected no errors but got: %v", err) checkErr(t, tc.mustFail, err)
}
if nodeID != tc.expectedNodeID { if nodeID != tc.expectedNodeID {
t.Errorf("expected nodeID: %v; got: %v", tc.expectedNodeID, nodeID) t.Errorf("expected nodeID: %v; got: %v", tc.expectedNodeID, nodeID)
...@@ -202,6 +222,10 @@ func TestClientNodeGetInfo(t *testing.T) { ...@@ -202,6 +222,10 @@ func TestClientNodeGetInfo(t *testing.T) {
if !reflect.DeepEqual(accessibleTopology, tc.expectedAccessibleTopology) { if !reflect.DeepEqual(accessibleTopology, tc.expectedAccessibleTopology) {
t.Errorf("expected accessibleTopology: %v; got: %v", *tc.expectedAccessibleTopology, *accessibleTopology) t.Errorf("expected accessibleTopology: %v; got: %v", *tc.expectedAccessibleTopology, *accessibleTopology)
} }
if !tc.mustFail {
fakeCloser.Check()
}
} }
} }
...@@ -221,11 +245,18 @@ func TestClientNodePublishVolume(t *testing.T) { ...@@ -221,11 +245,18 @@ func TestClientNodePublishVolume(t *testing.T) {
{name: "grpc error", volID: "vol-test", targetPath: "/test/path", mustFail: true, err: errors.New("grpc error")}, {name: "grpc error", volID: "vol-test", targetPath: "/test/path", mustFail: true, err: errors.New("grpc error")},
} }
client := setupClient(t, false)
for _, tc := range testCases { for _, tc := range testCases {
t.Logf("test case: %s", tc.name) t.Logf("test case: %s", tc.name)
client.(*fakeCsiDriverClient).nodeClient.SetNextError(tc.err) fakeCloser := fake.NewCloser(t)
client := &csiDriverClient{
driverName: "Fake Driver Name",
nodeClientCreator: func(driverName string) (csipb.NodeClient, io.Closer, error) {
nodeClient := fake.NewNodeClient(false /* stagingCapable */)
nodeClient.SetNextError(tc.err)
return nodeClient, fakeCloser, nil
},
}
err := client.NodePublishVolume( err := client.NodePublishVolume(
context.Background(), context.Background(),
tc.volID, tc.volID,
...@@ -238,9 +269,10 @@ func TestClientNodePublishVolume(t *testing.T) { ...@@ -238,9 +269,10 @@ func TestClientNodePublishVolume(t *testing.T) {
map[string]string{}, map[string]string{},
tc.fsType, tc.fsType,
) )
checkErr(t, tc.mustFail, err)
if tc.mustFail && err == nil { if !tc.mustFail {
t.Error("test must fail, but err is nil") fakeCloser.Check()
} }
} }
} }
...@@ -259,14 +291,23 @@ func TestClientNodeUnpublishVolume(t *testing.T) { ...@@ -259,14 +291,23 @@ func TestClientNodeUnpublishVolume(t *testing.T) {
{name: "grpc error", volID: "vol-test", targetPath: "/test/path", mustFail: true, err: errors.New("grpc error")}, {name: "grpc error", volID: "vol-test", targetPath: "/test/path", mustFail: true, err: errors.New("grpc error")},
} }
client := setupClient(t, false)
for _, tc := range testCases { for _, tc := range testCases {
t.Logf("test case: %s", tc.name) t.Logf("test case: %s", tc.name)
client.(*fakeCsiDriverClient).nodeClient.SetNextError(tc.err) fakeCloser := fake.NewCloser(t)
client := &csiDriverClient{
driverName: "Fake Driver Name",
nodeClientCreator: func(driverName string) (csipb.NodeClient, io.Closer, error) {
nodeClient := fake.NewNodeClient(false /* stagingCapable */)
nodeClient.SetNextError(tc.err)
return nodeClient, fakeCloser, nil
},
}
err := client.NodeUnpublishVolume(context.Background(), tc.volID, tc.targetPath) err := client.NodeUnpublishVolume(context.Background(), tc.volID, tc.targetPath)
if tc.mustFail && err == nil { checkErr(t, tc.mustFail, err)
t.Error("test must fail, but err is nil")
if !tc.mustFail {
fakeCloser.Check()
} }
} }
} }
...@@ -288,11 +329,18 @@ func TestClientNodeStageVolume(t *testing.T) { ...@@ -288,11 +329,18 @@ func TestClientNodeStageVolume(t *testing.T) {
{name: "grpc error", volID: "vol-test", stagingTargetPath: "/test/path", mustFail: true, err: errors.New("grpc error")}, {name: "grpc error", volID: "vol-test", stagingTargetPath: "/test/path", mustFail: true, err: errors.New("grpc error")},
} }
client := setupClient(t, false)
for _, tc := range testCases { for _, tc := range testCases {
t.Logf("Running test case: %s", tc.name) t.Logf("Running test case: %s", tc.name)
client.(*fakeCsiDriverClient).nodeClient.SetNextError(tc.err) fakeCloser := fake.NewCloser(t)
client := &csiDriverClient{
driverName: "Fake Driver Name",
nodeClientCreator: func(driverName string) (csipb.NodeClient, io.Closer, error) {
nodeClient := fake.NewNodeClient(false /* stagingCapable */)
nodeClient.SetNextError(tc.err)
return nodeClient, fakeCloser, nil
},
}
err := client.NodeStageVolume( err := client.NodeStageVolume(
context.Background(), context.Background(),
tc.volID, tc.volID,
...@@ -303,9 +351,10 @@ func TestClientNodeStageVolume(t *testing.T) { ...@@ -303,9 +351,10 @@ func TestClientNodeStageVolume(t *testing.T) {
tc.secret, tc.secret,
map[string]string{"attr0": "val0"}, map[string]string{"attr0": "val0"},
) )
checkErr(t, tc.mustFail, err)
if tc.mustFail && err == nil { if !tc.mustFail {
t.Error("test must fail, but err is nil") fakeCloser.Check()
} }
} }
} }
...@@ -324,17 +373,26 @@ func TestClientNodeUnstageVolume(t *testing.T) { ...@@ -324,17 +373,26 @@ func TestClientNodeUnstageVolume(t *testing.T) {
{name: "grpc error", volID: "vol-test", stagingTargetPath: "/test/path", mustFail: true, err: errors.New("grpc error")}, {name: "grpc error", volID: "vol-test", stagingTargetPath: "/test/path", mustFail: true, err: errors.New("grpc error")},
} }
client := setupClient(t, false)
for _, tc := range testCases { for _, tc := range testCases {
t.Logf("Running test case: %s", tc.name) t.Logf("Running test case: %s", tc.name)
client.(*fakeCsiDriverClient).nodeClient.SetNextError(tc.err) fakeCloser := fake.NewCloser(t)
client := &csiDriverClient{
driverName: "Fake Driver Name",
nodeClientCreator: func(driverName string) (csipb.NodeClient, io.Closer, error) {
nodeClient := fake.NewNodeClient(false /* stagingCapable */)
nodeClient.SetNextError(tc.err)
return nodeClient, fakeCloser, nil
},
}
err := client.NodeUnstageVolume( err := client.NodeUnstageVolume(
context.Background(), context.Background(),
tc.volID, tc.stagingTargetPath, tc.volID, tc.stagingTargetPath,
) )
if tc.mustFail && err == nil { checkErr(t, tc.mustFail, err)
t.Error("test must fail, but err is nil")
if !tc.mustFail {
fakeCloser.Check()
} }
} }
} }
...@@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") ...@@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = ["fake_client.go"], srcs = [
"fake_client.go",
"fake_closer.go",
],
importpath = "k8s.io/kubernetes/pkg/volume/csi/fake", importpath = "k8s.io/kubernetes/pkg/volume/csi/fake",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
......
/*
Copyright 2018 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 fake
import (
"testing"
)
func NewCloser(t *testing.T) *Closer {
return &Closer{
t: t,
}
}
type Closer struct {
wasCalled bool
t *testing.T
}
func (c *Closer) Close() error {
c.wasCalled = true
return nil
}
func (c *Closer) Check() *Closer {
c.t.Helper()
if !c.wasCalled {
c.t.Error("expected closer to have been called")
}
return c
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment