diff --git a/cmd/main.go b/cmd/main.go index a25b14338d..c1c7fbd2ff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,6 +28,7 @@ import ( "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud/metadata" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/metrics" + "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" flag "github.com/spf13/pflag" "k8s.io/component-base/featuregate" logsapi "k8s.io/component-base/logs/api/v1" @@ -132,19 +133,33 @@ func main() { r.InitializeMetricsHandler(options.HttpEndpoint, "/metrics") } + cfg := metadata.MetadataServiceConfig{ + EC2MetadataClient: metadata.DefaultEC2MetadataClient, + K8sAPIClient: metadata.DefaultKubernetesAPIClient, + } + region := os.Getenv("AWS_REGION") + var md metadata.MetadataService + var metadataErr error + if region == "" { klog.V(5).InfoS("[Debug] Retrieving region from metadata service") - cfg := metadata.MetadataServiceConfig{ - EC2MetadataClient: metadata.DefaultEC2MetadataClient, - K8sAPIClient: metadata.DefaultKubernetesAPIClient, - } - metadata, metadataErr := metadata.NewMetadataService(cfg, region) + md, metadataErr = metadata.NewMetadataService(cfg, region) if metadataErr != nil { klog.ErrorS(metadataErr, "Could not determine region from any metadata service. The region can be manually supplied via the AWS_REGION environment variable.") - panic(err) + panic(metadataErr) + } + region = md.GetRegion() + } + + if md == nil { + if options.Mode == driver.NodeMode || options.Mode == driver.AllMode { + md, metadataErr = metadata.NewMetadataService(cfg, region) + if metadataErr != nil { + klog.ErrorS(metadataErr, "failed to initialize metadata service") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } } - region = metadata.GetRegion() } cloud, err := cloud.NewCloud(region, options.AwsSdkDebugLog, options.UserAgentExtra, options.Batching) @@ -153,7 +168,17 @@ func main() { klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - drv, err := driver.NewDriver(cloud, &options) + m, err := mounter.NewNodeMounter() + if err != nil { + panic(err) + } + + k8sClient, err := cfg.K8sAPIClient() + if err != nil { + klog.V(2).InfoS("Failed to setup k8s client") + } + + drv, err := driver.NewDriver(cloud, &options, m, md, k8sClient) if err != nil { klog.ErrorS(err, "failed to create driver") klog.FlushAndExit(klog.ExitFlushTimeout, 1) diff --git a/hack/update-mockgen.sh b/hack/update-mockgen.sh index 0e35f5b205..10cd3ed6ba 100755 --- a/hack/update-mockgen.sh +++ b/hack/update-mockgen.sh @@ -21,7 +21,7 @@ BIN="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../bin" # Source-based mocking for internal interfaces "${BIN}/mockgen" -package cloud -destination=./pkg/cloud/mock_cloud.go -source pkg/cloud/interface.go "${BIN}/mockgen" -package metadata -destination=./pkg/cloud/metadata/mock_metadata.go -source pkg/cloud/metadata/interface.go -"${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_mount.go -source pkg/driver/mount.go +"${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mock_mount.go -source pkg/mounter/mount.go "${BIN}/mockgen" -package mounter -destination=./pkg/mounter/mock_mount_windows.go -source pkg/mounter/safe_mounter_windows.go "${BIN}/mockgen" -package cloud -destination=./pkg/cloud/mock_ec2.go -source pkg/cloud/ec2_interface.go EC2API @@ -30,8 +30,3 @@ BIN="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../bin" "${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_corev1.go k8s.io/client-go/kubernetes/typed/core/v1 CoreV1Interface,NodeInterface "${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_storagev1.go k8s.io/client-go/kubernetes/typed/storage/v1 VolumeAttachmentInterface,StorageV1Interface "${BIN}/mockgen" -package driver -destination=./pkg/driver/mock_k8s_storagev1_csinode.go k8s.io/client-go/kubernetes/typed/storage/v1 CSINodeInterface - -# Fixes "Mounter Type cannot implement 'Mounter' as it has a non-exported method and is defined in a different package" -# See https://github.com/kubernetes/mount-utils/commit/a20fcfb15a701977d086330b47b7efad51eb608e for context. -sed -i '/type MockMounter struct {/a \\tmount_utils.Interface' pkg/driver/mock_mount.go -sed -i '/type MockProxyMounter struct {/a \\tmount.Interface' pkg/mounter/mock_mount_windows.go diff --git a/pkg/driver/constants.go b/pkg/driver/constants.go index f34ccbc7ee..7775bbbd6c 100644 --- a/pkg/driver/constants.go +++ b/pkg/driver/constants.go @@ -32,12 +32,6 @@ const ( VolumeAttributePartition = "partition" ) -// constants of disk partition suffix -const ( - diskPartitionSuffix = "" - nvmeDiskPartitionSuffix = "p" -) - // constants of keys in volume parameters const ( // VolumeTypeKey represents key for volume type @@ -162,12 +156,6 @@ const ( DefaultModifyVolumeRequestHandlerTimeout = 2 * time.Second ) -// constants for disk block size -const ( - //DefaultBlockSize represents the default block size (4KB) - DefaultBlockSize = 4096 -) - // constants for fstypes const ( // FSTypeExt2 represents the ext2 filesystem type diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 544f8e3fe7..6347142974 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -24,9 +24,12 @@ import ( "github.com/awslabs/volume-modifier-for-k8s/pkg/rpc" csi "github.com/container-storage-interface/spec/lib/go/csi" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud" + "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud/metadata" + "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/util" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" + "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" ) @@ -56,12 +59,12 @@ const ( type Driver struct { controller *ControllerService - nodeService - srv *grpc.Server - options *Options + node *NodeService + srv *grpc.Server + options *Options } -func NewDriver(c cloud.Cloud, o *Options) (*Driver, error) { +func NewDriver(c cloud.Cloud, o *Options, m mounter.Mounter, md metadata.MetadataService, k kubernetes.Interface) (*Driver, error) { klog.InfoS("Driver Information", "Driver", DriverName, "Version", driverVersion) if err := ValidateDriverOptions(o); err != nil { @@ -76,10 +79,10 @@ func NewDriver(c cloud.Cloud, o *Options) (*Driver, error) { case ControllerMode: driver.controller = NewControllerService(c, o) case NodeMode: - driver.nodeService = newNodeService(o) + driver.node = NewNodeService(o, md, m, k) case AllMode: driver.controller = NewControllerService(c, o) - driver.nodeService = newNodeService(o) + driver.node = NewNodeService(o, md, m, k) default: return nil, fmt.Errorf("unknown mode: %s", o.Mode) } @@ -122,10 +125,10 @@ func (d *Driver) Run() error { csi.RegisterControllerServer(d.srv, d.controller) rpc.RegisterModifyServer(d.srv, d.controller) case NodeMode: - csi.RegisterNodeServer(d.srv, d) + csi.RegisterNodeServer(d.srv, d.node) case AllMode: csi.RegisterControllerServer(d.srv, d.controller) - csi.RegisterNodeServer(d.srv, d) + csi.RegisterNodeServer(d.srv, d.node) rpc.RegisterModifyServer(d.srv, d.controller) default: return fmt.Errorf("unknown mode: %s", d.options.Mode) diff --git a/pkg/driver/mount_linux.go b/pkg/driver/mount_linux.go deleted file mode 100644 index 62b4946533..0000000000 --- a/pkg/driver/mount_linux.go +++ /dev/null @@ -1,160 +0,0 @@ -//go:build linux -// +build linux - -/* -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 driver - -import ( - "fmt" - "os" - "strconv" - "strings" - - mountutils "k8s.io/mount-utils" -) - -// GetDeviceNameFromMount returns the volume ID for a mount path. -func (m NodeMounter) GetDeviceNameFromMount(mountPath string) (string, int, error) { - return mountutils.GetDeviceNameFromMount(m, mountPath) -} - -// IsCorruptedMnt return true if err is about corrupted mount point -func (m NodeMounter) IsCorruptedMnt(err error) bool { - return mountutils.IsCorruptedMnt(err) -} - -// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code -// Please mirror the change to func MakeFile in ./sanity_test.go -func (m *NodeMounter) MakeFile(path string) error { - f, err := os.OpenFile(path, os.O_CREATE, os.FileMode(0644)) - if err != nil { - if !os.IsExist(err) { - return err - } - } - if err = f.Close(); err != nil { - return err - } - return nil -} - -// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code -// Please mirror the change to func MakeFile in ./sanity_test.go -func (m *NodeMounter) MakeDir(path string) error { - err := os.MkdirAll(path, os.FileMode(0755)) - if err != nil { - if !os.IsExist(err) { - return err - } - } - return nil -} - -// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code -// Please mirror the change to func MakeFile in ./sanity_test.go -func (m *NodeMounter) PathExists(path string) (bool, error) { - return mountutils.PathExists(path) -} - -func (m *NodeMounter) NeedResize(devicePath string, deviceMountPath string) (bool, error) { - return mountutils.NewResizeFs(m.Exec).NeedResize(devicePath, deviceMountPath) -} - -func (m *NodeMounter) getExtSize(devicePath string) (uint64, uint64, error) { - output, err := m.SafeFormatAndMount.Exec.Command("dumpe2fs", "-h", devicePath).CombinedOutput() - if err != nil { - return 0, 0, fmt.Errorf("failed to read size of filesystem on %s: %w: %s", devicePath, err, string(output)) - } - - blockSize, blockCount, _ := m.parseFsInfoOutput(string(output), ":", "block size", "block count") - - if blockSize == 0 { - return 0, 0, fmt.Errorf("could not find block size of device %s", devicePath) - } - if blockCount == 0 { - return 0, 0, fmt.Errorf("could not find block count of device %s", devicePath) - } - return blockSize, blockSize * blockCount, nil -} - -func (m *NodeMounter) getXFSSize(devicePath string) (uint64, uint64, error) { - output, err := m.SafeFormatAndMount.Exec.Command("xfs_io", "-c", "statfs", devicePath).CombinedOutput() - if err != nil { - return 0, 0, fmt.Errorf("failed to read size of filesystem on %s: %w: %s", devicePath, err, string(output)) - } - - blockSize, blockCount, _ := m.parseFsInfoOutput(string(output), "=", "geom.bsize", "geom.datablocks") - - if blockSize == 0 { - return 0, 0, fmt.Errorf("could not find block size of device %s", devicePath) - } - if blockCount == 0 { - return 0, 0, fmt.Errorf("could not find block count of device %s", devicePath) - } - return blockSize, blockSize * blockCount, nil -} - -func (m *NodeMounter) parseFsInfoOutput(cmdOutput string, spliter string, blockSizeKey string, blockCountKey string) (uint64, uint64, error) { - lines := strings.Split(cmdOutput, "\n") - var blockSize, blockCount uint64 - var err error - - for _, line := range lines { - tokens := strings.Split(line, spliter) - if len(tokens) != 2 { - continue - } - key, value := strings.ToLower(strings.TrimSpace(tokens[0])), strings.ToLower(strings.TrimSpace(tokens[1])) - if key == blockSizeKey { - blockSize, err = strconv.ParseUint(value, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse block size %s: %w", value, err) - } - } - if key == blockCountKey { - blockCount, err = strconv.ParseUint(value, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse block count %s: %w", value, err) - } - } - } - return blockSize, blockCount, err -} - -func (m *NodeMounter) Unpublish(path string) error { - // On linux, unpublish and unstage both perform an unmount - return m.Unstage(path) -} - -func (m *NodeMounter) Unstage(path string) error { - err := mountutils.CleanupMountPoint(path, m, false) - // Ignore the error when it contains "not mounted", because that indicates the - // world is already in the desired state - // - // mount-utils attempts to detect this on its own but fails when running on - // a read-only root filesystem, which our manifests use by default - if err == nil || strings.Contains(fmt.Sprint(err), "not mounted") { - return nil - } else { - return err - } -} - -func (m *NodeMounter) NewResizeFs() (Resizefs, error) { - return mountutils.NewResizeFs(m.Exec), nil -} diff --git a/pkg/driver/mount_test.go b/pkg/driver/mount_test.go deleted file mode 100644 index 1dc5de8cd4..0000000000 --- a/pkg/driver/mount_test.go +++ /dev/null @@ -1,420 +0,0 @@ -//go:build linux -// +build linux - -/* -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 driver - -import ( - "os" - "path/filepath" - "testing" - - "k8s.io/mount-utils" - - utilexec "k8s.io/utils/exec" - fakeexec "k8s.io/utils/exec/testing" -) - -func TestGetFileSystemSize(t *testing.T) { - cmdOutputSuccessXfs := - ` - statfs.f_bsize = 4096 - statfs.f_blocks = 1832448 - statfs.f_bavail = 1822366 - statfs.f_files = 3670016 - statfs.f_ffree = 3670012 - statfs.f_flags = 0x1020 - geom.bsize = 4096 - geom.agcount = 4 - geom.agblocks = 458752 - geom.datablocks = 1835008 - geom.rtblocks = 0 - geom.rtextents = 0 - geom.rtextsize = 1 - geom.sunit = 0 - geom.swidth = 0 - counts.freedata = 1822372 - counts.freertx = 0 - counts.freeino = 61 - counts.allocino = 64 -` - cmdOutputNoDataXfs := - ` - statfs.f_bsize = 4096 - statfs.f_blocks = 1832448 - statfs.f_bavail = 1822366 - statfs.f_files = 3670016 - statfs.f_ffree = 3670012 - statfs.f_flags = 0x1020 - geom.agcount = 4 - geom.agblocks = 458752 - geom.rtblocks = 0 - geom.rtextents = 0 - geom.rtextsize = 1 - geom.sunit = 0 - geom.swidth = 0 - counts.freedata = 1822372 - counts.freertx = 0 - counts.freeino = 61 - counts.allocino = 64 -` - cmdOutputSuccessExt4 := - ` -Filesystem volume name: cloudimg-rootfs -Last mounted on: / -Filesystem UUID: testUUID -Filesystem magic number: 0xEF53 -Filesystem revision #: 1 (dynamic) -Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit -Default mount options: user_xattr acl -Filesystem state: clean -Errors behavior: Continue -Filesystem OS type: Linux -Inode count: 3840000 -Block count: 5242880 -Reserved block count: 0 -Free blocks: 5514413 -Free inodes: 3677492 -First block: 0 -Block size: 4096 -Fragment size: 4096 -Group descriptor size: 64 -Reserved GDT blocks: 252 -Blocks per group: 32768 -Fragments per group: 32768 -Inodes per group: 16000 -Inode blocks per group: 1000 -Flex block group size: 16 -Mount count: 2 -Maximum mount count: -1 -Check interval: 0 () -Lifetime writes: 180 GB -Reserved blocks uid: 0 (user root) -Reserved blocks gid: 0 (group root) -First inode: 11 -Inode size: 256 -Required extra isize: 32 -Desired extra isize: 32 -Journal inode: 8 -Default directory hash: half_md4 -Directory Hash Seed: Test Hashing -Journal backup: inode blocks -Checksum type: crc32c -Checksum: 0x57705f62 -Journal features: journal_incompat_revoke journal_64bit journal_checksum_v3 -Journal size: 64M -Journal length: 16384 -Journal sequence: 0x00037109 -Journal start: 1 -Journal checksum type: crc32c -Journal checksum: 0xb7df3c6e -` - cmdOutputNoDataExt4 := - `Filesystem volume name: cloudimg-rootfs -Last mounted on: / -Filesystem UUID: testUUID -Filesystem magic number: 0xEF53 -Filesystem revision #: 1 (dynamic) -Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit -Default mount options: user_xattr acl -Filesystem state: clean -Errors behavior: Continue -Filesystem OS type: Linux -Inode count: 3840000 -Reserved block count: 0 -Free blocks: 5514413 -Free inodes: 3677492 -First block: 0 -Fragment size: 4096 -Group descriptor size: 64 -Reserved GDT blocks: 252 -Blocks per group: 32768 -Fragments per group: 32768 -Inodes per group: 16000 -Inode blocks per group: 1000 -Flex block group size: 16 -Mount count: 2 -Maximum mount count: -1 -Check interval: 0 () -Lifetime writes: 180 GB -Reserved blocks uid: 0 (user root) -Reserved blocks gid: 0 (group root) -First inode: 11 -Inode size: 256 -Required extra isize: 32 -Desired extra isize: 32 -Journal inode: 8 -Default directory hash: half_md4 -Directory Hash Seed: Test Hashing -Journal backup: inode blocks -Checksum type: crc32c -Checksum: 0x57705f62 -Journal features: journal_incompat_revoke journal_64bit journal_checksum_v3 -Journal size: 64M -Journal length: 16384 -Journal sequence: 0x00037109 -Journal start: 1 -Journal checksum type: crc32c -Journal checksum: 0xb7df3c6e -` - testcases := []struct { - name string - devicePath string - blocksize uint64 - blockCount uint64 - cmdOutput string - expectError bool - fsType string - }{ - { - name: "success parse xfs info", - devicePath: "/dev/test1", - blocksize: 4096, - blockCount: 1835008, - cmdOutput: cmdOutputSuccessXfs, - expectError: false, - fsType: "xfs", - }, - { - name: "block size not present - xfs", - devicePath: "/dev/test1", - blocksize: 0, - blockCount: 0, - cmdOutput: cmdOutputNoDataXfs, - expectError: true, - fsType: "xfs", - }, - { - name: "success parse ext info", - devicePath: "/dev/test1", - blocksize: 4096, - blockCount: 5242880, - cmdOutput: cmdOutputSuccessExt4, - expectError: false, - fsType: "ext4", - }, - { - name: "block size not present - ext4", - devicePath: "/dev/test1", - blocksize: 0, - blockCount: 0, - cmdOutput: cmdOutputNoDataExt4, - expectError: true, - fsType: "ext4", - }, - } - - for _, test := range testcases { - t.Run(test.name, func(t *testing.T) { - fcmd := fakeexec.FakeCmd{ - CombinedOutputScript: []fakeexec.FakeAction{ - func() ([]byte, []byte, error) { return []byte(test.cmdOutput), nil, nil }, - }, - } - fexec := fakeexec.FakeExec{ - CommandScript: []fakeexec.FakeCommandAction{ - func(cmd string, args ...string) utilexec.Cmd { - return fakeexec.InitFakeCmd(&fcmd, cmd, args...) - }, - }, - } - safe := mount.SafeFormatAndMount{ - Interface: mount.New(""), - Exec: &fexec, - } - fakeMounter := NodeMounter{&safe} - - var blockSize uint64 - var fsSize uint64 - var err error - switch test.fsType { - case "xfs": - blockSize, fsSize, err = fakeMounter.getXFSSize(test.devicePath) - case "ext4": - blockSize, fsSize, err = fakeMounter.getExtSize(test.devicePath) - } - - if blockSize != test.blocksize { - t.Fatalf("Parse wrong block size value, expect %d, but got %d", test.blocksize, blockSize) - } - if fsSize != test.blocksize*test.blockCount { - t.Fatalf("Parse wrong fs size value, expect %d, but got %d", test.blocksize*test.blockCount, fsSize) - } - if !test.expectError && err != nil { - t.Fatalf("Expect no error but got %v", err) - } - }) - } -} - -func TestNeedResize(t *testing.T) { - testcases := []struct { - name string - devicePath string - deviceMountPath string - deviceSize string - cmdOutputFsType string - expectError bool - expectResult bool - }{ - { - name: "False - Unsupported fs type", - devicePath: "/dev/test1", - deviceMountPath: "/mnt/test1", - deviceSize: "2048", - cmdOutputFsType: "TYPE=ntfs", - expectError: true, - expectResult: false, - }, - } - - for _, test := range testcases { - t.Run(test.name, func(t *testing.T) { - fcmd := fakeexec.FakeCmd{ - CombinedOutputScript: []fakeexec.FakeAction{ - func() ([]byte, []byte, error) { return []byte(test.deviceSize), nil, nil }, - func() ([]byte, []byte, error) { return []byte(test.cmdOutputFsType), nil, nil }, - }, - } - fexec := fakeexec.FakeExec{ - CommandScript: []fakeexec.FakeCommandAction{ - func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, - func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, - }, - } - safe := mount.SafeFormatAndMount{ - Interface: mount.New(""), - Exec: &fexec, - } - fakeMounter := NodeMounter{&safe} - - needResize, err := fakeMounter.NeedResize(test.devicePath, test.deviceMountPath) - if needResize != test.expectResult { - t.Fatalf("Expect result is %v but got %v", test.expectResult, needResize) - } - if !test.expectError && err != nil { - t.Fatalf("Expect no error but got %v", err) - } - }) - } -} - -func TestMakeDir(t *testing.T) { - // Setup the full driver and its environment - dir, err := os.MkdirTemp("", "mount-ebs-csi") - if err != nil { - t.Fatalf("error creating directory %v", err) - } - defer os.RemoveAll(dir) - - targetPath := filepath.Join(dir, "targetdir") - - mountObj, err := newNodeMounter() - if err != nil { - t.Fatalf("error creating mounter %v", err) - } - - if mountObj.MakeDir(targetPath) != nil { - t.Fatalf("Expect no error but got: %v", err) - } - - if mountObj.MakeDir(targetPath) != nil { - t.Fatalf("Expect no error but got: %v", err) - } - - if exists, err := mountObj.PathExists(targetPath); !exists { - t.Fatalf("Expect no error but got: %v", err) - } -} - -func TestMakeFile(t *testing.T) { - // Setup the full driver and its environment - dir, err := os.MkdirTemp("", "mount-ebs-csi") - if err != nil { - t.Fatalf("error creating directory %v", err) - } - defer os.RemoveAll(dir) - - targetPath := filepath.Join(dir, "targetfile") - - mountObj, err := newNodeMounter() - if err != nil { - t.Fatalf("error creating mounter %v", err) - } - - if mountObj.MakeFile(targetPath) != nil { - t.Fatalf("Expect no error but got: %v", err) - } - - if mountObj.MakeFile(targetPath) != nil { - t.Fatalf("Expect no error but got: %v", err) - } - - if exists, err := mountObj.PathExists(targetPath); !exists { - t.Fatalf("Expect no error but got: %v", err) - } - -} - -func TestPathExists(t *testing.T) { - // Setup the full driver and its environment - dir, err := os.MkdirTemp("", "mount-ebs-csi") - if err != nil { - t.Fatalf("error creating directory %v", err) - } - defer os.RemoveAll(dir) - - targetPath := filepath.Join(dir, "notafile") - - mountObj, err := newNodeMounter() - if err != nil { - t.Fatalf("error creating mounter %v", err) - } - - exists, err := mountObj.PathExists(targetPath) - - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } - - if exists { - t.Fatalf("Expected file %s to not exist", targetPath) - } - -} - -func TestGetDeviceName(t *testing.T) { - // Setup the full driver and its environment - dir, err := os.MkdirTemp("", "mount-ebs-csi") - if err != nil { - t.Fatalf("error creating directory %v", err) - } - defer os.RemoveAll(dir) - - targetPath := filepath.Join(dir, "notafile") - - mountObj, err := newNodeMounter() - if err != nil { - t.Fatalf("error creating mounter %v", err) - } - - if _, _, err := mountObj.GetDeviceNameFromMount(targetPath); err != nil { - t.Fatalf("Expect no error but got: %v", err) - } - -} diff --git a/pkg/driver/node.go b/pkg/driver/node.go index ff12ef3e2e..d9a3680f66 100644 --- a/pkg/driver/node.go +++ b/pkg/driver/node.go @@ -30,6 +30,7 @@ import ( "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud/metadata" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver/internal" + "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/util" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -81,53 +82,35 @@ var ( } ) -// nodeService represents the node service of CSI driver -type nodeService struct { - metadata metadata.MetadataService - mounter Mounter - deviceIdentifier DeviceIdentifier - inFlight *internal.InFlight - options *Options +// NodeService represents the node service of CSI driver +type NodeService struct { + metadata metadata.MetadataService + mounter mounter.Mounter + inFlight *internal.InFlight + options *Options } -// newNodeService creates a new node service -// it panics if failed to create the service -func newNodeService(o *Options) nodeService { +// NewNodeService creates a new node service +func NewNodeService(o *Options, md metadata.MetadataService, m mounter.Mounter, k kubernetes.Interface) *NodeService { klog.V(5).InfoS("[Debug] Retrieving node info from metadata service") region := os.Getenv("AWS_REGION") klog.InfoS("regionFromSession Node service", "region", region) - cfg := metadata.MetadataServiceConfig{ - EC2MetadataClient: metadata.DefaultEC2MetadataClient, - K8sAPIClient: metadata.DefaultKubernetesAPIClient, - } - - metadata, err := metadata.NewMetadataService(cfg, region) - if err != nil { - panic(err) - } - - nodeMounter, err := newNodeMounter() - if err != nil { - panic(err) - } - // Remove taint from node to indicate driver startup success // This is done at the last possible moment to prevent race conditions or false positive removals time.AfterFunc(taintRemovalInitialDelay, func() { - removeTaintInBackground(cfg.K8sAPIClient, removeNotReadyTaint) + removeTaintInBackground(k, taintRemovalBackoff, removeNotReadyTaint) }) - return nodeService{ - metadata: metadata, - mounter: nodeMounter, - deviceIdentifier: newNodeDeviceIdentifier(), - inFlight: internal.NewInFlight(), - options: o, + return &NodeService{ + metadata: md, + mounter: m, + inFlight: internal.NewInFlight(), + options: o, } } -func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { +func (d *NodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { klog.V(4).InfoS("NodeStageVolume: called", "args", *req) volumeID := req.GetVolumeId() @@ -225,7 +208,7 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol } } - source, err := d.findDevicePath(devicePath, volumeID, partition) + source, err := d.mounter.FindDevicePath(devicePath, volumeID, partition, d.metadata.GetRegion()) if err != nil { return nil, status.Errorf(codes.Internal, "Failed to find device path %s. %v", devicePath, err) } @@ -304,12 +287,8 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol } if needResize { - r, err := d.mounter.NewResizeFs() - if err != nil { - return nil, status.Errorf(codes.Internal, "Error attempting to create new ResizeFs: %v", err) - } klog.V(2).InfoS("Volume needs resizing", "source", source) - if _, err := r.Resize(source, target); err != nil { + if _, err := d.mounter.Resize(source, target); err != nil { return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q): %v", volumeID, source, err) } } @@ -317,7 +296,7 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol return &csi.NodeStageVolumeResponse{}, nil } -func (d *nodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { +func (d *NodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { klog.V(4).InfoS("NodeUnstageVolume: called", "args", *req) volumeID := req.GetVolumeId() if len(volumeID) == 0 { @@ -367,7 +346,7 @@ func (d *nodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstag return &csi.NodeUnstageVolumeResponse{}, nil } -func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { +func (d *NodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { klog.V(4).InfoS("NodeExpandVolume: called", "args", *req) volumeID := req.GetVolumeId() if len(volumeID) == 0 { @@ -394,13 +373,13 @@ func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandV } else { // TODO use util.GenericResizeFS // VolumeCapability is nil, check if volumePath point to a block device - isBlock, err := d.IsBlockDevice(volumePath) + isBlock, err := d.mounter.IsBlockDevice(volumePath) if err != nil { return nil, status.Errorf(codes.Internal, "failed to determine if volumePath [%v] is a block device: %v", volumePath, err) } if isBlock { // Skip resizing for Block NodeExpandVolume - bcap, err := d.getBlockSizeBytes(volumePath) + bcap, err := d.mounter.GetBlockSizeBytes(volumePath) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get block capacity on path %s: %v", req.GetVolumePath(), err) } @@ -414,29 +393,24 @@ func (d *nodeService) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandV return nil, status.Errorf(codes.Internal, "failed to get device name from mount %s: %v", volumePath, err) } - devicePath, err := d.findDevicePath(deviceName, volumeID, "") + devicePath, err := d.mounter.FindDevicePath(deviceName, volumeID, "", d.metadata.GetRegion()) if err != nil { return nil, status.Errorf(codes.Internal, "failed to find device path for device name %s for mount %s: %v", deviceName, req.GetVolumePath(), err) } - r, err := d.mounter.NewResizeFs() - if err != nil { - return nil, status.Errorf(codes.Internal, "Error attempting to create new ResizeFs: %v", err) - } - // TODO: lock per volume ID to have some idempotency - if _, err = r.Resize(devicePath, volumePath); err != nil { - return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q): %v", volumeID, devicePath, err) + if _, err = d.mounter.Resize(devicePath, volumePath); err != nil { + return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q): %v", volumeID, devicePath, err) } - bcap, err := d.getBlockSizeBytes(devicePath) + bcap, err := d.mounter.GetBlockSizeBytes(devicePath) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get block capacity on path %s: %v", req.GetVolumePath(), err) } return &csi.NodeExpandVolumeResponse{CapacityBytes: bcap}, nil } -func (d *nodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { +func (d *NodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { klog.V(4).InfoS("NodePublishVolume: called", "args", *req) volumeID := req.GetVolumeId() if len(volumeID) == 0 { @@ -489,7 +463,7 @@ func (d *nodeService) NodePublishVolume(ctx context.Context, req *csi.NodePublis return &csi.NodePublishVolumeResponse{}, nil } -func (d *nodeService) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { +func (d *NodeService) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { klog.V(4).InfoS("NodeUnpublishVolume: called", "args", *req) volumeID := req.GetVolumeId() if len(volumeID) == 0 { @@ -500,9 +474,11 @@ func (d *nodeService) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpu if len(target) == 0 { return nil, status.Error(codes.InvalidArgument, "Target path not provided") } + if ok := d.inFlight.Insert(volumeID); !ok { return nil, status.Errorf(codes.Aborted, VolumeOperationAlreadyExists, volumeID) } + defer func() { klog.V(4).InfoS("NodeUnPublishVolume: volume operation finished", "volumeId", volumeID) d.inFlight.Delete(volumeID) @@ -517,7 +493,7 @@ func (d *nodeService) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpu return &csi.NodeUnpublishVolumeResponse{}, nil } -func (d *nodeService) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { +func (d *NodeService) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { klog.V(4).InfoS("NodeGetVolumeStats: called", "args", *req) if len(req.GetVolumeId()) == 0 { return nil, status.Error(codes.InvalidArgument, "NodeGetVolumeStats volume ID was empty") @@ -534,15 +510,15 @@ func (d *nodeService) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVo return nil, status.Errorf(codes.NotFound, "path %s does not exist", req.GetVolumePath()) } - isBlock, err := d.IsBlockDevice(req.GetVolumePath()) + isBlock, err := d.mounter.IsBlockDevice(req.GetVolumePath()) if err != nil { return nil, status.Errorf(codes.Internal, "failed to determine whether %s is block device: %v", req.GetVolumePath(), err) } if isBlock { - bcap, blockErr := d.getBlockSizeBytes(req.GetVolumePath()) + bcap, blockErr := d.mounter.GetBlockSizeBytes(req.GetVolumePath()) if blockErr != nil { - return nil, status.Errorf(codes.Internal, "failed to get block capacity on path %s: %v", req.GetVolumePath(), err) + return nil, status.Errorf(codes.Internal, "failed to get block capacity on path %s: %v", req.GetVolumePath(), blockErr) } return &csi.NodeGetVolumeStatsResponse{ Usage: []*csi.VolumeUsage{ @@ -580,7 +556,7 @@ func (d *nodeService) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVo } -func (d *nodeService) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { +func (d *NodeService) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { klog.V(4).InfoS("NodeGetCapabilities: called", "args", *req) var caps []*csi.NodeServiceCapability for _, cap := range nodeCaps { @@ -596,7 +572,7 @@ func (d *nodeService) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetC return &csi.NodeGetCapabilitiesResponse{Capabilities: caps}, nil } -func (d *nodeService) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { +func (d *NodeService) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { klog.V(4).InfoS("NodeGetInfo: called", "args", *req) zone := d.metadata.GetAvailabilityZone() @@ -627,7 +603,7 @@ func (d *nodeService) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoReque }, nil } -func (d *nodeService) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, mountOptions []string) error { +func (d *NodeService) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, mountOptions []string) error { target := req.GetTargetPath() volumeID := req.GetVolumeId() volumeContext := req.GetVolumeContext() @@ -649,7 +625,7 @@ func (d *nodeService) nodePublishVolumeForBlock(req *csi.NodePublishVolumeReques } } - source, err := d.findDevicePath(devicePath, volumeID, partition) + source, err := d.mounter.FindDevicePath(devicePath, volumeID, partition, d.metadata.GetRegion()) if err != nil { return status.Errorf(codes.Internal, "Failed to find device path %s. %v", devicePath, err) } @@ -703,7 +679,7 @@ func (d *nodeService) nodePublishVolumeForBlock(req *csi.NodePublishVolumeReques // isMounted checks if target is mounted. It does NOT return an error if target // doesn't exist. -func (d *nodeService) isMounted(_ string, target string) (bool, error) { +func (d *NodeService) isMounted(_ string, target string) (bool, error) { /* Checking if it's a mount point using IsLikelyNotMountPoint. There are three different return values, 1. true, err when the directory does not exist or corrupted. @@ -743,7 +719,7 @@ func (d *nodeService) isMounted(_ string, target string) (bool, error) { return !notMnt, nil } -func (d *nodeService) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeRequest, mountOptions []string, mode *csi.VolumeCapability_Mount) error { +func (d *NodeService) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeRequest, mountOptions []string, mode *csi.VolumeCapability_Mount) error { target := req.GetTargetPath() source := req.GetStagingTargetPath() if m := mode.Mount; m != nil { @@ -754,7 +730,7 @@ func (d *nodeService) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeR } } - if err := d.preparePublishTarget(target); err != nil { + if err := d.mounter.PreparePublishTarget(target); err != nil { return status.Errorf(codes.Internal, err.Error()) } @@ -786,11 +762,11 @@ func (d *nodeService) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeR } // getVolumesLimit returns the limit of volumes that the node supports -func (d *nodeService) getVolumesLimit() int64 { +func (d *NodeService) getVolumesLimit() int64 { + if d.options.VolumeAttachLimit >= 0 { return d.options.VolumeAttachLimit } - if util.IsSBE(d.metadata.GetRegion()) { return sbeDeviceVolumeAttachmentLimit } @@ -875,8 +851,8 @@ type JSONPatch struct { } // removeTaintInBackground is a goroutine that retries removeNotReadyTaint with exponential backoff -func removeTaintInBackground(k8sClient metadata.KubernetesAPIClient, removalFunc func(metadata.KubernetesAPIClient) error) { - backoffErr := wait.ExponentialBackoff(taintRemovalBackoff, func() (bool, error) { +func removeTaintInBackground(k8sClient kubernetes.Interface, backoff wait.Backoff, removalFunc func(kubernetes.Interface) error) { + backoffErr := wait.ExponentialBackoff(backoff, func() (bool, error) { err := removalFunc(k8sClient) if err != nil { klog.ErrorS(err, "Unexpected failure when attempting to remove node taint(s)") @@ -893,19 +869,13 @@ func removeTaintInBackground(k8sClient metadata.KubernetesAPIClient, removalFunc // removeNotReadyTaint removes the taint ebs.csi.aws.com/agent-not-ready from the local node // This taint can be optionally applied by users to prevent startup race conditions such as // https://github.com/kubernetes/kubernetes/issues/95911 -func removeNotReadyTaint(k8sClient metadata.KubernetesAPIClient) error { +func removeNotReadyTaint(clientset kubernetes.Interface) error { nodeName := os.Getenv("CSI_NODE_NAME") if nodeName == "" { klog.V(4).InfoS("CSI_NODE_NAME missing, skipping taint removal") return nil } - clientset, err := k8sClient() - if err != nil { - klog.V(4).InfoS("Failed to setup k8s client") - return nil //lint:ignore nilerr If there are no k8s credentials, treat that as a soft failure - } - node, err := clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) if err != nil { return err @@ -980,7 +950,7 @@ func recheckFormattingOptionParameter(context map[string]string, key string, fsC if ok { // This check is already performed on the controller side // However, because it is potentially security-sensitive, we redo it here to be safe - if isAlphanumeric := util.StringIsAlphanumeric(value); !isAlphanumeric { + if isAlphanumeric := util.StringIsAlphanumeric(v); !isAlphanumeric { return "", status.Errorf(codes.InvalidArgument, "Invalid %s (aborting!): %v", key, err) } diff --git a/pkg/driver/node_linux_test.go b/pkg/driver/node_linux_test.go deleted file mode 100644 index 00c787ec91..0000000000 --- a/pkg/driver/node_linux_test.go +++ /dev/null @@ -1,285 +0,0 @@ -//go:build linux -// +build linux - -/* -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 driver - -import ( - "fmt" - "io/fs" - "os" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud/metadata" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver/internal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestFindDevicePath(t *testing.T) { - devicePath := "/dev/xvdaa" - nvmeDevicePath := "/dev/nvme1n1" - snowDevicePath := "/dev/vda" - volumeID := "vol-test" - nvmeName := "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_voltest" - deviceFileInfo := fs.FileInfo(&fakeFileInfo{devicePath, os.ModeDevice}) - symlinkFileInfo := fs.FileInfo(&fakeFileInfo{nvmeName, os.ModeSymlink}) - nvmeDevicePathSymlinkFileInfo := fs.FileInfo(&fakeFileInfo{nvmeDevicePath, os.ModeSymlink}) - type testCase struct { - name string - devicePath string - volumeID string - partition string - expectMock func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) - expectDevicePath string - expectError string - } - testCases := []testCase{ - { - name: "11: device path exists and nvme device path exists", - devicePath: devicePath, - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(nvmeDevicePathSymlinkFileInfo, nil), - mockDeviceIdentifier.EXPECT().EvalSymlinks(gomock.Eq(devicePath)).Return(nvmeDevicePath, nil), - ) - }, - expectDevicePath: nvmeDevicePath, - }, - { - name: "10: device path exists and nvme device path doesn't exist", - devicePath: devicePath, - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil), - ) - }, - expectDevicePath: devicePath, - }, - { - name: "01: device path doesn't exist and nvme device path exists", - devicePath: devicePath, - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(false, nil), - - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(nvmeName)).Return(symlinkFileInfo, nil), - mockDeviceIdentifier.EXPECT().EvalSymlinks(gomock.Eq(symlinkFileInfo.Name())).Return(nvmeDevicePath, nil), - ) - }, - expectDevicePath: nvmeDevicePath, - }, - { - name: "00: device path doesn't exist and nvme device path doesn't exist", - devicePath: devicePath, - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(false, nil), - - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(nvmeName)).Return(nil, os.ErrNotExist), - ) - }, - expectError: errNoDevicePathFound(devicePath, volumeID).Error(), - }, - { - name: "success: device path doesn't exist and snow path exists", - devicePath: devicePath, - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(false, nil), - - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(nvmeName)).Return(nil, os.ErrNotExist), - ) - }, - expectDevicePath: snowDevicePath, - }, - { - name: "success: non-standard snow device path", - devicePath: "/dev/sda", - volumeID: volumeID, - partition: "", - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq("/dev/sda")).Return(false, nil), - - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(nvmeName)).Return(nil, os.ErrNotExist), - ) - }, - expectDevicePath: snowDevicePath, - }, - } - // The partition variant of each case should be the same except the partition - // is expected to be appended to devicePath - generatedTestCases := []testCase{} - for _, tc := range testCases { - tc.name += " (with partition)" - tc.partition = "1" - if tc.expectDevicePath == devicePath || tc.expectDevicePath == snowDevicePath { - tc.expectDevicePath += tc.partition - } else if tc.expectDevicePath == nvmeDevicePath { - tc.expectDevicePath += "p" + tc.partition - } - generatedTestCases = append(generatedTestCases, tc) - } - testCases = append(testCases, generatedTestCases...) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - nodeDriver := nodeService{ - metadata: &metadata.Metadata{}, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - options: &Options{}, - } - - if tc.expectDevicePath == snowDevicePath+tc.partition { - nodeDriver = nodeService{ - metadata: &metadata.Metadata{Region: "snow"}, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - options: &Options{}, - } - } - - if tc.expectMock != nil { - tc.expectMock(*mockMounter, *mockDeviceIdentifier) - } - - devicePath, err := nodeDriver.findDevicePath(tc.devicePath, tc.volumeID, tc.partition) - if tc.expectError != "" { - assert.EqualError(t, err, tc.expectError) - } else { - assert.Equal(t, tc.expectDevicePath, devicePath) - require.NoError(t, err) - } - }) - } -} - -const fakeVolumeName = "vol11111111111111111" -const fakeIncorrectVolumeName = "vol21111111111111111" - -func TestVerifyVolumeSerialMatch(t *testing.T) { - type testCase struct { - name string - execOutput string - execError error - expectError bool - } - testCases := []testCase{ - { - name: "success: empty", - execOutput: "", - }, - { - name: "success: single", - execOutput: fakeVolumeName, - }, - { - name: "success: multiple", - execOutput: fakeVolumeName + "\n" + fakeVolumeName + "\n" + fakeVolumeName, - }, - { - name: "success: whitespace", - execOutput: "\t " + fakeVolumeName + " \n \t ", - }, - { - name: "success: extra output", - execOutput: "extra output without name in it\n" + fakeVolumeName, - }, - { - name: "success: failed command", - execError: fmt.Errorf("Exec failed"), - }, - { - name: "failure: wrong volume", - execOutput: fakeIncorrectVolumeName, - expectError: true, - }, - { - name: "failure: mixed", - execOutput: fakeVolumeName + "\n" + fakeIncorrectVolumeName, - expectError: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockExecRunner := func(_ string, _ ...string) ([]byte, error) { - return []byte(tc.execOutput), tc.execError - } - - result := verifyVolumeSerialMatch("path", fakeVolumeName, mockExecRunner) - if tc.expectError { - assert.Error(t, result) - } else { - require.NoError(t, result) - } - }) - } -} - -type fakeFileInfo struct { - name string - mode os.FileMode -} - -func (fi *fakeFileInfo) Name() string { - return fi.name -} - -func (fi *fakeFileInfo) Size() int64 { - return 0 -} - -func (fi *fakeFileInfo) Mode() os.FileMode { - return fi.mode -} - -func (fi *fakeFileInfo) ModTime() time.Time { - return time.Now() -} - -func (fi *fakeFileInfo) IsDir() bool { - return false -} - -func (fi *fakeFileInfo) Sys() interface{} { - return nil -} diff --git a/pkg/driver/node_test.go b/pkg/driver/node_test.go index e336cb5b15..1b4856e9f9 100644 --- a/pkg/driver/node_test.go +++ b/pkg/driver/node_test.go @@ -1,8 +1,5 @@ -//go:build linux -// +build linux - /* -Copyright 2019 The Kubernetes Authors. +Copyright 2024 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. @@ -23,1627 +20,1980 @@ import ( "context" "errors" "fmt" - "io/fs" "os" "reflect" "runtime" - "strings" "testing" + "time" "github.com/aws/aws-sdk-go-v2/aws/arn" - "github.com/container-storage-interface/spec/lib/go/csi" + csi "github.com/container-storage-interface/spec/lib/go/csi" "github.com/golang/mock/gomock" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/cloud/metadata" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/driver/internal" + "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" "github.com/stretchr/testify/assert" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" ) -var ( - volumeID = "voltest" - nvmeName = "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_voltest" - symlinkFileInfo = fs.FileInfo(&fakeFileInfo{nvmeName, os.ModeSymlink}) -) +func TestNewNodeService(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() -func TestNodeStageVolume(t *testing.T) { + mockMetadataService := metadata.NewMockMetadataService(ctrl) + mockMounter := mounter.NewMockMounter(ctrl) + mockKubernetesClient := NewMockKubernetesClient(ctrl) - var ( - targetPath = "/test/path" - devicePath = "/dev/fake" - nvmeDevicePath = "/dev/nvmefake1n1" - deviceFileInfo = fs.FileInfo(&fakeFileInfo{devicePath, os.ModeDevice}) - //deviceSymlinkFileInfo = fs.FileInfo(&fakeFileInfo{nvmeDevicePath, os.ModeSymlink}) - stdVolCap = &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{ - Mount: &csi.VolumeCapability_MountVolume{ - FsType: FSTypeExt4, - }, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, - } - stdVolContext = map[string]string{VolumeAttributePartition: "1"} - devicePathWithPartition = devicePath + "1" - // With few exceptions, all "success" non-block cases have roughly the same - // expected calls and only care about testing the FormatAndMountSensitiveWithFormatOptions call. The - // exceptions should not call this, instead they should define expectMock - // from scratch. - successExpectMock = func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(false, nil) - mockMounter.EXPECT().MakeDir(targetPath).Return(nil) - mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) - } - ) + os.Setenv("AWS_REGION", "us-west-2") + defer os.Unsetenv("AWS_REGION") + + options := &Options{} + + nodeService := NewNodeService(options, mockMetadataService, mockMounter, mockKubernetesClient) + + if nodeService == nil { + t.Fatal("Expected NewNodeService to return a non-nil NodeService") + } + + if nodeService.metadata != mockMetadataService { + t.Error("Expected NodeService.metadata to be set to the mock MetadataService") + } + + if nodeService.mounter != mockMounter { + t.Error("Expected NodeService.mounter to be set to the mock Mounter") + } + + if nodeService.inFlight == nil { + t.Error("Expected NodeService.inFlight to be initialized") + } + + if nodeService.options != options { + t.Error("Expected NodeService.options to be set to the provided options") + } +} + +func TestNodeStageVolume(t *testing.T) { testCases := []struct { name string - request *csi.NodeStageVolumeRequest - inFlightFunc func(*internal.InFlight) *internal.InFlight - expectMock func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) - expectedCode codes.Code + req *csi.NodeStageVolumeRequest + mounterMock func(ctrl *gomock.Controller) *mounter.MockMounter + metadataMock func(ctrl *gomock.Controller) *metadata.MockMetadataService + expectedErr error + inflight bool }{ { - name: "success normal", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, + name: "success", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{DevicePathKey: "/dev/xvdba"}, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Any(), gomock.Any()).Return(false, nil) + return m }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Len(0)) + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, + expectedErr: nil, }, { - name: "success normal [raw block]", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "missing_volume_id", + req: &csi.NodeStageVolumeRequest{ + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Nil(), gomock.Len(0)).Times(0) }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID not provided"), }, { - name: "success with mount options", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "missing_staging_target", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ Mount: &csi.VolumeCapability_MountVolume{ - MountFlags: []string{"dirsync", "noexec"}, + FsType: "ext4", }, }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, - VolumeId: volumeID, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt4), gomock.Eq([]string{"dirsync", "noexec"}), gomock.Nil(), gomock.Len(0)) + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "Staging target not provided"), + }, + { + name: "missing_volume_capability", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "Volume capability not provided"), }, { - name: "success fsType ext3", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "invalid_volume_attribute", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ Mount: &csi.VolumeCapability_MountVolume{ - FsType: FSTypeExt3, + FsType: "ext4", }, }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt3), gomock.Any(), gomock.Nil(), gomock.Len(0)) + VolumeContext: map[string]string{ + VolumeAttributePartition: "invalid-partition", + }, }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "Volume Attribute is not valid"), }, { - name: "success mount with default fsType ext4", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "unsupported_volume_capability", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ Mount: &csi.VolumeCapability_MountVolume{ - FsType: "", + FsType: "ext4", }, }, AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + Mode: csi.VolumeCapability_AccessMode_UNKNOWN, }, }, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt4), gomock.Any(), gomock.Nil(), gomock.Len(0)) }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "Volume capability not supported"), }, { - name: "success device already mounted at target", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, nil) - mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return(devicePath, 1, nil) - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Nil(), gomock.Len(0)).Times(0) + name: "block_volume", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, }, + mounterMock: nil, + metadataMock: nil, + expectedErr: nil, }, { - name: "success nvme device already mounted at target", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, nil) - - // If the device is nvme GetDeviceNameFromMount should return the - // canonical device path - mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return(nvmeDevicePath, 1, nil) - - // The publish context device path may not exist but the driver should - // find the canonical device path (see TestFindDevicePath), compare it - // to the one returned by GetDeviceNameFromMount, and then skip - // FormatAndMountSensitiveWithFormatOptions - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(false, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(nvmeName)).Return(symlinkFileInfo, nil) - mockDeviceIdentifier.EXPECT().EvalSymlinks(gomock.Eq(symlinkFileInfo.Name())).Return(nvmeDevicePath, nil) - - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Nil(), gomock.Len(0)).Times(0) + name: "missing_mount_volume", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{}, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Error(codes.InvalidArgument, "NodeStageVolume: mount is nil within volume capability"), }, { - name: "success with partition", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeContext: stdVolContext, - VolumeId: volumeID, - }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(false, nil) - mockMounter.EXPECT().MakeDir(targetPath).Return(nil) - mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - // The device path argument should be canonicalized to contain the - // partition - mockMounter.EXPECT().NeedResize(gomock.Eq(devicePathWithPartition), gomock.Eq(targetPath)).Return(false, nil) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePathWithPartition), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Len(0)) + name: "default_fstype", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{DevicePathKey: "/dev/xvdba"}, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(false, nil) + m.EXPECT().MakeDir(gomock.Any()).Return(nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 0, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), defaultFsType, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Any(), gomock.Any()).Return(false, nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: nil, + }, + { + name: "invalid_fstype", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "invalid", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, }, + mounterMock: nil, + metadataMock: nil, + expectedErr: status.Errorf(codes.InvalidArgument, "NodeStageVolume: invalid fstype invalid"), }, { - name: "success with invalid partition config, will ignore partition", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeContext: map[string]string{VolumeAttributePartition: "0"}, - VolumeId: volumeID, + name: "invalid_block_size", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + BlockSizeKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Len(0)) + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil + }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid blocksize (aborting!): "), }, { - name: "success with block size", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{BlockSizeKey: "1024"}, + name: "invalid_inode_size", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + InodeSizeKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-b", "1024"})) + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil + }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid inodesize (aborting!): "), }, { - name: "success with inode size in ext4", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{InodeSizeKey: "256"}, + name: "invalid_bytes_per_inode", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + BytesPerInodeKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-I", "256"})) + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil + }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid bytesperinode (aborting!): "), }, { - name: "success with inode size in xfs", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "invalid_number_of_inodes", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Mount{ Mount: &csi.VolumeCapability_MountVolume{ - FsType: FSTypeXfs, + FsType: "ext4", }, }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, - VolumeId: volumeID, - VolumeContext: map[string]string{InodeSizeKey: "256"}, + VolumeContext: map[string]string{ + NumberOfInodesKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeXfs), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-i", "size=256"})) + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid numberofinodes (aborting!): "), }, { - name: "success with bytes-per-inode", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{BytesPerInodeKey: "8192"}, + name: "invalid_ext4_bigalloc", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + Ext4BigAllocKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-i", "8192"})) + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid ext4bigalloc (aborting!): "), }, { - name: "success with number-of-inodes", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{NumberOfInodesKey: "13107200"}, + name: "invalid_ext4_cluster_size", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + Ext4ClusterSizeKey: "-", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-N", "13107200"})) + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil }, + expectedErr: status.Error(codes.InvalidArgument, "Invalid ext4clustersize (aborting!): "), }, { - name: "success with bigalloc feature flag enabled in ext4", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{Ext4BigAllocKey: "true"}, + name: "device_path_not_provided", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + Ext4ClusterSizeKey: "51", + }, + PublishContext: map[string]string{}, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-O", "bigalloc"})) + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil + }, + expectedErr: status.Error(codes.InvalidArgument, "Device path not provided"), }, { - name: "success with custom cluster size and bigalloc feature flag enabled in ext4", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - VolumeContext: map[string]string{Ext4BigAllocKey: "true", Ext4ClusterSizeKey: "16384"}, + name: "volume_operation_already_exists", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectMock: func(mockMounter MockMounter, mockDeviceIdentifier MockDeviceIdentifier) { - successExpectMock(mockMounter, mockDeviceIdentifier) - mockMounter.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any(), gomock.Nil(), gomock.Eq([]string{"-O", "bigalloc", "-C", "16384"})) + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + return m }, - }, - { - name: "fail no VolumeId", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + return nil }, - expectedCode: codes.InvalidArgument, + expectedErr: status.Errorf(codes.Aborted, VolumeOperationAlreadyExists, "vol-test"), + inflight: true, }, { - name: "fail no mount", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "valid_partition", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{}, + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, AccessMode: &csi.VolumeCapability_AccessMode{ Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, + VolumeContext: map[string]string{ + VolumeAttributePartition: "1", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), "1", gomock.Any()).Return("/dev/xvdba1", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Any(), gomock.Any()).Return(false, nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, - expectedCode: codes.InvalidArgument, + expectedErr: nil, }, { - name: "fail no StagingTargetPath", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - VolumeCapability: stdVolCap, - VolumeId: volumeID, + name: "invalid_partition", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + VolumeAttributePartition: "0", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), "", gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Any(), gomock.Any()).Return(false, nil) + return m }, - expectedCode: codes.InvalidArgument, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: nil, }, { - name: "fail no VolumeCapability", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeId: volumeID, + name: "find_device_path_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Eq("/dev/xvdba"), gomock.Eq("vol-test"), gomock.Eq(""), gomock.Eq("us-west-2")).Return("", errors.New("find device path error")) + return m }, - expectedCode: codes.InvalidArgument, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Errorf(codes.Internal, "Failed to find device path %s. %v", "/dev/xvdba", errors.New("find device path error")), }, { - name: "fail invalid VolumeCapability", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, + name: "path_exists_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_UNKNOWN, + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, }, }, - VolumeId: volumeID, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectedCode: codes.InvalidArgument, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(false, errors.New("path exists error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "failed to check if target \"/staging/path\" exists: path exists error"), }, { - name: "fail no devicePath", - request: &csi.NodeStageVolumeRequest{ - VolumeCapability: stdVolCap, - VolumeId: volumeID, + name: "create_target_dir_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectedCode: codes.InvalidArgument, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(false, nil) + m.EXPECT().MakeDir(gomock.Eq("/staging/path")).Return(errors.New("make dir error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "could not create target dir \"/staging/path\": make dir error"), }, { - name: "fail invalid volumeContext", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeContext: map[string]string{VolumeAttributePartition: "partition1"}, - VolumeId: volumeID, + name: "get_device_name_from_mount_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - expectedCode: codes.InvalidArgument, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/staging/path")).Return("", 0, errors.New("get device name error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "failed to check if volume is already mounted: get device name error"), }, { - name: "fail with in-flight request", - request: &csi.NodeStageVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, + name: "volume_already_staged", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, - inFlightFunc: func(inFlight *internal.InFlight) *internal.InFlight { - inFlight.Insert(volumeID) - return inFlight + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/staging/path")).Return("/dev/xvdba", 1, nil) + return m }, - expectedCode: codes.Aborted, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: nil, }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) + { + name: "format_and_mount_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/staging/path")).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path"), gomock.Eq("ext4"), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("format and mount error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "could not format \"/dev/xvdba\" and mount it at \"/staging/path\": format and mount error"), + }, + { + name: "need_resize_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path")).Return(false, errors.New("need resize error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "Could not determine if volume \"vol-test\" (\"/dev/xvdba\") need to be resized: need resize error"), + }, + { + name: "resize_error", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + m.EXPECT().NeedResize(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().Resize(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path")).Return(false, errors.New("resize error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: status.Error(codes.Internal, "Could not resize volume \"vol-test\" (\"/dev/xvdba\"): resize error"), + }, + { + name: "format_options_ext4", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "ext4", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + BlockSizeKey: "4096", + InodeSizeKey: "512", + BytesPerInodeKey: "16384", + NumberOfInodesKey: "1000000", + Ext4BigAllocKey: "true", + Ext4ClusterSizeKey: "65536", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/staging/path")).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path"), gomock.Eq("ext4"), gomock.Any(), gomock.Any(), gomock.Eq([]string{"-b", "4096", "-I", "512", "-i", "16384", "-N", "1000000", "-O", "bigalloc", "-C", "65536"})).Return(nil) + m.EXPECT().NeedResize(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path")).Return(false, nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: nil, + }, + { + name: "format_options_xfs", + req: &csi.NodeStageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{ + FsType: "xfs", + }, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + VolumeContext: map[string]string{ + BlockSizeKey: "4096", + InodeSizeKey: "512", + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Eq("/staging/path")).Return(true, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/staging/path")).Return("", 1, nil) + m.EXPECT().FormatAndMountSensitiveWithFormatOptions(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path"), gomock.Eq("xfs"), gomock.Any(), gomock.Any(), gomock.Eq([]string{"-b", "size=4096", "-i", "size=512"})).Return(nil) + m.EXPECT().NeedResize(gomock.Eq("/dev/xvdba"), gomock.Eq("/staging/path")).Return(false, nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl) + } - inFlight := internal.NewInFlight() - if tc.inFlightFunc != nil { - tc.inFlightFunc(inFlight) + var metadata *metadata.MockMetadataService + if tc.metadataMock != nil { + metadata = tc.metadataMock(ctrl) } - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: inFlight, + driver := &NodeService{ + metadata: metadata, + mounter: mounter, + inFlight: internal.NewInFlight(), } - if tc.expectMock != nil { - tc.expectMock(*mockMounter, *mockDeviceIdentifier) + if tc.inflight { + driver.inFlight.Insert("vol-test") } - _, err := awsDriver.NodeStageVolume(context.TODO(), tc.request) - if tc.expectedCode != codes.OK { - expectErr(t, err, tc.expectedCode) - } else if err != nil { - t.Fatalf("Expect no error but got: %v", err) + _, err := driver.NodeStageVolume(context.Background(), tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr, err) } }) } } -func TestNodeUnstageVolume(t *testing.T) { - targetPath := "/test/path" - devicePath := "/dev/fake" - +func TestGetVolumesLimit(t *testing.T) { testCases := []struct { - name string - testFunc func(t *testing.T) + name string + expectedErr error + expectedVal int64 + options *Options + metadataMock func(ctrl *gomock.Controller) *metadata.MockMetadataService }{ { - name: "success normal", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().GetDeviceNameFromMount(gomock.Eq(targetPath)).Return(devicePath, 1, nil) - mockMounter.EXPECT().Unstage(gomock.Eq(targetPath)).Return(nil) - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - VolumeId: volumeID, - } - - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "VolumeAttachLimit_specified", + options: &Options{ + VolumeAttachLimit: 10, + ReservedVolumeAttachments: -1, }, + expectedVal: 10, }, { - name: "success no device mounted at target", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().GetDeviceNameFromMount(gomock.Eq(targetPath)).Return(devicePath, 0, nil) - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - VolumeId: volumeID, - } - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "sbeDeviceVolumeAttachmentLimit", + options: &Options{ + VolumeAttachLimit: -1, + }, + expectedVal: sbeDeviceVolumeAttachmentLimit, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("snow") + return m }, }, { - name: "success device mounted at multiple targets", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().GetDeviceNameFromMount(gomock.Eq(targetPath)).Return(devicePath, 2, nil) - mockMounter.EXPECT().Unstage(gomock.Eq(targetPath)).Return(nil) - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - VolumeId: volumeID, - } - - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "t2.medium_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 38, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetInstanceType().Return("t2.medium") + return m }, }, { - name: "fail no VolumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - } - - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + name: "ReservedVolumeAttachments_specified", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: 3, + }, + expectedVal: 36, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("t2.medium") + return m }, }, { - name: "fail no StagingTargetPath", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnstageVolumeRequest{ - VolumeId: volumeID, - } - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + name: "m5d.large_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 23, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("m5d.large") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetNumAttachedENIs().Return(3) + return m }, }, { - name: "fail GetDeviceName returns error", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().GetDeviceNameFromMount(gomock.Eq(targetPath)).Return("", 0, errors.New("GetDeviceName faield")) - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - VolumeId: volumeID, - } - - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - expectErr(t, err, codes.Internal) + name: "d3en.12xlarge_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 1, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("d3en.12xlarge") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetNumAttachedENIs().Return(1) + return m }, }, { - name: "fail another operation in-flight on given volumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnstageVolumeRequest{ - StagingTargetPath: targetPath, - VolumeId: volumeID, - } - - awsDriver.inFlight.Insert(volumeID) - _, err := awsDriver.NodeUnstageVolume(context.TODO(), req) - expectErr(t, err, codes.Aborted) + name: "d3.8xlarge_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 1, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("d3.8xlarge") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetNumAttachedENIs().Return(1) + return m + }, + }, + { + name: "nitro_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 127, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("m7i.48xlarge") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + return m + }, + }, + { + name: "attached_max_enis", + options: &Options{ + VolumeAttachLimit: -1, + }, + expectedVal: 1, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("t3.xlarge") + m.EXPECT().GetNumAttachedENIs().Return(40) + return m + }, + }, + { + name: "inf1.24xlarge_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 9, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("inf1.24xlarge") + m.EXPECT().GetNumAttachedENIs().Return(1) + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + return m + }, + }, + { + name: "mac1.metal_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 14, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("mac1.metal") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetNumAttachedENIs().Return(1) + return m + }, + }, + { + name: "u-12tb1.metal_volume_attach_limit", + options: &Options{ + VolumeAttachLimit: -1, + ReservedVolumeAttachments: -1, + }, + expectedVal: 17, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + m.EXPECT().GetInstanceType().Return("u-12tb1.metal") + m.EXPECT().GetNumBlockDeviceMappings().Return(0) + m.EXPECT().GetNumAttachedENIs().Return(1) + return m }, }, } for _, tc := range testCases { - t.Run(tc.name, tc.testFunc) + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var mounter *mounter.MockMounter + + var metadata *metadata.MockMetadataService + if tc.metadataMock != nil { + metadata = tc.metadataMock(ctrl) + } + + driver := &NodeService{ + mounter: mounter, + inFlight: internal.NewInFlight(), + options: tc.options, + metadata: metadata, + } + + value := driver.getVolumesLimit() + if value != tc.expectedVal { + t.Fatalf("Expected value %v but got %v", tc.expectedVal, value) + } + }) } } func TestNodePublishVolume(t *testing.T) { - targetPath := "/test/path" - stagingTargetPath := "/test/staging/path" - devicePath := "/dev/fake" - deviceFileInfo := fs.FileInfo(&fakeFileInfo{devicePath, os.ModeDevice}) - stdVolCap := &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{ - Mount: &csi.VolumeCapability_MountVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, - } - stdVolContext := map[string]string{"partition": "1"} - devicePathWithPartition := devicePath + "1" testCases := []struct { - name string - testFunc func(t *testing.T) + name string + req *csi.NodePublishVolumeRequest + mounterMock func(ctrl *gomock.Controller) *mounter.MockMounter + metadataMock func(ctrl *gomock.Controller) *metadata.MockMetadataService + expectedErr error + inflight bool }{ { - name: "success normal", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(stagingTargetPath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Eq([]string{"bind"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } + name: "success_block_device", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().MakeFile(gomock.Any()).Return(nil) + m.EXPECT().IsLikelyNotMountPoint(gomock.Any()).Return(true, nil) + m.EXPECT().Mount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, }, { - name: "success filesystem mounted already", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(false, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "success_fs", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PreparePublishTarget(gomock.Any()).Return(nil) + m.EXPECT().IsLikelyNotMountPoint(gomock.Any()).Return(true, nil) + m.EXPECT().Mount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + return m }, }, { - name: "success filesystem mountpoint error", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, nil), - ) - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, errors.New("Internal system error")) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.Internal) + name: "volume_id_not_provided", + req: &csi.NodePublishVolumeRequest{ + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID not provided"), }, { - name: "success filesystem corrupted mountpoint error", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, errors.New("CorruptedMntError")) - mockMounter.EXPECT().IsCorruptedMnt(gomock.Eq(errors.New("CorruptedMntError"))).Return(true) - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, errors.New("internal system error")) - mockMounter.EXPECT().Unpublish(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(stagingTargetPath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Eq([]string{"bind"})).Return(nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "staging_target_path_not_provided", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + expectedErr: status.Error(codes.InvalidArgument, "Staging target not provided"), }, { - name: "success fstype xfs", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(stagingTargetPath), gomock.Eq(targetPath), gomock.Eq(FSTypeXfs), gomock.Eq([]string{"bind", "nouuid"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{ - Mount: &csi.VolumeCapability_MountVolume{ - FsType: FSTypeXfs, - }, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "target_path_not_provided", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + expectedErr: status.Error(codes.InvalidArgument, "Target path not provided"), }, { - name: "success readonly", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(stagingTargetPath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Eq([]string{"bind", "ro"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - Readonly: true, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "capability_not_provided", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + expectedErr: status.Error(codes.InvalidArgument, "Volume capability not provided"), }, { - name: "success mount options", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(stagingTargetPath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Eq([]string{"bind", "test-flag"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Mount{ - Mount: &csi.VolumeCapability_MountVolume{ - // this request will call mount with the bind option, - // adding "bind" here we test that we don't add the - // same option twice. "test-flag" is a canary to check - // that the driver calls mount with that flag - MountFlags: []string{"bind", "test-flag"}, - }, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "success_block_device", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + inflight: true, + expectedErr: status.Errorf(codes.Aborted, VolumeOperationAlreadyExists, "vol-test"), }, { - name: "success normal [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - ) - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(""), gomock.Eq([]string{"bind"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "success_block_device", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_Mode(csi.ControllerServiceCapability_RPC_UNKNOWN), + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, }, + expectedErr: status.Errorf(codes.InvalidArgument, "Volume capability not supported"), }, { - name: "success mounted already [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - ) - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(false, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "read_only_enabled", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + Readonly: true, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().MakeFile(gomock.Any()).Return(nil) + m.EXPECT().IsLikelyNotMountPoint(gomock.Any()).Return(true, nil) + m.EXPECT().Mount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, }, { - name: "success mountpoint error [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, nil), - ) - - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, errors.New("Internal System Error")) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "nodePublishVolumeForBlock_error", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.Internal) + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, }, + expectedErr: status.Errorf(codes.InvalidArgument, "Device path not provided"), }, { - name: "success corrupted mountpoint error [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - mockMounter.EXPECT().PathExists(gomock.Eq(targetPath)).Return(true, errors.New("CorruptedMntError")), - ) - - mockMounter.EXPECT().IsCorruptedMnt(errors.New("CorruptedMntError")).Return(true) - - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().Unpublish(gomock.Eq(targetPath)).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, errors.New("Internal System Error")) - mockMounter.EXPECT().Mount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Any(), gomock.Any()).Return(nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "nodePublishVolumeForBlock_invalid_volume_attribute", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeId: volumeID, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + VolumeContext: map[string]string{ + VolumeAttributePartition: "invalid-partition", + }, }, + expectedErr: status.Error(codes.InvalidArgument, "Volume Attribute is invalid"), }, { - name: "success normal with partition [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - ) - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(devicePathWithPartition), gomock.Eq(targetPath), gomock.Eq(""), gomock.Eq([]string{"bind"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "nodePublishVolumeForBlock_invalid_partition", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeContext: stdVolContext, - VolumeId: volumeID, - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + VolumeContext: map[string]string{ + VolumeAttributePartition: "0", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().MakeFile(gomock.Any()).Return(nil) + m.EXPECT().IsLikelyNotMountPoint(gomock.Any()).Return(true, nil) + m.EXPECT().Mount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, }, { - name: "success normal with invalid partition config, will ignore the config [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - gomock.InOrder( - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(true, nil), - mockMounter.EXPECT().PathExists(gomock.Eq("/test")).Return(false, nil), - ) - mockMounter.EXPECT().MakeDir(gomock.Eq("/test")).Return(nil) - mockMounter.EXPECT().MakeFile(targetPath).Return(nil) - mockMounter.EXPECT().Mount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(""), gomock.Eq([]string{"bind"})).Return(nil) - mockMounter.EXPECT().IsLikelyNotMountPoint(gomock.Eq(targetPath)).Return(true, nil) - mockDeviceIdentifier.EXPECT().Lstat(gomock.Eq(devicePath)).Return(deviceFileInfo, nil) - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "nodePublishVolumeForBlock_valid_partition", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeContext: map[string]string{VolumeAttributePartition: "0"}, - VolumeId: volumeID, - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + VolumeContext: map[string]string{ + VolumeAttributePartition: "1", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("/dev/xvdba", nil) + m.EXPECT().PathExists(gomock.Any()).Return(true, nil) + m.EXPECT().MakeFile(gomock.Any()).Return(nil) + m.EXPECT().IsLikelyNotMountPoint(gomock.Any()).Return(true, nil) + m.EXPECT().Mount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, }, { - name: "Fail invalid volumeContext config [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, + name: "nodePublishVolumeForBlock_device_path_failure", + req: &csi.NodePublishVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + TargetPath: "/target/path", + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Block{ + Block: &csi.VolumeCapability_BlockVolume{}, }, - VolumeContext: map[string]string{VolumeAttributePartition: "partition1"}, - VolumeId: volumeID, - } + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + }, + PublishContext: map[string]string{ + DevicePathKey: "/dev/xvdba", + }, + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + m.EXPECT().FindDevicePath(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("", errors.New("device path error")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m }, + expectedErr: status.Error(codes.Internal, "Failed to find device path /dev/xvdba. device path error"), }, - { - name: "fail no device path [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl) + } - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } + var metadata *metadata.MockMetadataService + if tc.metadataMock != nil { + metadata = tc.metadataMock(ctrl) + } - req := &csi.NodePublishVolumeRequest{ - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, - }, - VolumeId: volumeID, - } + driver := &NodeService{ + metadata: metadata, + mounter: mounter, + inFlight: internal.NewInFlight(), + } - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + if tc.inflight { + driver.inFlight.Insert("vol-test") + } + + _, err := driver.NodePublishVolume(context.Background(), tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr, err) + } + }) + } +} +func TestNodeUnstageVolume(t *testing.T) { + testCases := []struct { + name string + req *csi.NodeUnstageVolumeRequest + mounterMock func(ctrl *gomock.Controller) *mounter.MockMounter + expectedErr error + inflight bool + }{ + { + name: "success", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("dev-test", 1, nil) + m.EXPECT().Unstage(gomock.Any()).Return(nil) + return m }, }, { - name: "fail to find deivce path [raw block]", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() + name: "missing_volume_id", + req: &csi.NodeUnstageVolumeRequest{ + StagingTargetPath: "/staging/path", + }, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID not provided"), + }, + { + name: "missing_staging_target", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + }, + expectedErr: status.Error(codes.InvalidArgument, "Staging target not provided"), + }, + { + name: "unstage_failed", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 1, nil) + m.EXPECT().Unstage(gomock.Any()).Return(errors.New("unstage failed")) + return m + }, + expectedErr: status.Errorf(codes.Internal, "Could not unmount target %q: %v", "/staging/path", errors.New("unstage failed")), + }, + { + name: "target_not_mounted", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 0, nil) + return m + }, + }, + { + name: "get_device_name_from_mount_failed", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("", 0, errors.New("failed to get device name")) + return m + }, + expectedErr: status.Error(codes.Internal, "failed to check if target \"/staging/path\" is a mount point: failed to get device name"), + }, + { + name: "multiple_references", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().GetDeviceNameFromMount(gomock.Any()).Return("dev-test", 2, nil) + m.EXPECT().Unstage(gomock.Any()).Return(nil) + return m + }, + }, + { + name: "operation_already_exists", + req: &csi.NodeUnstageVolumeRequest{ + VolumeId: "vol-test", + StagingTargetPath: "/staging/path", + }, + expectedErr: status.Error(codes.Aborted, "An operation with the given volume=\"vol-test\" is already in progress"), + inflight: true, + }, + } - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl) + } - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, - }, - VolumeId: volumeID, - } + driver := &NodeService{ + mounter: mounter, + inFlight: internal.NewInFlight(), + } - mockMounter.EXPECT().PathExists(gomock.Eq(devicePath)).Return(false, errors.New("findDevicePath failed")) + if tc.inflight { + driver.inFlight.Insert("vol-test") + } - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.Internal) + _, err := driver.NodeUnstageVolume(context.Background(), tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr, err) + } + }) + } +} +func TestNodeGetCapabilities(t *testing.T) { + req := &csi.NodeGetCapabilitiesRequest{} + expectedCaps := []*csi.NodeServiceCapability{ + { + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, + }, }, }, { - name: "fail no VolumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) - + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, + }, }, }, { - name: "fail no StagingTargetPath", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_GET_VOLUME_STATS, + }, + }, + }, + } - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) + driver := &NodeService{} - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } + resp, err := driver.NodeGetCapabilities(context.Background(), req) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - TargetPath: targetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } + if len(resp.GetCapabilities()) != len(expectedCaps) { + t.Fatalf("Expected %d capabilities, but got %d", len(expectedCaps), len(resp.GetCapabilities())) + } - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + for i, cap := range resp.GetCapabilities() { + if cap.GetRpc().GetType() != expectedCaps[i].GetRpc().GetType() { + t.Fatalf("Expected capability %v, but got %v", expectedCaps[i].GetRpc().GetType(), cap.GetRpc().GetType()) + } + } +} +func TestNodeGetInfo(t *testing.T) { + testCases := []struct { + name string + metadataMock func(ctrl *gomock.Controller) *metadata.MockMetadataService + expectedResp *csi.NodeGetInfoResponse + }{ + { + name: "without_outpost_arn", + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetInstanceID().Return("i-1234567890abcdef0") + m.EXPECT().GetAvailabilityZone().Return("us-west-2a") + m.EXPECT().GetOutpostArn().Return(arn.ARN{}) + return m + }, + expectedResp: &csi.NodeGetInfoResponse{ + NodeId: "i-1234567890abcdef0", + AccessibleTopology: &csi.Topology{ + Segments: map[string]string{ + ZoneTopologyKey: "us-west-2a", + WellKnownZoneTopologyKey: "us-west-2a", + OSTopologyKey: runtime.GOOS, + }, + }, }, }, { - name: "fail no TargetPath", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() + name: "with_outpost_arn", + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetInstanceID().Return("i-1234567890abcdef0") + m.EXPECT().GetAvailabilityZone().Return("us-west-2a") + m.EXPECT().GetOutpostArn().Return(arn.ARN{ + Partition: "aws", + Service: "outposts", + Region: "us-west-2", + AccountID: "123456789012", + Resource: "op-1234567890abcdef0", + }) + return m + }, + expectedResp: &csi.NodeGetInfoResponse{ + NodeId: "i-1234567890abcdef0", + AccessibleTopology: &csi.Topology{ + Segments: map[string]string{ + ZoneTopologyKey: "us-west-2a", + WellKnownZoneTopologyKey: "us-west-2a", + OSTopologyKey: runtime.GOOS, + AwsRegionKey: "us-west-2", + AwsPartitionKey: "aws", + AwsAccountIDKey: "123456789012", + AwsOutpostIDKey: "op-1234567890abcdef0", + }, + }, + }, + }, + } - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } + metadataService := tc.metadataMock(ctrl) + mounter := mounter.NewMockMounter(ctrl) - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - VolumeCapability: stdVolCap, - VolumeId: volumeID, - } + driver := &NodeService{ + metadata: metadataService, + mounter: mounter, + inFlight: internal.NewInFlight(), + options: &Options{}, + } + + resp, err := driver.NodeGetInfo(context.Background(), &csi.NodeGetInfoRequest{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + if !reflect.DeepEqual(resp, tc.expectedResp) { + t.Fatalf("Expected response %+v, but got %+v", tc.expectedResp, resp) + } + }) + } +} +func TestNodeUnpublishVolume(t *testing.T) { + testCases := []struct { + name string + req *csi.NodeUnpublishVolumeRequest + mounterMock func(ctrl *gomock.Controller) *mounter.MockMounter + expectedErr error + inflight bool + }{ + { + name: "success", + req: &csi.NodeUnpublishVolumeRequest{ + VolumeId: "vol-test", + TargetPath: "/target/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().Unpublish(gomock.Eq("/target/path")).Return(nil) + return m }, }, { - name: "fail no VolumeCapability", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: devicePath}, - StagingTargetPath: stagingTargetPath, - TargetPath: targetPath, - VolumeId: volumeID, - } - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) - + name: "missing_volume_id", + req: &csi.NodeUnpublishVolumeRequest{ + TargetPath: "/target/path", }, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID not provided"), }, { - name: "fail invalid VolumeCapability", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: "/test/staging/path", - TargetPath: "/test/target/path", - VolumeId: volumeID, - VolumeCapability: &csi.VolumeCapability{ - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_UNKNOWN, - }, - }, - } - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) - + name: "missing_target_path", + req: &csi.NodeUnpublishVolumeRequest{ + VolumeId: "vol-test", }, + expectedErr: status.Error(codes.InvalidArgument, "Target path not provided"), }, { - name: "fail another operation in-flight on given volumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodePublishVolumeRequest{ - PublishContext: map[string]string{DevicePathKey: "/dev/fake"}, - StagingTargetPath: "/test/staging/path", - TargetPath: "/test/target/path", - VolumeId: volumeID, - VolumeCapability: &csi.VolumeCapability{ - AccessType: &csi.VolumeCapability_Block{ - Block: &csi.VolumeCapability_BlockVolume{}, - }, - AccessMode: &csi.VolumeCapability_AccessMode{ - Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - }, - }, - } - awsDriver.inFlight.Insert(volumeID) - - _, err := awsDriver.NodePublishVolume(context.TODO(), req) - expectErr(t, err, codes.Aborted) - + name: "unpublish_failed", + req: &csi.NodeUnpublishVolumeRequest{ + VolumeId: "vol-test", + TargetPath: "/target/path", }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().Unpublish(gomock.Eq("/target/path")).Return(errors.New("unpublish failed")) + return m + }, + expectedErr: status.Errorf(codes.Internal, "Could not unmount %q: %v", "/target/path", errors.New("unpublish failed")), + }, + { + name: "operation_already_exists", + req: &csi.NodeUnpublishVolumeRequest{ + VolumeId: "vol-test", + TargetPath: "/target/path", + }, + expectedErr: status.Error(codes.Aborted, "An operation with the given volume=\"vol-test\" is already in progress"), + inflight: true, }, } for _, tc := range testCases { - t.Run(tc.name, tc.testFunc) + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl) + } + + driver := &NodeService{ + mounter: mounter, + inFlight: internal.NewInFlight(), + } + + if tc.inflight { + driver.inFlight.Insert("vol-test") + } + + _, err := driver.NodeUnpublishVolume(context.Background(), tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr, err) + } + }) } } -func TestNodeExpandVolume(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - tests := []struct { - name string - request csi.NodeExpandVolumeRequest - expectResponseCode codes.Code +func TestNodeExpandVolume(t *testing.T) { + testCases := []struct { + name string + req *csi.NodeExpandVolumeRequest + mounterMock func(ctrl *gomock.Controller) *mounter.MockMounter + metadataMock func(ctrl *gomock.Controller) *metadata.MockMetadataService + expectedResp *csi.NodeExpandVolumeResponse + expectedErr error }{ { - name: "fail missing volumeId", - request: csi.NodeExpandVolumeRequest{}, - expectResponseCode: codes.InvalidArgument, + name: "success", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/volume/path")).Return("device-name", 1, nil) + m.EXPECT().FindDevicePath(gomock.Eq("device-name"), gomock.Eq("vol-test"), gomock.Eq(""), gomock.Eq("us-west-2")).Return("/dev/xvdba", nil) + m.EXPECT().Resize(gomock.Eq("/dev/xvdba"), gomock.Eq("/volume/path")).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(gomock.Eq("/dev/xvdba")).Return(int64(1000), nil) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedResp: &csi.NodeExpandVolumeResponse{CapacityBytes: int64(1000)}, }, { - name: "fail missing volumePath", - request: csi.NodeExpandVolumeRequest{ - StagingTargetPath: "/testDevice/Path", - VolumeId: "test-volume-id", + name: "missing_volume_id", + req: &csi.NodeExpandVolumeRequest{ + VolumePath: "/volume/path", }, - expectResponseCode: codes.InvalidArgument, + expectedErr: status.Error(codes.InvalidArgument, "Volume ID not provided"), }, { - name: "fail volume path not exist", - request: csi.NodeExpandVolumeRequest{ - VolumePath: "./test", - VolumeId: "test-volume-id", + name: "missing_volume_path", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", }, - expectResponseCode: codes.Internal, + expectedErr: status.Error(codes.InvalidArgument, "volume path must be provided"), }, { - name: "Fail validate VolumeCapability", - request: csi.NodeExpandVolumeRequest{ - VolumePath: "./test", - VolumeId: "test-volume-id", + name: "invalid_volume_capability", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Block{ Block: &csi.VolumeCapability_BlockVolume{}, @@ -1653,13 +2003,13 @@ func TestNodeExpandVolume(t *testing.T) { }, }, }, - expectResponseCode: codes.InvalidArgument, + expectedErr: status.Error(codes.InvalidArgument, "VolumeCapability is invalid: block:<> access_mode:<> "), }, { - name: "Success [VolumeCapability is block]", - request: csi.NodeExpandVolumeRequest{ - VolumePath: "./test", - VolumeId: "test-volume-id", + name: "block_device", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", VolumeCapability: &csi.VolumeCapability{ AccessType: &csi.VolumeCapability_Block{ Block: &csi.VolumeCapability_BlockVolume{}, @@ -1669,697 +2019,308 @@ func TestNodeExpandVolume(t *testing.T) { }, }, }, - expectResponseCode: codes.OK, + expectedResp: &csi.NodeExpandVolumeResponse{}, }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := awsDriver.NodeExpandVolume(context.Background(), &test.request) - if err != nil { - if test.expectResponseCode != codes.OK { - expectErr(t, err, test.expectResponseCode) - } else { - t.Fatalf("Expect no error but got: %v", err) - } - } - }) - } -} - -func TestNodeUnpublishVolume(t *testing.T) { - targetPath := "/test/path" - - testCases := []struct { - name string - testFunc func(t *testing.T) - }{ { - name: "success normal", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnpublishVolumeRequest{ - TargetPath: targetPath, - VolumeId: volumeID, - } - - mockMounter.EXPECT().Unpublish(gomock.Eq(targetPath)).Return(nil) - _, err := awsDriver.NodeUnpublishVolume(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "is_block_device_error", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, errors.New("failed to determine if block device")) + return m }, + expectedErr: status.Error(codes.Internal, "failed to determine if volumePath [/volume/path] is a block device: failed to determine if block device"), }, { - name: "fail no VolumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnpublishVolumeRequest{ - TargetPath: targetPath, - } - - _, err := awsDriver.NodeUnpublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + name: "get_block_size_bytes_error", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(gomock.Eq("/volume/path")).Return(int64(0), errors.New("failed to get block size")) + return m + }, + expectedErr: status.Error(codes.Internal, "failed to get block capacity on path /volume/path: failed to get block size"), }, { - name: "fail no TargetPath", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnpublishVolumeRequest{ - VolumeId: volumeID, - } - - _, err := awsDriver.NodeUnpublishVolume(context.TODO(), req) - expectErr(t, err, codes.InvalidArgument) + name: "block_device_success", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(gomock.Eq("/volume/path")).Return(int64(1000), nil) + return m }, + expectedResp: &csi.NodeExpandVolumeResponse{CapacityBytes: int64(1000)}, }, { - name: "fail error on unpublish", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnpublishVolumeRequest{ - TargetPath: targetPath, - VolumeId: volumeID, - } - - mockMounter.EXPECT().Unpublish(gomock.Eq(targetPath)).Return(errors.New("test Unpublish error")) - _, err := awsDriver.NodeUnpublishVolume(context.TODO(), req) - expectErr(t, err, codes.Internal) + name: "get_device_name_error", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/volume/path")).Return("", 0, errors.New("failed to get device name")) + return m + }, + metadataMock: nil, + expectedResp: nil, + expectedErr: status.Error(codes.Internal, "failed to get device name from mount /volume/path: failed to get device name"), }, { - name: "fail another operation in-flight on given volumeId", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeUnpublishVolumeRequest{ - TargetPath: targetPath, - VolumeId: volumeID, - } - - awsDriver.inFlight.Insert(volumeID) - _, err := awsDriver.NodeUnpublishVolume(context.TODO(), req) - expectErr(t, err, codes.Aborted) + name: "find_device_path_error", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/volume/path")).Return("device-name", 1, nil) + m.EXPECT().FindDevicePath(gomock.Eq("device-name"), gomock.Eq("vol-test"), gomock.Eq(""), gomock.Eq("us-west-2")).Return("", errors.New("failed to find device path")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedResp: nil, + expectedErr: status.Error(codes.Internal, "failed to find device path for device name device-name for mount /volume/path: failed to find device path"), + }, + { + name: "resize_error", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", + }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/volume/path")).Return("device-name", 1, nil) + m.EXPECT().FindDevicePath(gomock.Eq("device-name"), gomock.Eq("vol-test"), gomock.Eq(""), gomock.Eq("us-west-2")).Return("/dev/xvdba", nil) + m.EXPECT().Resize(gomock.Eq("/dev/xvdba"), gomock.Eq("/volume/path")).Return(false, errors.New("failed to resize volume")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedResp: nil, + expectedErr: status.Error(codes.Internal, "Could not resize volume \"vol-test\" (\"/dev/xvdba\"): failed to resize volume"), + }, + { + name: "get_block_size_bytes_error_after_resize", + req: &csi.NodeExpandVolumeRequest{ + VolumeId: "vol-test", + VolumePath: "/volume/path", }, + mounterMock: func(ctrl *gomock.Controller) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().IsBlockDevice(gomock.Eq("/volume/path")).Return(false, nil) + m.EXPECT().GetDeviceNameFromMount(gomock.Eq("/volume/path")).Return("device-name", 1, nil) + m.EXPECT().FindDevicePath(gomock.Eq("device-name"), gomock.Eq("vol-test"), gomock.Eq(""), gomock.Eq("us-west-2")).Return("/dev/xvdba", nil) + m.EXPECT().Resize(gomock.Eq("/dev/xvdba"), gomock.Eq("/volume/path")).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(gomock.Eq("/dev/xvdba")).Return(int64(0), errors.New("failed to get block size")) + return m + }, + metadataMock: func(ctrl *gomock.Controller) *metadata.MockMetadataService { + m := metadata.NewMockMetadataService(ctrl) + m.EXPECT().GetRegion().Return("us-west-2") + return m + }, + expectedResp: nil, + expectedErr: status.Error(codes.Internal, "failed to get block capacity on path /volume/path: failed to get block size"), }, } for _, tc := range testCases { - t.Run(tc.name, tc.testFunc) + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl) + } + + var metadata *metadata.MockMetadataService + if tc.metadataMock != nil { + metadata = tc.metadataMock(ctrl) + } + + driver := &NodeService{ + mounter: mounter, + metadata: metadata, + } + + resp, err := driver.NodeExpandVolume(context.Background(), tc.req) + if !reflect.DeepEqual(err, tc.expectedErr) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr, err) + } + + if !reflect.DeepEqual(resp, tc.expectedResp) { + t.Fatalf("Expected response '%v' but got '%v'", tc.expectedResp, resp) + } + }) } } func TestNodeGetVolumeStats(t *testing.T) { testCases := []struct { - name string - testFunc func(t *testing.T) + name string + validVolId bool + validPath bool + metricsStatErr bool + mounterMock func(mockCtl *gomock.Controller, dir string) *mounter.MockMounter + expectedErr func(dir string) error }{ { - name: "success normal", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - VolumePath := "./test" - err := os.MkdirAll(VolumePath, 0644) - if err != nil { - t.Fatalf("fail to create dir: %v", err) - } - defer os.RemoveAll(VolumePath) - - mockMounter.EXPECT().PathExists(VolumePath).Return(true, nil) - - awsDriver := nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeGetVolumeStatsRequest{ - VolumeId: volumeID, - VolumePath: VolumePath, - } - _, err = awsDriver.NodeGetVolumeStats(context.TODO(), req) - if err != nil { - t.Fatalf("Expect no error but got: %v", err) - } + name: "success normal", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(true, nil) + m.EXPECT().IsBlockDevice(gomock.Eq(dir)).Return(false, nil) + return m + }, + expectedErr: func(dir string) error { + return nil }, }, { - name: "fail path not exist", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - VolumePath := "/test" - - mockMounter.EXPECT().PathExists(VolumePath).Return(false, nil) - - awsDriver := nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeGetVolumeStatsRequest{ - VolumeId: volumeID, - VolumePath: VolumePath, - } - _, err := awsDriver.NodeGetVolumeStats(context.TODO(), req) - expectErr(t, err, codes.NotFound) + name: "invalid_volume_id", + validVolId: false, + expectedErr: func(dir string) error { + return status.Error(codes.InvalidArgument, "NodeGetVolumeStats volume ID was empty") }, }, { - name: "fail can't determine block device", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - VolumePath := "/test" - - mockMounter.EXPECT().PathExists(VolumePath).Return(true, nil) - - awsDriver := nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeGetVolumeStatsRequest{ - VolumeId: volumeID, - VolumePath: VolumePath, - } - _, err := awsDriver.NodeGetVolumeStats(context.TODO(), req) - expectErr(t, err, codes.Internal) + name: "invalid_volume_path", + validVolId: true, + validPath: false, + expectedErr: func(dir string) error { + return status.Error(codes.InvalidArgument, "NodeGetVolumeStats volume path was empty") }, }, { - name: "fail error calling existsPath", - testFunc: func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - VolumePath := "/test" - - mockMounter.EXPECT().PathExists(VolumePath).Return(false, errors.New("get existsPath call fail")) - - awsDriver := nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - req := &csi.NodeGetVolumeStatsRequest{ - VolumeId: volumeID, - VolumePath: VolumePath, - } - _, err := awsDriver.NodeGetVolumeStats(context.TODO(), req) - expectErr(t, err, codes.Internal) + name: "path_exists_error", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(false, errors.New("path exists error")) + return m + }, + expectedErr: func(dir string) error { + return status.Errorf(codes.Internal, "unknown error when stat on %s: path exists error", dir) }, }, - } - - for _, tc := range testCases { - t.Run(tc.name, tc.testFunc) - } - -} - -func TestNodeGetCapabilities(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - awsDriver := nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - } - - caps := []*csi.NodeServiceCapability{ { - Type: &csi.NodeServiceCapability_Rpc{ - Rpc: &csi.NodeServiceCapability_RPC{ - Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, - }, + name: "path_does_not_exist", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(false, nil) + return m + }, + expectedErr: func(dir string) error { + return status.Errorf(codes.NotFound, "path %s does not exist", dir) }, }, { - Type: &csi.NodeServiceCapability_Rpc{ - Rpc: &csi.NodeServiceCapability_RPC{ - Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, - }, + name: "is_block_device_error", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(true, nil) + m.EXPECT().IsBlockDevice(gomock.Eq(dir)).Return(false, errors.New("is block device error")) + return m + }, + expectedErr: func(dir string) error { + return status.Errorf(codes.Internal, "failed to determine whether %s is block device: is block device error", dir) }, }, { - Type: &csi.NodeServiceCapability_Rpc{ - Rpc: &csi.NodeServiceCapability_RPC{ - Type: csi.NodeServiceCapability_RPC_GET_VOLUME_STATS, - }, + name: "get_block_size_bytes_error", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(true, nil) + m.EXPECT().IsBlockDevice(gomock.Eq(dir)).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(dir).Return(int64(0), errors.New("get block size bytes error")) + return m + }, + expectedErr: func(dir string) error { + return status.Errorf(codes.Internal, "failed to get block capacity on path %s: %v", dir, "get block size bytes error") }, }, - } - expResp := &csi.NodeGetCapabilitiesResponse{Capabilities: caps} - - req := &csi.NodeGetCapabilitiesRequest{} - resp, err := awsDriver.NodeGetCapabilities(context.TODO(), req) - if err != nil { - srvErr, ok := status.FromError(err) - if !ok { - t.Fatalf("Could not get error status code from error: %v", srvErr) - } - t.Fatalf("Expected nil error, got %d message %s", srvErr.Code(), srvErr.Message()) - } - if !reflect.DeepEqual(expResp, resp) { - t.Fatalf("Expected response {%+v}, got {%+v}", expResp, resp) - } -} - -func TestNodeGetInfo(t *testing.T) { - validOutpostArn, _ := arn.Parse(strings.ReplaceAll("arn:aws:outposts:us-west-2:111111111111:outpost/op-0aaa000a0aaaa00a0", "outpost/", "")) - emptyOutpostArn := arn.ARN{} - testCases := []struct { - name string - instanceID string - instanceType string - availabilityZone string - region string - attachedENIs int - blockDevices int - volumeAttachLimit int64 - reservedVolumeAttachments int - expMaxVolumes int64 - outpostArn arn.ARN - }{ { - name: "non-nitro instance success normal", - instanceID: "i-123456789abcdef01", - instanceType: "t2.medium", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - expMaxVolumes: 38, - attachedENIs: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "success normal with overwrite", - instanceID: "i-123456789abcdef01", - instanceType: "t2.medium", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: 42, - reservedVolumeAttachments: -1, - expMaxVolumes: 42, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance success normal", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 2, - expMaxVolumes: 25, // 28 (max) - 2 (enis) - 1 (root) - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance success normal with NVMe", - instanceID: "i-123456789abcdef01", - instanceType: "m5d.large", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 2, - expMaxVolumes: 24, - outpostArn: emptyOutpostArn, - }, - { - name: "success normal with NVMe and overwrite", - instanceID: "i-123456789abcdef01", - instanceType: "m5d.large", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: 30, - reservedVolumeAttachments: -1, - expMaxVolumes: 30, - outpostArn: emptyOutpostArn, - }, - { - name: "success normal outposts", - instanceID: "i-123456789abcdef01", - instanceType: "m5d.large", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: 30, - reservedVolumeAttachments: -1, - expMaxVolumes: 30, - outpostArn: validOutpostArn, - }, - { - name: "baremetal instances max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "c6i.metal", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 26, // 28 (max) - 1 (eni) - 1 (root) - outpostArn: emptyOutpostArn, - }, - { - name: "high memory baremetal instances max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "u-12tb1.metal", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 17, - outpostArn: emptyOutpostArn, - }, - { - name: "mac instances max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "mac1.metal", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 14, - outpostArn: emptyOutpostArn, - }, - { - name: "inf1.24xlarge instace max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "inf1.24xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 9, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instances already attached EBS volumes", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - blockDevices: 2, - expMaxVolumes: 24, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance already attached max EBS volumes", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - blockDevices: 27, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance already attached max EBS volumes ignoring mapped volumes in metadata", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: 1, - attachedENIs: 1, - blockDevices: 27, - expMaxVolumes: 26, - outpostArn: emptyOutpostArn, - }, - { - name: "non-nitro instance already attached max EBS volumes", - instanceID: "i-123456789abcdef01", - instanceType: "m5.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - blockDevices: 39, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "non-nitro instance ignoring mapped volumes in metadata", - instanceID: "i-123456789abcdef01", - instanceType: "m5.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: 1, - attachedENIs: 10, - blockDevices: 39, - expMaxVolumes: 17, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance already attached max ENIs", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 27, - blockDevices: 1, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance already attached max ENIs ignoring mapped volumes in metadata", - instanceID: "i-123456789abcdef01", - instanceType: "t3.xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: 1, - attachedENIs: 27, - blockDevices: 1, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "nitro instance with dedicated limit", - instanceID: "i-123456789abcdef01", - instanceType: "m7i.48xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 2, - expMaxVolumes: 127, // 128 (max) - 1 (root) - outpostArn: emptyOutpostArn, - }, - { - name: "d3.8xlarge instance max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "d3.8xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, - }, - { - name: "d3en.12xlarge instance max EBS attachment limit", - instanceID: "i-123456789abcdef01", - instanceType: "d3en.12xlarge", - availabilityZone: "us-west-2b", - region: "us-west-2", - volumeAttachLimit: -1, - reservedVolumeAttachments: -1, - attachedENIs: 1, - expMaxVolumes: 1, - outpostArn: emptyOutpostArn, + name: "success block device", + validVolId: true, + validPath: true, + mounterMock: func(ctrl *gomock.Controller, dir string) *mounter.MockMounter { + m := mounter.NewMockMounter(ctrl) + m.EXPECT().PathExists(dir).Return(true, nil) + m.EXPECT().IsBlockDevice(gomock.Eq(dir)).Return(true, nil) + m.EXPECT().GetBlockSizeBytes(dir).Return(int64(1024), nil) + return m + }, + expectedErr: func(dir string) error { + return nil + }, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - options := &Options{ - VolumeAttachLimit: tc.volumeAttachLimit, - ReservedVolumeAttachments: tc.reservedVolumeAttachments, - } - - mockMounter := NewMockMounter(mockCtl) - mockDeviceIdentifier := NewMockDeviceIdentifier(mockCtl) - - mockMetadata := metadata.NewMockMetadataService(mockCtl) - mockMetadata.EXPECT().GetInstanceID().Return(tc.instanceID) - mockMetadata.EXPECT().GetAvailabilityZone().Return(tc.availabilityZone) - mockMetadata.EXPECT().GetOutpostArn().Return(tc.outpostArn) - mockMetadata.EXPECT().GetRegion().Return(tc.region).AnyTimes() - - if tc.volumeAttachLimit < 0 { - mockMetadata.EXPECT().GetInstanceType().Return(tc.instanceType) - if tc.reservedVolumeAttachments == -1 { - mockMetadata.EXPECT().GetNumBlockDeviceMappings().Return(tc.blockDevices) - } - if cloud.IsNitroInstanceType(tc.instanceType) && cloud.GetDedicatedLimitForInstanceType(tc.instanceType) == 0 { - mockMetadata.EXPECT().GetNumAttachedENIs().Return(tc.attachedENIs) - } - } - - awsDriver := &nodeService{ - metadata: mockMetadata, - mounter: mockMounter, - deviceIdentifier: mockDeviceIdentifier, - inFlight: internal.NewInFlight(), - options: options, - } + ctrl := gomock.NewController(t) + defer ctrl.Finish() - resp, err := awsDriver.NodeGetInfo(context.TODO(), &csi.NodeGetInfoRequest{}) - if err != nil { - srvErr, ok := status.FromError(err) - if !ok { - t.Fatalf("Could not get error status code from error: %v", srvErr) - } - t.Fatalf("Expected nil error, got %d message %s", srvErr.Code(), srvErr.Message()) - } + dir := t.TempDir() - if resp.GetNodeId() != tc.instanceID { - t.Fatalf("Expected node ID %q, got %q", tc.instanceID, resp.GetNodeId()) + var mounter *mounter.MockMounter + if tc.mounterMock != nil { + mounter = tc.mounterMock(ctrl, dir) } - at := resp.GetAccessibleTopology() - if at.GetSegments()[ZoneTopologyKey] != tc.availabilityZone { - t.Fatalf("Expected topology %q, got %q", tc.availabilityZone, at.GetSegments()[ZoneTopologyKey]) - } - if at.GetSegments()[WellKnownZoneTopologyKey] != tc.availabilityZone { - t.Fatalf("Expected (well-known) topology %q, got %q", tc.availabilityZone, at.GetSegments()[WellKnownZoneTopologyKey]) - } - if at.GetSegments()[OSTopologyKey] != runtime.GOOS { - t.Fatalf("Expected os topology %q, got %q", runtime.GOOS, at.GetSegments()[OSTopologyKey]) + var metadata *metadata.MockMetadataService + driver := &NodeService{ + mounter: mounter, + metadata: metadata, } - if at.GetSegments()[AwsAccountIDKey] != tc.outpostArn.AccountID { - t.Fatalf("Expected AwsAccountId %q, got %q", tc.outpostArn.AccountID, at.GetSegments()[AwsAccountIDKey]) + req := &csi.NodeGetVolumeStatsRequest{} + if tc.validVolId { + req.VolumeId = "vol-test" } - - if at.GetSegments()[AwsRegionKey] != tc.outpostArn.Region { - t.Fatalf("Expected AwsRegion %q, got %q", tc.outpostArn.Region, at.GetSegments()[AwsRegionKey]) + if tc.validPath { + req.VolumePath = dir } - - if at.GetSegments()[AwsOutpostIDKey] != tc.outpostArn.Resource { - t.Fatalf("Expected AwsOutpostID %q, got %q", tc.outpostArn.Resource, at.GetSegments()[AwsOutpostIDKey]) + if tc.metricsStatErr { + req.VolumePath = "fake-path" } - if at.GetSegments()[AwsPartitionKey] != tc.outpostArn.Partition { - t.Fatalf("Expected AwsPartition %q, got %q", tc.outpostArn.Partition, at.GetSegments()[AwsPartitionKey]) - } + _, err := driver.NodeGetVolumeStats(context.TODO(), req) - if resp.GetMaxVolumesPerNode() != tc.expMaxVolumes { - t.Fatalf("Expected %d max volumes per node, got %d", tc.expMaxVolumes, resp.GetMaxVolumesPerNode()) + if !reflect.DeepEqual(err, tc.expectedErr(dir)) { + t.Fatalf("Expected error '%v' but got '%v'", tc.expectedErr(dir), err) } }) } @@ -2372,26 +2333,6 @@ func TestRemoveNotReadyTaint(t *testing.T) { setup func(t *testing.T, mockCtl *gomock.Controller) func() (kubernetes.Interface, error) expResult error }{ - { - name: "missing CSI_NODE_NAME", - setup: func(t *testing.T, mockCtl *gomock.Controller) func() (kubernetes.Interface, error) { - return func() (kubernetes.Interface, error) { - t.Fatalf("Unexpected call to k8s client getter") - return nil, nil - } - }, - expResult: nil, - }, - { - name: "failed to setup k8s client", - setup: func(t *testing.T, mockCtl *gomock.Controller) func() (kubernetes.Interface, error) { - t.Setenv("CSI_NODE_NAME", nodeName) - return func() (kubernetes.Interface, error) { - return nil, fmt.Errorf("Failed setup!") - } - }, - expResult: nil, - }, { name: "failed to get node", setup: func(t *testing.T, mockCtl *gomock.Controller) func() (kubernetes.Interface, error) { @@ -2694,7 +2635,11 @@ func TestRemoveNotReadyTaint(t *testing.T) { defer mockCtl.Finish() k8sClientGetter := tc.setup(t, mockCtl) - result := removeNotReadyTaint(k8sClientGetter) + client, err := k8sClientGetter() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + result := removeNotReadyTaint(client) if (result == nil) != (tc.expResult == nil) { t.Fatalf("expected %v, got %v", tc.expResult, result) @@ -2709,18 +2654,32 @@ func TestRemoveNotReadyTaint(t *testing.T) { } func TestRemoveTaintInBackground(t *testing.T) { - mockRemovalCount := 0 - mockRemovalFunc := func(_ metadata.KubernetesAPIClient) error { - mockRemovalCount += 1 - if mockRemovalCount == 3 { - return nil - } else { + t.Run("Successful taint removal", func(t *testing.T) { + mockRemovalCount := 0 + mockRemovalFunc := func(_ kubernetes.Interface) error { + mockRemovalCount += 1 + if mockRemovalCount == 3 { + return nil + } else { + return fmt.Errorf("Taint removal failed!") + } + } + removeTaintInBackground(nil, taintRemovalBackoff, mockRemovalFunc) + assert.Equal(t, 3, mockRemovalCount) + }) + + t.Run("Retries exhausted", func(t *testing.T) { + mockRemovalCount := 0 + mockRemovalFunc := func(_ kubernetes.Interface) error { + mockRemovalCount += 1 return fmt.Errorf("Taint removal failed!") } - } - - removeTaintInBackground(nil, mockRemovalFunc) - assert.Equal(t, 3, mockRemovalCount) + removeTaintInBackground(nil, wait.Backoff{ + Steps: 5, + Duration: 1 * time.Millisecond, + }, mockRemovalFunc) + assert.Equal(t, 5, mockRemovalCount) + }) } func getNodeMock(mockCtl *gomock.Controller, nodeName string, returnNode *corev1.Node, returnError error) (kubernetes.Interface, *MockNodeInterface) { @@ -2734,18 +2693,3 @@ func getNodeMock(mockCtl *gomock.Controller, nodeName string, returnNode *corev1 return mockClient, mockNode } - -func expectErr(t *testing.T, actualErr error, expectedCode codes.Code) { - if actualErr == nil { - t.Fatalf("Expect error but got no error") - } - - status, ok := status.FromError(actualErr) - if !ok { - t.Fatalf("Failed to get error status code from error: %v", actualErr) - } - - if status.Code() != expectedCode { - t.Fatalf("Expected error code %d, got %d message %s", codes.InvalidArgument, status.Code(), status.Message()) - } -} diff --git a/pkg/driver/node_windows.go b/pkg/driver/node_windows.go deleted file mode 100644 index 9e31a8c643..0000000000 --- a/pkg/driver/node_windows.go +++ /dev/null @@ -1,109 +0,0 @@ -//go:build windows -// +build windows - -/* -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 driver - -import ( - "context" - "fmt" - "strconv" - "strings" - - diskapi "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1" - diskclient "github.com/kubernetes-csi/csi-proxy/client/groups/disk/v1" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" - "k8s.io/klog/v2" -) - -// findDevicePath finds disk number of device -// https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-volumes.html#list-nvme-powershell -func (d *nodeService) findDevicePath(devicePath, volumeID, _ string) (string, error) { - diskClient, err := diskclient.NewClient() - if err != nil { - return "", fmt.Errorf("error creating csi-proxy disk client: %q", err) - } - defer diskClient.Close() - - response, err := diskClient.ListDiskIDs(context.TODO(), &diskapi.ListDiskIDsRequest{}) - if err != nil { - return "", fmt.Errorf("error listing disk ids: %q", err) - } - - diskIDs := response.GetDiskIDs() - - foundDiskNumber := "" - for diskNumber, diskID := range diskIDs { - serialNumber := diskID.GetSerialNumber() - cleanVolumeID := strings.ReplaceAll(volumeID, "-", "") - if strings.Contains(serialNumber, cleanVolumeID) { - foundDiskNumber = strconv.Itoa(int(diskNumber)) - break - } - } - - if foundDiskNumber == "" { - return "", fmt.Errorf("disk number for device path %q volume id %q not found", devicePath, volumeID) - } - - return foundDiskNumber, nil -} - -func (d *nodeService) preparePublishTarget(target string) error { - // On Windows, Mount will create the parent of target and mklink (create a symbolic link) at target later, so don't create a - // directory at target now. Otherwise mklink will error: "Cannot create a file when that file already exists". - // Instead, delete the target if it already exists (like if it was created by kubelet <1.20) - // https://github.com/kubernetes/kubernetes/pull/88759 - klog.V(4).InfoS("NodePublishVolume: removing dir", "target", target) - exists, err := d.mounter.PathExists(target) - if err != nil { - return fmt.Errorf("error checking path %q exists: %v", target, err) - } - - proxyMounter, ok := (d.mounter.(*NodeMounter)).SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) - if !ok { - return fmt.Errorf("failed to cast mounter to csi proxy mounter") - } - - if exists { - if err := proxyMounter.Rmdir(target); err != nil { - return fmt.Errorf("error Rmdir target %q: %v", target, err) - } - } - return nil -} - -// IsBlockDevice checks if the given path is a block device -func (d *nodeService) IsBlockDevice(fullPath string) (bool, error) { - return false, nil -} - -// getBlockSizeBytes gets the size of the disk in bytes -func (d *nodeService) getBlockSizeBytes(devicePath string) (int64, error) { - proxyMounter, ok := (d.mounter.(*NodeMounter)).SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) - if !ok { - return -1, fmt.Errorf("failed to cast mounter to csi proxy mounter") - } - - sizeInBytes, err := proxyMounter.GetDeviceSize(devicePath) - if err != nil { - return -1, err - } - - return sizeInBytes, nil -} diff --git a/pkg/driver/mock_mount.go b/pkg/mounter/mock_mount.go similarity index 79% rename from pkg/driver/mock_mount.go rename to pkg/mounter/mock_mount.go index 22d6227416..3048cd1d2c 100644 --- a/pkg/driver/mock_mount.go +++ b/pkg/mounter/mock_mount.go @@ -1,20 +1,18 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: pkg/driver/mount.go +// Source: pkg/mounter/mount.go -// Package driver is a generated GoMock package. -package driver +// Package mounter is a generated GoMock package. +package mounter import ( - os "os" reflect "reflect" gomock "github.com/golang/mock/gomock" - mount_utils "k8s.io/mount-utils" + mount "k8s.io/mount-utils" ) // MockMounter is a mock of Mounter interface. type MockMounter struct { - mount_utils.Interface ctrl *gomock.Controller recorder *MockMounterMockRecorder } @@ -50,6 +48,21 @@ func (mr *MockMounterMockRecorder) CanSafelySkipMountPointCheck() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSafelySkipMountPointCheck", reflect.TypeOf((*MockMounter)(nil).CanSafelySkipMountPointCheck)) } +// FindDevicePath mocks base method. +func (m *MockMounter) FindDevicePath(devicePath, volumeID, partition, region string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindDevicePath", devicePath, volumeID, partition, region) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindDevicePath indicates an expected call of FindDevicePath. +func (mr *MockMounterMockRecorder) FindDevicePath(devicePath, volumeID, partition, region interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindDevicePath", reflect.TypeOf((*MockMounter)(nil).FindDevicePath), devicePath, volumeID, partition, region) +} + // FormatAndMountSensitiveWithFormatOptions mocks base method. func (m *MockMounter) FormatAndMountSensitiveWithFormatOptions(source, target, fstype string, options, sensitiveOptions, formatOptions []string) error { m.ctrl.T.Helper() @@ -64,6 +77,21 @@ func (mr *MockMounterMockRecorder) FormatAndMountSensitiveWithFormatOptions(sour return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAndMountSensitiveWithFormatOptions", reflect.TypeOf((*MockMounter)(nil).FormatAndMountSensitiveWithFormatOptions), source, target, fstype, options, sensitiveOptions, formatOptions) } +// GetBlockSizeBytes mocks base method. +func (m *MockMounter) GetBlockSizeBytes(devicePath string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockSizeBytes", devicePath) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockSizeBytes indicates an expected call of GetBlockSizeBytes. +func (mr *MockMounterMockRecorder) GetBlockSizeBytes(devicePath interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockSizeBytes", reflect.TypeOf((*MockMounter)(nil).GetBlockSizeBytes), devicePath) +} + // GetDeviceNameFromMount mocks base method. func (m *MockMounter) GetDeviceNameFromMount(mountPath string) (string, int, error) { m.ctrl.T.Helper() @@ -95,6 +123,21 @@ func (mr *MockMounterMockRecorder) GetMountRefs(pathname interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMountRefs", reflect.TypeOf((*MockMounter)(nil).GetMountRefs), pathname) } +// IsBlockDevice mocks base method. +func (m *MockMounter) IsBlockDevice(fullPath string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsBlockDevice", fullPath) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsBlockDevice indicates an expected call of IsBlockDevice. +func (mr *MockMounterMockRecorder) IsBlockDevice(fullPath interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBlockDevice", reflect.TypeOf((*MockMounter)(nil).IsBlockDevice), fullPath) +} + // IsCorruptedMnt mocks base method. func (m *MockMounter) IsCorruptedMnt(err error) bool { m.ctrl.T.Helper() @@ -140,10 +183,10 @@ func (mr *MockMounterMockRecorder) IsMountPoint(file interface{}) *gomock.Call { } // List mocks base method. -func (m *MockMounter) List() ([]mount_utils.MountPoint, error) { +func (m *MockMounter) List() ([]mount.MountPoint, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]mount_utils.MountPoint) + ret0, _ := ret[0].([]mount.MountPoint) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -253,34 +296,48 @@ func (mr *MockMounterMockRecorder) NeedResize(devicePath, deviceMountPath interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NeedResize", reflect.TypeOf((*MockMounter)(nil).NeedResize), devicePath, deviceMountPath) } -// NewResizeFs mocks base method. -func (m *MockMounter) NewResizeFs() (Resizefs, error) { +// PathExists mocks base method. +func (m *MockMounter) PathExists(path string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewResizeFs") - ret0, _ := ret[0].(Resizefs) + ret := m.ctrl.Call(m, "PathExists", path) + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// NewResizeFs indicates an expected call of NewResizeFs. -func (mr *MockMounterMockRecorder) NewResizeFs() *gomock.Call { +// PathExists indicates an expected call of PathExists. +func (mr *MockMounterMockRecorder) PathExists(path interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewResizeFs", reflect.TypeOf((*MockMounter)(nil).NewResizeFs)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PathExists", reflect.TypeOf((*MockMounter)(nil).PathExists), path) } -// PathExists mocks base method. -func (m *MockMounter) PathExists(path string) (bool, error) { +// PreparePublishTarget mocks base method. +func (m *MockMounter) PreparePublishTarget(target string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PathExists", path) + ret := m.ctrl.Call(m, "PreparePublishTarget", target) + ret0, _ := ret[0].(error) + return ret0 +} + +// PreparePublishTarget indicates an expected call of PreparePublishTarget. +func (mr *MockMounterMockRecorder) PreparePublishTarget(target interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePublishTarget", reflect.TypeOf((*MockMounter)(nil).PreparePublishTarget), target) +} + +// Resize mocks base method. +func (m *MockMounter) Resize(devicePath, deviceMountPath string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Resize", devicePath, deviceMountPath) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// PathExists indicates an expected call of PathExists. -func (mr *MockMounterMockRecorder) PathExists(path interface{}) *gomock.Call { +// Resize indicates an expected call of Resize. +func (mr *MockMounterMockRecorder) Resize(devicePath, deviceMountPath interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PathExists", reflect.TypeOf((*MockMounter)(nil).PathExists), path) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resize", reflect.TypeOf((*MockMounter)(nil).Resize), devicePath, deviceMountPath) } // Unmount mocks base method. @@ -324,94 +381,3 @@ func (mr *MockMounterMockRecorder) Unstage(path interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unstage", reflect.TypeOf((*MockMounter)(nil).Unstage), path) } - -// MockResizefs is a mock of Resizefs interface. -type MockResizefs struct { - ctrl *gomock.Controller - recorder *MockResizefsMockRecorder -} - -// MockResizefsMockRecorder is the mock recorder for MockResizefs. -type MockResizefsMockRecorder struct { - mock *MockResizefs -} - -// NewMockResizefs creates a new mock instance. -func NewMockResizefs(ctrl *gomock.Controller) *MockResizefs { - mock := &MockResizefs{ctrl: ctrl} - mock.recorder = &MockResizefsMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockResizefs) EXPECT() *MockResizefsMockRecorder { - return m.recorder -} - -// Resize mocks base method. -func (m *MockResizefs) Resize(devicePath, deviceMountPath string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Resize", devicePath, deviceMountPath) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Resize indicates an expected call of Resize. -func (mr *MockResizefsMockRecorder) Resize(devicePath, deviceMountPath interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resize", reflect.TypeOf((*MockResizefs)(nil).Resize), devicePath, deviceMountPath) -} - -// MockDeviceIdentifier is a mock of DeviceIdentifier interface. -type MockDeviceIdentifier struct { - ctrl *gomock.Controller - recorder *MockDeviceIdentifierMockRecorder -} - -// MockDeviceIdentifierMockRecorder is the mock recorder for MockDeviceIdentifier. -type MockDeviceIdentifierMockRecorder struct { - mock *MockDeviceIdentifier -} - -// NewMockDeviceIdentifier creates a new mock instance. -func NewMockDeviceIdentifier(ctrl *gomock.Controller) *MockDeviceIdentifier { - mock := &MockDeviceIdentifier{ctrl: ctrl} - mock.recorder = &MockDeviceIdentifierMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDeviceIdentifier) EXPECT() *MockDeviceIdentifierMockRecorder { - return m.recorder -} - -// EvalSymlinks mocks base method. -func (m *MockDeviceIdentifier) EvalSymlinks(path string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EvalSymlinks", path) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// EvalSymlinks indicates an expected call of EvalSymlinks. -func (mr *MockDeviceIdentifierMockRecorder) EvalSymlinks(path interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalSymlinks", reflect.TypeOf((*MockDeviceIdentifier)(nil).EvalSymlinks), path) -} - -// Lstat mocks base method. -func (m *MockDeviceIdentifier) Lstat(name string) (os.FileInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Lstat", name) - ret0, _ := ret[0].(os.FileInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Lstat indicates an expected call of Lstat. -func (mr *MockDeviceIdentifierMockRecorder) Lstat(name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lstat", reflect.TypeOf((*MockDeviceIdentifier)(nil).Lstat), name) -} diff --git a/pkg/mounter/mock_mount_windows.go b/pkg/mounter/mock_mount_windows.go index ec70a25058..0c5ffe985c 100644 --- a/pkg/mounter/mock_mount_windows.go +++ b/pkg/mounter/mock_mount_windows.go @@ -13,7 +13,6 @@ import ( // MockProxyMounter is a mock of ProxyMounter interface. type MockProxyMounter struct { - mount.Interface ctrl *gomock.Controller recorder *MockProxyMounterMockRecorder } diff --git a/pkg/driver/mount.go b/pkg/mounter/mount.go similarity index 62% rename from pkg/driver/mount.go rename to pkg/mounter/mount.go index 5c44db5c49..e4a69fd7f6 100644 --- a/pkg/driver/mount.go +++ b/pkg/mounter/mount.go @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver +// Package mounter implements OS-specific functionality for interacting with mounts. +// +// The package should any implementation of mount related functionality that is not portable across platforms. +package mounter import ( - "os" - "path/filepath" - - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" mountutils "k8s.io/mount-utils" ) @@ -41,11 +40,11 @@ type Mounter interface { NeedResize(devicePath string, deviceMountPath string) (bool, error) Unpublish(path string) error Unstage(path string) error - NewResizeFs() (Resizefs, error) -} - -type Resizefs interface { Resize(devicePath, deviceMountPath string) (bool, error) + FindDevicePath(devicePath, volumeID, partition, region string) (string, error) + PreparePublishTarget(target string) error + IsBlockDevice(fullPath string) (bool, error) + GetBlockSizeBytes(devicePath string) (int64, error) } // NodeMounter implements Mounter. @@ -54,35 +53,12 @@ type NodeMounter struct { *mountutils.SafeFormatAndMount } -func newNodeMounter() (Mounter, error) { +// NewNodeMounter returns a new intsance of NodeMounter. +func NewNodeMounter() (Mounter, error) { // mounter.NewSafeMounter returns a SafeFormatAndMount - safeMounter, err := mounter.NewSafeMounter() + safeMounter, err := NewSafeMounter() if err != nil { return nil, err } return &NodeMounter{safeMounter}, nil } - -// DeviceIdentifier is for mocking os io functions used for the driver to -// identify an EBS volume's corresponding device (in Linux, the path under -// /dev; in Windows, the volume number) so that it can mount it. For volumes -// already mounted, see GetDeviceNameFromMount. -// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device -type DeviceIdentifier interface { - Lstat(name string) (os.FileInfo, error) - EvalSymlinks(path string) (string, error) -} - -type nodeDeviceIdentifier struct{} - -func newNodeDeviceIdentifier() DeviceIdentifier { - return &nodeDeviceIdentifier{} -} - -func (i *nodeDeviceIdentifier) Lstat(name string) (os.FileInfo, error) { - return os.Lstat(name) -} - -func (i *nodeDeviceIdentifier) EvalSymlinks(path string) (string, error) { - return filepath.EvalSymlinks(path) -} diff --git a/pkg/driver/node_linux.go b/pkg/mounter/mount_linux.go similarity index 61% rename from pkg/driver/node_linux.go rename to pkg/mounter/mount_linux.go index 5d0981ee72..e2b6c6b090 100644 --- a/pkg/driver/node_linux.go +++ b/pkg/mounter/mount_linux.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver +package mounter import ( "fmt" @@ -31,24 +31,18 @@ import ( "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/util" "golang.org/x/sys/unix" "k8s.io/klog/v2" + mountutils "k8s.io/mount-utils" ) -func (d *nodeService) appendPartition(devicePath, partition string) string { - if partition == "" { - return devicePath - } - - if strings.HasPrefix(devicePath, "/dev/nvme") { - return devicePath + nvmeDiskPartitionSuffix + partition - } - - return devicePath + diskPartitionSuffix + partition -} +const ( + nvmeDiskPartitionSuffix = "p" + diskPartitionSuffix = "" +) -// findDevicePath finds path of device and verifies its existence +// FindDevicePath finds path of device and verifies its existence // if the device is not nvme, return the path directly // if the device is nvme, finds and returns the nvme device path eg. /dev/nvme1n1 -func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (string, error) { +func (m *NodeMounter) FindDevicePath(devicePath, volumeID, partition, region string) (string, error) { strippedVolumeName := strings.Replace(volumeID, "-", "", -1) canonicalDevicePath := "" @@ -58,19 +52,19 @@ func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (st // | File: ‘/dev/xvdba’ -> ‘nvme1n1’ // Since these are maybes, not guarantees, the search for the nvme device // path below must happen and must rely on volume ID - exists, err := d.mounter.PathExists(devicePath) + exists, err := m.PathExists(devicePath) if err != nil { return "", fmt.Errorf("failed to check if path %q exists: %w", devicePath, err) } if exists { - stat, lstatErr := d.deviceIdentifier.Lstat(devicePath) + stat, lstatErr := os.Lstat(devicePath) if lstatErr != nil { return "", fmt.Errorf("failed to lstat %q: %w", devicePath, err) } if stat.Mode()&os.ModeSymlink == os.ModeSymlink { - canonicalDevicePath, err = d.deviceIdentifier.EvalSymlinks(devicePath) + canonicalDevicePath, err = filepath.EvalSymlinks(devicePath) if err != nil { return "", fmt.Errorf("failed to evaluate symlink %q: %w", devicePath, err) } @@ -82,7 +76,7 @@ func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (st if err = verifyVolumeSerialMatch(canonicalDevicePath, strippedVolumeName, execRunner); err != nil { return "", err } - return d.appendPartition(canonicalDevicePath, partition), nil + return m.appendPartition(canonicalDevicePath, partition), nil } klog.V(5).InfoS("[Debug] Falling back to nvme volume ID lookup", "devicePath", devicePath) @@ -94,8 +88,7 @@ func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (st // vol-0fab1d5e3f72a5e23 creates a symlink at // /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 nvmeName := "nvme-Amazon_Elastic_Block_Store_" + strippedVolumeName - - nvmeDevicePath, err := findNvmeVolume(d.deviceIdentifier, nvmeName) + nvmeDevicePath, err := findNvmeVolume(nvmeName) if err == nil { klog.V(5).InfoS("[Debug] successfully resolved", "nvmeName", nvmeName, "nvmeDevicePath", nvmeDevicePath) @@ -103,12 +96,12 @@ func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (st if err = verifyVolumeSerialMatch(canonicalDevicePath, strippedVolumeName, execRunner); err != nil { return "", err } - return d.appendPartition(canonicalDevicePath, partition), nil + return m.appendPartition(canonicalDevicePath, partition), nil } else { klog.V(5).InfoS("[Debug] error searching for nvme path", "nvmeName", nvmeName, "err", err) } - if util.IsSBE(d.metadata.GetRegion()) { + if util.IsSBE(region) { klog.V(5).InfoS("[Debug] Falling back to snow volume lookup", "devicePath", devicePath) // Snow completely ignores the requested device path and mounts volumes starting at /dev/vda .. /dev/vdb .. etc // Morph the device path to the snow form by chopping off the last letter and prefixing with /dev/vd @@ -118,19 +111,51 @@ func (d *nodeService) findDevicePath(devicePath, volumeID, partition string) (st } if canonicalDevicePath == "" { - return "", errNoDevicePathFound(devicePath, volumeID) + return "", fmt.Errorf("no device path for device %q volume %q found", devicePath, volumeID) } - canonicalDevicePath = d.appendPartition(canonicalDevicePath, partition) + canonicalDevicePath = m.appendPartition(canonicalDevicePath, partition) return canonicalDevicePath, nil } -// Helper to inject exec.Comamnd().CombinedOutput() for verifyVolumeSerialMatch +// findNvmeVolume looks for the nvme volume with the specified name +// It follows the symlink (if it exists) and returns the absolute path to the device +func findNvmeVolume(findName string) (device string, err error) { + p := filepath.Join("/dev/disk/by-id/", findName) + stat, err := os.Lstat(p) + if err != nil { + if os.IsNotExist(err) { + klog.V(5).InfoS("[Debug] nvme path not found", "path", p) + return "", fmt.Errorf("nvme path %q not found", p) + } + return "", fmt.Errorf("error getting stat of %q: %w", p, err) + } + + if stat.Mode()&os.ModeSymlink != os.ModeSymlink { + klog.InfoS("nvme file found, but was not a symlink", "path", p) + return "", fmt.Errorf("nvme file %q found, but was not a symlink", p) + } + // Find the target, resolving to an absolute path + // For example, /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 -> ../../nvme2n1 + resolved, err := filepath.EvalSymlinks(p) + if err != nil { + return "", fmt.Errorf("error reading target of symlink %q: %w", p, err) + } + + if !strings.HasPrefix(resolved, "/dev") { + return "", fmt.Errorf("resolved symlink for %q was unexpected: %q", p, resolved) + } + + return resolved, nil +} + +// execRunner is a helper to inject exec.Comamnd().CombinedOutput() for verifyVolumeSerialMatch // Tests use a mocked version that does not actually execute any binaries func execRunner(name string, arg ...string) ([]byte, error) { return exec.Command(name, arg...).CombinedOutput() } +// verifyVolumeSerialMatch checks the volume serial of the device against the expected volume func verifyVolumeSerialMatch(canonicalDevicePath string, strippedVolumeName string, execRunner func(string, ...string) ([]byte, error)) error { // In some rare cases, a race condition can lead to the /dev/disk/by-id/ symlink becoming out of date // See https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues/1224 for more info @@ -145,7 +170,7 @@ func verifyVolumeSerialMatch(canonicalDevicePath string, strippedVolumeName stri for _, volume := range volumeRegex.FindAllString(string(output), -1) { klog.V(6).InfoS("Comparing volume serial", "canonicalDevicePath", canonicalDevicePath, "expected", strippedVolumeName, "actual", volume) if volume != strippedVolumeName { - return fmt.Errorf("Refusing to mount %s because it claims to be %s but should be %s", canonicalDevicePath, volume, strippedVolumeName) + return fmt.Errorf("refusing to mount %s because it claims to be %s but should be %s", canonicalDevicePath, volume, strippedVolumeName) } } } else { @@ -156,51 +181,17 @@ func verifyVolumeSerialMatch(canonicalDevicePath string, strippedVolumeName stri return nil } -func errNoDevicePathFound(devicePath, volumeID string) error { - return fmt.Errorf("no device path for device %q volume %q found", devicePath, volumeID) -} - -// findNvmeVolume looks for the nvme volume with the specified name -// It follows the symlink (if it exists) and returns the absolute path to the device -func findNvmeVolume(deviceIdentifier DeviceIdentifier, findName string) (device string, err error) { - p := filepath.Join("/dev/disk/by-id/", findName) - stat, err := deviceIdentifier.Lstat(p) - if err != nil { - if os.IsNotExist(err) { - klog.V(5).InfoS("[Debug] nvme path not found", "path", p) - return "", fmt.Errorf("nvme path %q not found", p) - } - return "", fmt.Errorf("error getting stat of %q: %w", p, err) - } - - if stat.Mode()&os.ModeSymlink != os.ModeSymlink { - klog.InfoS("nvme file found, but was not a symlink", "path", p) - return "", fmt.Errorf("nvme file %q found, but was not a symlink", p) - } - // Find the target, resolving to an absolute path - // For example, /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 -> ../../nvme2n1 - resolved, err := deviceIdentifier.EvalSymlinks(p) - if err != nil { - return "", fmt.Errorf("error reading target of symlink %q: %w", p, err) - } - - if !strings.HasPrefix(resolved, "/dev") { - return "", fmt.Errorf("resolved symlink for %q was unexpected: %q", p, resolved) - } - - return resolved, nil -} - -func (d *nodeService) preparePublishTarget(target string) error { +// PreparePublishTarget creates the target directory for the volume to be mounted +func (m *NodeMounter) PreparePublishTarget(target string) error { klog.V(4).InfoS("NodePublishVolume: creating dir", "target", target) - if err := d.mounter.MakeDir(target); err != nil { - return fmt.Errorf("Could not create dir %q: %w", target, err) + if err := m.MakeDir(target); err != nil { + return fmt.Errorf("could not create dir %q: %w", target, err) } return nil } -// IsBlock checks if the given path is a block device -func (d *nodeService) IsBlockDevice(fullPath string) (bool, error) { +// IsBlockDevice checks if the given path is a block device +func (m *NodeMounter) IsBlockDevice(fullPath string) (bool, error) { var st unix.Stat_t err := unix.Stat(fullPath, &st) if err != nil { @@ -210,9 +201,9 @@ func (d *nodeService) IsBlockDevice(fullPath string) (bool, error) { return (st.Mode & unix.S_IFMT) == unix.S_IFBLK, nil } -func (d *nodeService) getBlockSizeBytes(devicePath string) (int64, error) { - cmd := d.mounter.(*NodeMounter).Exec.Command("blockdev", "--getsize64", devicePath) - output, err := cmd.Output() +// GetBlockSizeBytes gets the size of the disk in bytes +func (m *NodeMounter) GetBlockSizeBytes(devicePath string) (int64, error) { + output, err := m.Exec.Command("blockdev", "--getsize64", devicePath).Output() if err != nil { return -1, fmt.Errorf("error when getting size of block volume at path %s: output: %s, err: %w", devicePath, string(output), err) } @@ -223,3 +214,90 @@ func (d *nodeService) getBlockSizeBytes(devicePath string) (int64, error) { } return gotSizeBytes, nil } + +// appendPartition appends the partition to the device path +func (m *NodeMounter) appendPartition(devicePath, partition string) string { + if partition == "" { + return devicePath + } + + if strings.HasPrefix(devicePath, "/dev/nvme") { + return devicePath + nvmeDiskPartitionSuffix + partition + } + + return devicePath + diskPartitionSuffix + partition +} + +// GetDeviceNameFromMount returns the volume ID for a mount path. +func (m NodeMounter) GetDeviceNameFromMount(mountPath string) (string, int, error) { + return mountutils.GetDeviceNameFromMount(m, mountPath) +} + +// IsCorruptedMnt return true if err is about corrupted mount point +func (m NodeMounter) IsCorruptedMnt(err error) bool { + return mountutils.IsCorruptedMnt(err) +} + +// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code +// Please mirror the change to func MakeFile in ./sanity_test.go +func (m *NodeMounter) MakeFile(path string) error { + f, err := os.OpenFile(path, os.O_CREATE, os.FileMode(0644)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + if err = f.Close(); err != nil { + return err + } + return nil +} + +// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code +// Please mirror the change to func MakeFile in ./sanity_test.go +func (m *NodeMounter) MakeDir(path string) error { + err := os.MkdirAll(path, os.FileMode(0755)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +// This function is mirrored in ./sanity_test.go to make sure sanity test covered this block of code +// Please mirror the change to func MakeFile in ./sanity_test.go +func (m *NodeMounter) PathExists(path string) (bool, error) { + return mountutils.PathExists(path) +} + +// Resize resizes the filesystem of the given devicePath +func (m *NodeMounter) Resize(devicePath, deviceMountPath string) (bool, error) { + return mountutils.NewResizeFs(m.Exec).Resize(devicePath, deviceMountPath) +} + +// NeedResize checks if the filesystem of the given devicePath needs to be resized +func (m *NodeMounter) NeedResize(devicePath string, deviceMountPath string) (bool, error) { + return mountutils.NewResizeFs(m.Exec).NeedResize(devicePath, deviceMountPath) +} + +// Unpublish unmounts the given path +func (m *NodeMounter) Unpublish(path string) error { + // On linux, unpublish and unstage both perform an unmount + return m.Unstage(path) +} + +// Unstage unmounts the given path +func (m *NodeMounter) Unstage(path string) error { + err := mountutils.CleanupMountPoint(path, m, false) + // Ignore the error when it contains "not mounted", because that indicates the + // world is already in the desired state + // + // mount-utils attempts to detect this on its own but fails when running on + // a read-only root filesystem, which our manifests use by default + if err == nil || strings.Contains(fmt.Sprint(err), "not mounted") { + return nil + } else { + return err + } +} diff --git a/pkg/mounter/mount_test.go b/pkg/mounter/mount_test.go new file mode 100644 index 0000000000..0e9c3b3415 --- /dev/null +++ b/pkg/mounter/mount_test.go @@ -0,0 +1,308 @@ +//go:build linux +// +build linux + +/* +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 ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/mount-utils" + + utilexec "k8s.io/utils/exec" + fakeexec "k8s.io/utils/exec/testing" +) + +func TestNeedResize(t *testing.T) { + testcases := []struct { + name string + devicePath string + deviceMountPath string + deviceSize string + cmdOutputFsType string + expectError bool + expectResult bool + }{ + { + name: "False - Unsupported fs type", + devicePath: "/dev/test1", + deviceMountPath: "/mnt/test1", + deviceSize: "2048", + cmdOutputFsType: "TYPE=ntfs", + expectError: true, + expectResult: false, + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeAction{ + func() ([]byte, []byte, error) { return []byte(test.deviceSize), nil, nil }, + func() ([]byte, []byte, error) { return []byte(test.cmdOutputFsType), nil, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + safe := mount.SafeFormatAndMount{ + Interface: mount.New(""), + Exec: &fexec, + } + fakeMounter := NodeMounter{&safe} + + needResize, err := fakeMounter.NeedResize(test.devicePath, test.deviceMountPath) + if needResize != test.expectResult { + t.Fatalf("Expect result is %v but got %v", test.expectResult, needResize) + } + if !test.expectError && err != nil { + t.Fatalf("Expect no error but got %v", err) + } + }) + } +} + +func TestMakeDir(t *testing.T) { + // Setup the full driver and its environment + dir, err := os.MkdirTemp("", "mount-ebs-csi") + if err != nil { + t.Fatalf("error creating directory %v", err) + } + defer os.RemoveAll(dir) + + targetPath := filepath.Join(dir, "targetdir") + + mountObj, err := NewNodeMounter() + if err != nil { + t.Fatalf("error creating mounter %v", err) + } + + if mountObj.MakeDir(targetPath) != nil { + t.Fatalf("Expect no error but got: %v", err) + } + + if mountObj.MakeDir(targetPath) != nil { + t.Fatalf("Expect no error but got: %v", err) + } + + if exists, err := mountObj.PathExists(targetPath); !exists { + t.Fatalf("Expect no error but got: %v", err) + } +} + +func TestMakeFile(t *testing.T) { + // Setup the full driver and its environment + dir, err := os.MkdirTemp("", "mount-ebs-csi") + if err != nil { + t.Fatalf("error creating directory %v", err) + } + defer os.RemoveAll(dir) + + targetPath := filepath.Join(dir, "targetfile") + + mountObj, err := NewNodeMounter() + if err != nil { + t.Fatalf("error creating mounter %v", err) + } + + if mountObj.MakeFile(targetPath) != nil { + t.Fatalf("Expect no error but got: %v", err) + } + + if mountObj.MakeFile(targetPath) != nil { + t.Fatalf("Expect no error but got: %v", err) + } + + if exists, err := mountObj.PathExists(targetPath); !exists { + t.Fatalf("Expect no error but got: %v", err) + } + +} + +func TestPathExists(t *testing.T) { + // Setup the full driver and its environment + dir, err := os.MkdirTemp("", "mount-ebs-csi") + if err != nil { + t.Fatalf("error creating directory %v", err) + } + defer os.RemoveAll(dir) + + targetPath := filepath.Join(dir, "notafile") + + mountObj, err := NewNodeMounter() + if err != nil { + t.Fatalf("error creating mounter %v", err) + } + + exists, err := mountObj.PathExists(targetPath) + + if err != nil { + t.Fatalf("Expect no error but got: %v", err) + } + + if exists { + t.Fatalf("Expected file %s to not exist", targetPath) + } + +} + +func TestGetDeviceName(t *testing.T) { + // Setup the full driver and its environment + dir, err := os.MkdirTemp("", "mount-ebs-csi") + if err != nil { + t.Fatalf("error creating directory %v", err) + } + defer os.RemoveAll(dir) + + targetPath := filepath.Join(dir, "notafile") + + mountObj, err := NewNodeMounter() + if err != nil { + t.Fatalf("error creating mounter %v", err) + } + + if _, _, err := mountObj.GetDeviceNameFromMount(targetPath); err != nil { + t.Fatalf("Expect no error but got: %v", err) + } + +} + +func TestFindDevicePath(t *testing.T) { + testCases := []struct { + name string + volumeID string + partition string + region string + createTempDir bool + symlink bool + verifyErr error + deviceSize string + cmdOutputFsType string + expectedErr error + }{ + { + name: "Device path exists and matches volume ID", + volumeID: "vol-1234567890abcdef0", + partition: "1", + createTempDir: true, + symlink: false, + verifyErr: nil, + deviceSize: "1024", + cmdOutputFsType: "ext4", + expectedErr: nil, + }, + { + name: "Device path doesn't exist", + volumeID: "vol-1234567890abcdef0", + partition: "1", + createTempDir: false, + symlink: false, + verifyErr: nil, + deviceSize: "1024", + cmdOutputFsType: "ext4", + expectedErr: fmt.Errorf("no device path for device %q volume %q found", "/temp/vol-1234567890abcdef0", "vol-1234567890abcdef0"), + }, + { + name: "SBE region fallback", + volumeID: "vol-1234567890abcdef0", + partition: "1", + region: "snow", + createTempDir: false, + symlink: false, + verifyErr: nil, + deviceSize: "1024", + cmdOutputFsType: "ext4", + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var tmpDir string + var err error + + if tc.createTempDir { + tmpDir, err = os.MkdirTemp("", "temp-test-device-path") + if err != nil { + t.Fatalf("Failed to create temporary directory: %v", err) + } + defer os.RemoveAll(tmpDir) + } else { + tmpDir = "/temp" + } + + devicePath := filepath.Join(tmpDir, tc.volumeID) + expectedResult := devicePath + tc.partition + + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeAction{ + func() ([]byte, []byte, error) { return []byte(tc.deviceSize), nil, nil }, + func() ([]byte, []byte, error) { return []byte(tc.cmdOutputFsType), nil, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) utilexec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + + safe := mount.SafeFormatAndMount{ + Interface: mount.New(""), + Exec: &fexec, + } + + fakeMounter := NodeMounter{&safe} + + if tc.createTempDir { + if tc.symlink { + symlinkErr := os.Symlink(devicePath, devicePath) + if symlinkErr != nil { + t.Fatalf("Failed to create symlink: %v", err) + } + } else { + _, osCreateErr := os.Create(devicePath) + if osCreateErr != nil { + t.Fatalf("Failed to create device path: %v", err) + } + } + } + + result, err := fakeMounter.FindDevicePath(devicePath, tc.volumeID, tc.partition, tc.region) + + if tc.region == "snow" { + expectedResult = "/dev/vd" + tc.volumeID[len(tc.volumeID)-1:] + tc.partition + } + + if tc.expectedErr == nil { + assert.Equal(t, expectedResult, result) + assert.NoError(t, err) + } else { + assert.Empty(t, result) + assert.EqualError(t, err, tc.expectedErr.Error()) + } + }) + } +} diff --git a/pkg/driver/mount_windows.go b/pkg/mounter/mount_windows.go similarity index 56% rename from pkg/driver/mount_windows.go rename to pkg/mounter/mount_windows.go index 8824f3dba4..15ceb1f63e 100644 --- a/pkg/driver/mount_windows.go +++ b/pkg/mounter/mount_windows.go @@ -17,18 +17,102 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver +package mounter import ( + "context" "fmt" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/resizefs" - mountutils "k8s.io/mount-utils" "regexp" + "strconv" + "strings" + + diskapi "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1" + diskclient "github.com/kubernetes-csi/csi-proxy/client/groups/disk/v1" + "k8s.io/klog/v2" + mountutils "k8s.io/mount-utils" ) +const ( + DefaultBlockSize = 4096 +) + +func (m NodeMounter) FindDevicePath(devicePath, volumeID, _, _ string) (string, error) { + diskClient, err := diskclient.NewClient() + if err != nil { + return "", fmt.Errorf("error creating csi-proxy disk client: %q", err) + } + defer diskClient.Close() + + response, err := diskClient.ListDiskIDs(context.TODO(), &diskapi.ListDiskIDsRequest{}) + if err != nil { + return "", fmt.Errorf("error listing disk ids: %q", err) + } + + diskIDs := response.GetDiskIDs() + + foundDiskNumber := "" + for diskNumber, diskID := range diskIDs { + serialNumber := diskID.GetSerialNumber() + cleanVolumeID := strings.ReplaceAll(volumeID, "-", "") + if strings.Contains(serialNumber, cleanVolumeID) { + foundDiskNumber = strconv.Itoa(int(diskNumber)) + break + } + } + + if foundDiskNumber == "" { + return "", fmt.Errorf("disk number for device path %q volume id %q not found", devicePath, volumeID) + } + + return foundDiskNumber, nil +} + +func (m NodeMounter) PreparePublishTarget(target string) error { + // On Windows, Mount will create the parent of target and mklink (create a symbolic link) at target later, so don't create a + // directory at target now. Otherwise mklink will error: "Cannot create a file when that file already exists". + // Instead, delete the target if it already exists (like if it was created by kubelet <1.20) + // https://github.com/kubernetes/kubernetes/pull/88759 + klog.V(4).InfoS("NodePublishVolume: removing dir", "target", target) + exists, err := m.PathExists(target) + if err != nil { + return fmt.Errorf("error checking path %q exists: %v", target, err) + } + + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) + if !ok { + return fmt.Errorf("failed to cast mounter to csi proxy mounter") + } + + if exists { + if err := proxyMounter.Rmdir(target); err != nil { + return fmt.Errorf("error Rmdir target %q: %v", target, err) + } + } + return nil +} + +// IsBlockDevice checks if the given path is a block device +func (m NodeMounter) IsBlockDevice(fullPath string) (bool, error) { + return false, nil +} + +// getBlockSizeBytes gets the size of the disk in bytes +func (m NodeMounter) GetBlockSizeBytes(devicePath string) (int64, error) { + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) + if !ok { + return -1, fmt.Errorf("failed to cast mounter to csi proxy mounter") + } + + sizeInBytes, err := proxyMounter.GetDeviceSize(devicePath) + if err != nil { + return -1, err + } + + return sizeInBytes, nil +} + func (m NodeMounter) FormatAndMountSensitiveWithFormatOptions(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -44,7 +128,7 @@ func (m NodeMounter) FormatAndMountSensitiveWithFormatOptions(source string, tar // Command to determine ref count would be something like: // Get-Volume -UniqueId "\\?\Volume{7c3da0c1-0000-0000-0000-010000000000}\" | Get-Partition | Select AccessPaths func (m NodeMounter) GetDeviceNameFromMount(mountPath string) (string, int, error) { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return "", 0, fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -76,7 +160,7 @@ func (m NodeMounter) IsCorruptedMnt(err error) bool { } func (m *NodeMounter) MakeFile(path string) error { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -84,7 +168,7 @@ func (m *NodeMounter) MakeFile(path string) error { } func (m *NodeMounter) MakeDir(path string) error { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -92,16 +176,24 @@ func (m *NodeMounter) MakeDir(path string) error { } func (m *NodeMounter) PathExists(path string) (bool, error) { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return false, fmt.Errorf("failed to cast mounter to csi proxy mounter") } return proxyMounter.ExistsPath(path) } +func (m *NodeMounter) Resize(devicePath, deviceMountPath string) (bool, error) { + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) + if !ok { + return false, fmt.Errorf("failed to cast mounter to csi proxy mounter") + } + return proxyMounter.ResizeVolume(deviceMountPath) +} + // NeedResize called at NodeStage to ensure file system is the correct size func (m *NodeMounter) NeedResize(devicePath, deviceMountPath string) (bool, error) { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return false, fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -124,7 +216,7 @@ func (m *NodeMounter) NeedResize(devicePath, deviceMountPath string) (bool, erro // Unmount volume from target path func (m *NodeMounter) Unpublish(target string) error { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -139,7 +231,7 @@ func (m *NodeMounter) Unpublish(target string) error { // Unmount volume from staging path // usually this staging path is a global directory on the node func (m *NodeMounter) Unstage(target string) error { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) + proxyMounter, ok := m.SafeFormatAndMount.Interface.(*CSIProxyMounter) if !ok { return fmt.Errorf("failed to cast mounter to csi proxy mounter") } @@ -150,11 +242,3 @@ func (m *NodeMounter) Unstage(target string) error { } return nil } - -func (m *NodeMounter) NewResizeFs() (Resizefs, error) { - proxyMounter, ok := m.SafeFormatAndMount.Interface.(*mounter.CSIProxyMounter) - if !ok { - return nil, fmt.Errorf("failed to cast mounter to csi proxy mounter") - } - return resizefs.NewResizeFs(proxyMounter), nil -} diff --git a/pkg/mounter/safe_mounter_unix.go b/pkg/mounter/safe_mounter.go similarity index 100% rename from pkg/mounter/safe_mounter_unix.go rename to pkg/mounter/safe_mounter.go diff --git a/pkg/mounter/safe_mounter_unix_test.go b/pkg/mounter/safe_mounter_test.go similarity index 100% rename from pkg/mounter/safe_mounter_unix_test.go rename to pkg/mounter/safe_mounter_test.go diff --git a/pkg/resizefs/resizefs_windows.go b/pkg/resizefs/resizefs_windows.go deleted file mode 100644 index 4a8c0638c5..0000000000 --- a/pkg/resizefs/resizefs_windows.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build windows -// +build windows - -package resizefs - -import ( - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" - "k8s.io/klog/v2" -) - -// resizeFs provides support for resizing file systems -type resizeFs struct { - proxy mounter.ProxyMounter -} - -// NewResizeFs returns an instance of resizeFs -func NewResizeFs(p mounter.ProxyMounter) *resizeFs { - return &resizeFs{proxy: p} -} - -// Resize performs resize of file system -func (r *resizeFs) Resize(_, deviceMountPath string) (bool, error) { - klog.V(3).InfoS("Resize - Expanding mounted volume", "deviceMountPath", deviceMountPath) - return r.proxy.ResizeVolume(deviceMountPath) -} diff --git a/pkg/resizefs/resizefs_windows_test.go b/pkg/resizefs/resizefs_windows_test.go deleted file mode 100644 index 088931b922..0000000000 --- a/pkg/resizefs/resizefs_windows_test.go +++ /dev/null @@ -1,59 +0,0 @@ -//go:build windows -// +build windows - -package resizefs - -import ( - "errors" - "github.com/golang/mock/gomock" - "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" - "testing" -) - -func TestResize(t *testing.T) { - testCases := []struct { - name string - deviceMountPath string - expectedResult bool - expectedError bool - prepare func(m *mounter.MockProxyMounter) - }{ - { - name: "success: normal", - deviceMountPath: "/mnt/test", - expectedResult: true, - expectedError: false, - prepare: func(m *mounter.MockProxyMounter) { - m.EXPECT().ResizeVolume(gomock.Eq("/mnt/test")).Return(true, nil) - }, - }, - { - name: "failure: invalid device mount path", - deviceMountPath: "/", - expectedResult: false, - expectedError: true, - prepare: func(m *mounter.MockProxyMounter) { - m.EXPECT().ResizeVolume(gomock.Eq("/")).Return(false, errors.New("Could not resize volume")) - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockCtl := gomock.NewController(t) - defer mockCtl.Finish() - - mockProxyMounter := mounter.NewMockProxyMounter(mockCtl) - tc.prepare(mockProxyMounter) - - r := NewResizeFs(mockProxyMounter) - res, err := r.Resize("", tc.deviceMountPath) - - if tc.expectedError && err == nil { - t.Fatalf("Expected error, but got no error") - } - if res != tc.expectedResult { - t.Fatalf("Expected result is: %v, but got: %v", tc.expectedResult, res) - } - }) - } -}