From 35e3b0a0654dacb9f7379feb734aa5b60addd5e1 Mon Sep 17 00:00:00 2001 From: Mayank Shah Date: Thu, 19 Nov 2020 12:49:11 +0530 Subject: [PATCH] test: Add unit tests for identityserver and nodeserver Signed-off-by: Mayank Shah --- go.mod | 3 +- pkg/mounter/safe_mounter_unix.go | 28 ++++ pkg/nfs/fake_mounter.go | 73 +++++++++++ pkg/nfs/identityserver_test.go | 102 +++++++++++++++ pkg/nfs/indentityserver.go | 7 +- pkg/nfs/nfs_test.go | 56 ++++++++ pkg/nfs/nodeserver.go | 10 ++ pkg/nfs/nodeserver_test.go | 213 +++++++++++++++++++++++++++++++ test/utils/testutil/testutil.go | 32 +++++ 9 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 pkg/mounter/safe_mounter_unix.go create mode 100644 pkg/nfs/fake_mounter.go create mode 100644 pkg/nfs/identityserver_test.go create mode 100644 pkg/nfs/nfs_test.go create mode 100644 pkg/nfs/nodeserver_test.go create mode 100644 test/utils/testutil/testutil.go diff --git a/go.mod b/go.mod index 24288978..f9410585 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/container-storage-interface/spec v1.3.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/golang/protobuf v1.4.1 github.com/kubernetes-csi/csi-lib-utils v0.7.0 github.com/kubernetes-csi/external-snapshotter/v2 v2.0.0-20200617021606-4800ca72d403 github.com/onsi/ginkgo v1.11.0 @@ -12,7 +13,7 @@ require ( github.com/pborman/uuid v1.2.0 github.com/prometheus/client_golang v1.5.1 // indirect github.com/spf13/cobra v0.0.5 - github.com/stretchr/testify v1.5.1 // indirect + github.com/stretchr/testify v1.5.1 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e golang.org/x/text v0.3.3 // indirect diff --git a/pkg/mounter/safe_mounter_unix.go b/pkg/mounter/safe_mounter_unix.go new file mode 100644 index 00000000..678c2950 --- /dev/null +++ b/pkg/mounter/safe_mounter_unix.go @@ -0,0 +1,28 @@ +// +build linux darwin + +/* +Copyright 2020 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 mounter + +import ( + utilexec "k8s.io/utils/exec" + "k8s.io/utils/mount" +) + +func NewSafeMounter() (*mount.SafeFormatAndMount, error) { + return &mount.SafeFormatAndMount{ + Interface: mount.New(""), + Exec: utilexec.New(), + }, nil +} diff --git a/pkg/nfs/fake_mounter.go b/pkg/nfs/fake_mounter.go new file mode 100644 index 00000000..f052c61f --- /dev/null +++ b/pkg/nfs/fake_mounter.go @@ -0,0 +1,73 @@ +/* +Copyright 2020 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 nfs + +import ( + "fmt" + "runtime" + "strings" + + "github.com/kubernetes-csi/csi-driver-nfs/pkg/mounter" + + "k8s.io/utils/mount" +) + +type fakeMounter struct { + mount.FakeMounter +} + +// Mount overrides mount.FakeMounter.Mount. +func (f *fakeMounter) Mount(source string, target string, fstype string, options []string) error { + if strings.Contains(source, "error_mount") { + return fmt.Errorf("fake Mount: source error") + } else if strings.Contains(target, "error_mount") { + return fmt.Errorf("fake Mount: target error") + } + + return nil +} + +// MountSensitive overrides mount.FakeMounter.MountSensitive. +func (f *fakeMounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { + if strings.Contains(source, "error_mount_sens") { + return fmt.Errorf("fake MountSensitive: source error") + } else if strings.Contains(target, "error_mount_sens") { + return fmt.Errorf("fake MountSensitive: target error") + } + + return nil +} + +//IsLikelyNotMountPoint overrides mount.FakeMounter.IsLikelyNotMountPoint. +func (f *fakeMounter) IsLikelyNotMountPoint(file string) (bool, error) { + if strings.Contains(file, "error_is_likely") { + return false, fmt.Errorf("fake IsLikelyNotMountPoint: fake error") + } + if strings.Contains(file, "false_is_likely") { + return false, nil + } + return true, nil +} + +func NewFakeMounter() (*mount.SafeFormatAndMount, error) { + if runtime.GOOS == "windows" { + return mounter.NewSafeMounter() + } + return &mount.SafeFormatAndMount{ + Interface: &fakeMounter{}, + }, nil +} diff --git a/pkg/nfs/identityserver_test.go b/pkg/nfs/identityserver_test.go new file mode 100644 index 00000000..27759739 --- /dev/null +++ b/pkg/nfs/identityserver_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 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 nfs + +import ( + "context" + "reflect" + "testing" + + "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestGetPluginInfo(t *testing.T) { + req := csi.GetPluginInfoRequest{} + emptyNameDriver := NewEmptyDriver("name") + emptyVersionDriver := NewEmptyDriver("version") + tests := []struct { + desc string + driver *Driver + expectedErr error + }{ + { + desc: "Successful Request", + driver: NewEmptyDriver(""), + expectedErr: nil, + }, + { + desc: "Driver name missing", + driver: emptyNameDriver, + expectedErr: status.Error(codes.Unavailable, "Driver name not configured"), + }, + { + desc: "Driver version missing", + driver: emptyVersionDriver, + expectedErr: status.Error(codes.Unavailable, "Driver is missing version"), + }, + } + + for _, test := range tests { + fakeIdentityServer := IdentityServer{ + Driver: test.driver, + } + _, err := fakeIdentityServer.GetPluginInfo(context.Background(), &req) + if !reflect.DeepEqual(err, test.expectedErr) { + t.Errorf("Unexpected error: %v\nExpected: %v", err, test.expectedErr) + } + } +} + +func TestProbe(t *testing.T) { + d := NewEmptyDriver("") + req := csi.ProbeRequest{} + fakeIdentityServer := IdentityServer{ + Driver: d, + } + resp, err := fakeIdentityServer.Probe(context.Background(), &req) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, resp.XXX_sizecache, int32(0)) + assert.Equal(t, resp.Ready.Value, true) +} + +func TestGetPluginCapabilities(t *testing.T) { + expectedCap := []*csi.PluginCapability{ + { + Type: &csi.PluginCapability_Service_{ + Service: &csi.PluginCapability_Service{ + Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, + }, + }, + }, + } + + d := NewEmptyDriver("") + fakeIdentityServer := IdentityServer{ + Driver: d, + } + req := csi.GetPluginCapabilitiesRequest{} + resp, err := fakeIdentityServer.GetPluginCapabilities(context.Background(), &req) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, resp.XXX_sizecache, int32(0)) + assert.Equal(t, resp.Capabilities, expectedCap) + +} diff --git a/pkg/nfs/indentityserver.go b/pkg/nfs/indentityserver.go index 8f276bd1..9ee82bd3 100644 --- a/pkg/nfs/indentityserver.go +++ b/pkg/nfs/indentityserver.go @@ -19,6 +19,7 @@ package nfs import ( "github.com/container-storage-interface/spec/lib/go/csi" "github.com/golang/glog" + "github.com/golang/protobuf/ptypes/wrappers" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -45,8 +46,12 @@ func (ids *IdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPlugin }, nil } +// Probe check whether the plugin is running or not. +// This method does not need to return anything. +// Currently the spec does not dictate what you should return either. +// Hence, return an empty response func (ids *IdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) { - return &csi.ProbeResponse{}, nil + return &csi.ProbeResponse{Ready: &wrappers.BoolValue{Value: true}}, nil } func (ids *IdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { diff --git a/pkg/nfs/nfs_test.go b/pkg/nfs/nfs_test.go new file mode 100644 index 00000000..440736bc --- /dev/null +++ b/pkg/nfs/nfs_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2019 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 nfs + +import "github.com/container-storage-interface/spec/lib/go/csi" + +const ( + fakeNodeID = "fakeNodeID" +) + +func NewEmptyDriver(emptyField string) *Driver { + var d *Driver + var perm *uint32 + switch emptyField { + case "version": + d = &Driver{ + name: DriverName, + version: "", + nodeID: fakeNodeID, + cap: map[csi.VolumeCapability_AccessMode_Mode]bool{}, + perm: perm, + } + case "name": + d = &Driver{ + name: "", + version: version, + nodeID: fakeNodeID, + cap: map[csi.VolumeCapability_AccessMode_Mode]bool{}, + perm: perm, + } + default: + d = &Driver{ + name: DriverName, + version: version, + nodeID: fakeNodeID, + cap: map[csi.VolumeCapability_AccessMode_Mode]bool{}, + perm: perm, + } + } + + return d +} diff --git a/pkg/nfs/nodeserver.go b/pkg/nfs/nodeserver.go index 91f18c34..ce5617c5 100644 --- a/pkg/nfs/nodeserver.go +++ b/pkg/nfs/nodeserver.go @@ -160,3 +160,13 @@ func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol func (ns *NodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "") } + +func makeDir(pathname string) error { + err := os.MkdirAll(pathname, os.FileMode(0755)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} diff --git a/pkg/nfs/nodeserver_test.go b/pkg/nfs/nodeserver_test.go new file mode 100644 index 00000000..7a31c957 --- /dev/null +++ b/pkg/nfs/nodeserver_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2020 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 nfs + +import ( + "context" + "errors" + "os" + "reflect" + "testing" + + "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/kubernetes-csi/csi-driver-nfs/test/utils/testutil" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestNodePublishVolume(t *testing.T) { + volumeCap := csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER} + alreadyMountedTarget := testutil.GetWorkDirPath("false_is_likely_exist_target", t) + targetTest := testutil.GetWorkDirPath("target_test", t) + + tests := []struct { + desc string + req csi.NodePublishVolumeRequest + skipOnWindows bool + expectedErr error + }{ + { + desc: "[Error] Volume capabilities missing", + req: csi.NodePublishVolumeRequest{}, + expectedErr: status.Error(codes.InvalidArgument, "Volume capability missing in request"), + }, + { + desc: "[Error] Volume ID missing", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}}, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID missing in request"), + }, + { + desc: "[Error] Target path missing", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}, + VolumeId: "vol_1"}, + expectedErr: status.Error(codes.InvalidArgument, "Target path not provided"), + }, + { + desc: "[Success] Stage target path missing", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}, + VolumeId: "vol_1", + TargetPath: targetTest}, + expectedErr: nil, + }, + { + desc: "[Success] Valid request read only", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}, + VolumeId: "vol_1", + TargetPath: targetTest, + Readonly: true}, + expectedErr: nil, + }, + { + desc: "[Success] Valid request already mounted", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}, + VolumeId: "vol_1", + TargetPath: alreadyMountedTarget, + Readonly: true}, + expectedErr: nil, + }, + { + desc: "[Success] Valid request", + req: csi.NodePublishVolumeRequest{VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap}, + VolumeId: "vol_1", + TargetPath: targetTest, + Readonly: true}, + expectedErr: nil, + }, + } + + // setup + _ = makeDir(alreadyMountedTarget) + + ns, err := getTestNodeServer() + if err != nil { + t.Fatalf(err.Error()) + } + + for _, tc := range tests { + _, err := ns.NodePublishVolume(context.Background(), &tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Errorf("Desc:%v\nUnexpected error: %v\nExpected: %v", tc.desc, err, tc.expectedErr) + } + } + + // Clean up + err = os.RemoveAll(targetTest) + assert.NoError(t, err) + err = os.RemoveAll(alreadyMountedTarget) + assert.NoError(t, err) + +} + +func TestNodeUnpublishVolume(t *testing.T) { + errorTarget := testutil.GetWorkDirPath("error_is_likely_target", t) + targetTest := testutil.GetWorkDirPath("target_test", t) + targetFile := testutil.GetWorkDirPath("abc.go", t) + + tests := []struct { + desc string + req csi.NodeUnpublishVolumeRequest + expectedErr error + }{ + { + desc: "[Error] Volume ID missing", + req: csi.NodeUnpublishVolumeRequest{TargetPath: targetTest}, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID missing in request"), + }, + { + desc: "[Error] Target missing", + req: csi.NodeUnpublishVolumeRequest{VolumeId: "vol_1"}, + expectedErr: status.Error(codes.InvalidArgument, "Target path missing in request"), + }, + { + desc: "[Error] Unmount error mocked by IsLikelyNotMountPoint", + req: csi.NodeUnpublishVolumeRequest{TargetPath: errorTarget, VolumeId: "vol_1"}, + expectedErr: status.Error(codes.Internal, "fake IsLikelyNotMountPoint: fake error"), + }, + { + desc: "[Error] Volume not mounted", + req: csi.NodeUnpublishVolumeRequest{TargetPath: targetFile, VolumeId: "vol_1"}, + expectedErr: status.Error(codes.NotFound, "Volume not mounted"), + }, + } + + // Setup + _ = makeDir(errorTarget) + + ns, err := getTestNodeServer() + if err != nil { + t.Fatalf(err.Error()) + } + + for _, tc := range tests { + _, err := ns.NodeUnpublishVolume(context.Background(), &tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Errorf("Desc:%v\nUnexpected error: %v\nExpected: %v", tc.desc, err, tc.expectedErr) + } + } + + // Clean up + err = os.RemoveAll(errorTarget) + assert.NoError(t, err) +} + +func TestNodeGetInfo(t *testing.T) { + + ns, err := getTestNodeServer() + if err != nil { + t.Fatalf(err.Error()) + } + + // Test valid request + req := csi.NodeGetInfoRequest{} + resp, err := ns.NodeGetInfo(context.Background(), &req) + assert.NoError(t, err) + assert.Equal(t, resp.GetNodeId(), fakeNodeID) +} + +func TestNodeGetCapabilities(t *testing.T) { + + ns, err := getTestNodeServer() + if err != nil { + t.Fatalf(err.Error()) + } + + capType := &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_UNKNOWN, + }, + } + + // Test valid request + req := csi.NodeGetCapabilitiesRequest{} + resp, err := ns.NodeGetCapabilities(context.Background(), &req) + assert.NotNil(t, resp) + assert.Equal(t, resp.Capabilities[0].GetType(), capType) + assert.NoError(t, err) +} + +func getTestNodeServer() (NodeServer, error) { + d := NewEmptyDriver("") + mounter, err := NewFakeMounter() + if err != nil { + return NodeServer{}, errors.New("failed to get fake mounter") + } + return NodeServer{ + Driver: d, + mounter: mounter, + }, nil +} diff --git a/test/utils/testutil/testutil.go b/test/utils/testutil/testutil.go new file mode 100644 index 00000000..cc2dd937 --- /dev/null +++ b/test/utils/testutil/testutil.go @@ -0,0 +1,32 @@ +/* +Copyright 2020 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 testutil + +import ( + "fmt" + "os" + "testing" +) + +// GetWorkDirPath returns the path to the current working directory +func GetWorkDirPath(dir string, t *testing.T) string { + path, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %s", err) + } + return fmt.Sprintf("%s%c%s", path, os.PathSeparator, dir) +}