From 31ce352a9c8753baefb63072b00dc4725599f466 Mon Sep 17 00:00:00 2001 From: Xiang Li Date: Fri, 23 Apr 2021 12:58:00 -0700 Subject: [PATCH] Resize filesystem when restore a snapshot to larger size volume --- pkg/driver/mocks/mock_mount.go | 15 + pkg/driver/mount.go | 122 ++++++++ pkg/driver/mount_test.go | 288 ++++++++++++++++++ pkg/driver/node.go | 21 +- pkg/driver/node_test.go | 6 + pkg/driver/sanity_test.go | 4 + vendor/k8s.io/utils/exec/testing/fake_exec.go | 266 ++++++++++++++++ vendor/modules.txt | 1 + 8 files changed, 719 insertions(+), 4 deletions(-) create mode 100644 vendor/k8s.io/utils/exec/testing/fake_exec.go diff --git a/pkg/driver/mocks/mock_mount.go b/pkg/driver/mocks/mock_mount.go index 7e556e4737..eeb81d98c9 100644 --- a/pkg/driver/mocks/mock_mount.go +++ b/pkg/driver/mocks/mock_mount.go @@ -234,6 +234,21 @@ func (mr *MockMounterMockRecorder) MountSensitiveWithoutSystemd(source, target, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MountSensitiveWithoutSystemd", reflect.TypeOf((*MockMounter)(nil).MountSensitiveWithoutSystemd), source, target, fstype, options, sensitiveOptions) } +// NeedResize mocks base method. +func (m *MockMounter) NeedResize(devicePath, deviceMountPath string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NeedResize", devicePath, deviceMountPath) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NeedResize indicates an expected call of NeedResize. +func (mr *MockMounterMockRecorder) NeedResize(devicePath, deviceMountPath interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NeedResize", reflect.TypeOf((*MockMounter)(nil).NeedResize), devicePath, deviceMountPath) +} + // PathExists mocks base method. func (m *MockMounter) PathExists(path string) (bool, error) { m.ctrl.T.Helper() diff --git a/pkg/driver/mount.go b/pkg/driver/mount.go index e81d79eac9..f1ce7b48c8 100644 --- a/pkg/driver/mount.go +++ b/pkg/driver/mount.go @@ -17,7 +17,11 @@ limitations under the License. package driver import ( + "fmt" + "k8s.io/klog" "os" + "strconv" + "strings" "github.com/kubernetes-sigs/aws-ebs-csi-driver/pkg/mounter" mountutils "k8s.io/mount-utils" @@ -39,6 +43,7 @@ type Mounter interface { MakeFile(path string) error MakeDir(path string) error PathExists(path string) (bool, error) + NeedResize(devicePath string, deviceMountPath string) (bool, error) } type NodeMounter struct { @@ -91,3 +96,120 @@ func (m *NodeMounter) MakeDir(path string) error { func (m *NodeMounter) PathExists(path string) (bool, error) { return mountutils.PathExists(path) } + +//TODO: use common util from vendor kubernetes/mount-util +func (m *NodeMounter) NeedResize(devicePath string, deviceMountPath string) (bool, error) { + // TODO(xiangLi) resize fs size on formatted file system following this PR https://github.com/kubernetes/kubernetes/pull/99223 + // Port the in-tree un-released change first, need to remove after in-tree release + deviceSize, err := m.getDeviceSize(devicePath) + if err != nil { + return false, err + } + var fsSize, blockSize uint64 + format, err := m.SafeFormatAndMount.GetDiskFormat(devicePath) + if err != nil { + formatErr := fmt.Errorf("ResizeFS.Resize - error checking format for device %s: %v", devicePath, err) + return false, formatErr + } + + // If disk has no format, there is no need to resize the disk because mkfs.* + // by default will use whole disk anyways. + if format == "" { + return false, nil + } + + klog.V(3).Infof("ResizeFs.needResize - checking mounted volume %s", devicePath) + switch format { + case "ext3", "ext4": + blockSize, fsSize, err = m.getExtSize(devicePath) + klog.V(5).Infof("Ext size: filesystem size=%d, block size=%d", fsSize, blockSize) + case "xfs": + blockSize, fsSize, err = m.getXFSSize(deviceMountPath) + klog.V(5).Infof("Xfs size: filesystem size=%d, block size=%d, err=%v", fsSize, blockSize, err) + default: + klog.Errorf("Not able to parse given filesystem info. fsType: %s, will not resize", format) + return false, fmt.Errorf("Could not parse fs info on given filesystem format: %s. Supported fs types are: xfs, ext3, ext4", format) + } + if err != nil { + return false, err + } + // Tolerate one block difference, just in case of rounding errors somewhere. + klog.V(5).Infof("Volume %s: device size=%d, filesystem size=%d, block size=%d", devicePath, deviceSize, fsSize, blockSize) + if deviceSize <= fsSize+blockSize { + return false, nil + } + return true, nil +} +func (m *NodeMounter) getDeviceSize(devicePath string) (uint64, error) { + output, err := m.SafeFormatAndMount.Exec.Command("blockdev", "--getsize64", devicePath).CombinedOutput() + outStr := strings.TrimSpace(string(output)) + if err != nil { + return 0, fmt.Errorf("failed to read size of device %s: %s: %s", devicePath, err, outStr) + } + size, err := strconv.ParseUint(outStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse size of device %s %s: %s", devicePath, outStr, err) + } + return size, nil +} + +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: %s: %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: %s: %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: %s", 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: %s", value, err) + } + } + } + return blockSize, blockCount, err +} diff --git a/pkg/driver/mount_test.go b/pkg/driver/mount_test.go index 7c49da8ae2..18354e9442 100644 --- a/pkg/driver/mount_test.go +++ b/pkg/driver/mount_test.go @@ -18,11 +18,299 @@ package driver import ( "io/ioutil" + "k8s.io/mount-utils" "os" "path/filepath" "testing" + + 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, &fexec} + + 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: utilexec.New(), + } + fakeMounter := NodeMounter{safe, &fexec} + + 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 := ioutil.TempDir("", "mount-ebs-csi") diff --git a/pkg/driver/node.go b/pkg/driver/node.go index 03d20cf2fe..46dd519e61 100644 --- a/pkg/driver/node.go +++ b/pkg/driver/node.go @@ -133,18 +133,18 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol return &csi.NodeStageVolumeResponse{}, nil } - mount := volCap.GetMount() - if mount == nil { + mountVolume := volCap.GetMount() + if mountVolume == nil { return nil, status.Error(codes.InvalidArgument, "NodeStageVolume: mount is nil within volume capability") } - fsType := mount.GetFsType() + fsType := mountVolume.GetFsType() if len(fsType) == 0 { fsType = defaultFsType } var mountOptions []string - for _, f := range mount.MountFlags { + for _, f := range mountVolume.MountFlags { if !hasMountOption(mountOptions, f) { mountOptions = append(mountOptions, f) } @@ -216,6 +216,19 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol msg := fmt.Sprintf("could not format %q and mount it at %q: %v", source, target, err) return nil, status.Error(codes.Internal, msg) } + //TODO: use the common function from vendor pkg kubernetes/mount-util + + needResize, err := d.mounter.NeedResize(source, target) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not determine if volume %q (%q) need to be resized: %v", req.GetVolumeId(), source, err) + } + if needResize { + r := mountutils.NewResizeFs(d.mounter) + klog.V(2).Infof("Volume %s needs resizing", source) + if _, err := r.Resize(source, target); err != nil { + return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q): %v", volumeID, source, err) + } + } return &csi.NodeStageVolumeResponse{}, nil } diff --git a/pkg/driver/node_test.go b/pkg/driver/node_test.go index 1142eadce1..f1e16ea8c6 100644 --- a/pkg/driver/node_test.go +++ b/pkg/driver/node_test.go @@ -74,6 +74,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any()) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) }, }, { @@ -121,6 +122,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt4), gomock.Eq([]string{"dirsync", "noexec"})) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) }, }, { @@ -149,6 +151,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt3), gomock.Any()) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) }, }, { @@ -177,6 +180,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(FSTypeExt4), gomock.Any()) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) }, }, { @@ -290,6 +294,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePathWithPartition), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any()) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePathWithPartition), gomock.Eq(targetPath)).Return(false, nil) }, }, { @@ -309,6 +314,7 @@ func TestNodeStageVolume(t *testing.T) { mockMounter.EXPECT().MakeDir(targetPath).Return(nil) mockMounter.EXPECT().GetDeviceNameFromMount(targetPath).Return("", 1, nil) mockMounter.EXPECT().FormatAndMount(gomock.Eq(devicePath), gomock.Eq(targetPath), gomock.Eq(defaultFsType), gomock.Any()) + mockMounter.EXPECT().NeedResize(gomock.Eq(devicePath), gomock.Eq(targetPath)).Return(false, nil) }, }, { diff --git a/pkg/driver/sanity_test.go b/pkg/driver/sanity_test.go index 64ac0f667f..b8e8a3e2e3 100644 --- a/pkg/driver/sanity_test.go +++ b/pkg/driver/sanity_test.go @@ -360,3 +360,7 @@ func (f *fakeMounter) PathExists(filename string) (bool, error) { } return true, nil } + +func (f *fakeMounter) NeedResize(source string, path string) (bool, error) { + return false, nil +} diff --git a/vendor/k8s.io/utils/exec/testing/fake_exec.go b/vendor/k8s.io/utils/exec/testing/fake_exec.go new file mode 100644 index 0000000000..7380689256 --- /dev/null +++ b/vendor/k8s.io/utils/exec/testing/fake_exec.go @@ -0,0 +1,266 @@ +/* +Copyright 2017 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 testingexec + +import ( + "context" + "fmt" + "io" + + "k8s.io/utils/exec" +) + +// FakeExec is a simple scripted Interface type. +type FakeExec struct { + CommandScript []FakeCommandAction + CommandCalls int + LookPathFunc func(string) (string, error) + // ExactOrder enforces that commands are called in the order they are scripted, + // and with the exact same arguments + ExactOrder bool + // DisableScripts removes the requirement that a slice of FakeCommandAction be + // populated before calling Command(). This makes the fakeexec (and subsequent + // calls to Run() or CombinedOutput() always return success and there is no + // ability to set their output. + DisableScripts bool +} + +var _ exec.Interface = &FakeExec{} + +// FakeCommandAction is the function to be executed +type FakeCommandAction func(cmd string, args ...string) exec.Cmd + +// Command is to track the commands that are executed +func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd { + if fake.DisableScripts { + fakeCmd := &FakeCmd{DisableScripts: true} + return InitFakeCmd(fakeCmd, cmd, args...) + } + if fake.CommandCalls > len(fake.CommandScript)-1 { + panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args)) + } + i := fake.CommandCalls + fake.CommandCalls++ + fakeCmd := fake.CommandScript[i](cmd, args...) + if fake.ExactOrder { + argv := append([]string{cmd}, args...) + fc := fakeCmd.(*FakeCmd) + if cmd != fc.Argv[0] { + panic(fmt.Sprintf("received command: %s, expected: %s", cmd, fc.Argv[0])) + } + if len(argv) != len(fc.Argv) { + panic(fmt.Sprintf("command (%s) received with extra/missing arguments. Expected %v, Received %v", cmd, fc.Argv, argv)) + } + for i, a := range argv[1:] { + if a != fc.Argv[i+1] { + panic(fmt.Sprintf("command (%s) called with unexpected argument. Expected %s, Received %s", cmd, fc.Argv[i+1], a)) + } + } + } + return fakeCmd +} + +// CommandContext wraps arguments into exec.Cmd +func (fake *FakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd { + return fake.Command(cmd, args...) +} + +// LookPath is for finding the path of a file +func (fake *FakeExec) LookPath(file string) (string, error) { + return fake.LookPathFunc(file) +} + +// FakeCmd is a simple scripted Cmd type. +type FakeCmd struct { + Argv []string + CombinedOutputScript []FakeAction + CombinedOutputCalls int + CombinedOutputLog [][]string + OutputScript []FakeAction + OutputCalls int + OutputLog [][]string + RunScript []FakeAction + RunCalls int + RunLog [][]string + Dirs []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Env []string + StdoutPipeResponse FakeStdIOPipeResponse + StderrPipeResponse FakeStdIOPipeResponse + WaitResponse error + StartResponse error + DisableScripts bool +} + +var _ exec.Cmd = &FakeCmd{} + +// InitFakeCmd is for creating a fake exec.Cmd +func InitFakeCmd(fake *FakeCmd, cmd string, args ...string) exec.Cmd { + fake.Argv = append([]string{cmd}, args...) + return fake +} + +// FakeStdIOPipeResponse holds responses to use as fakes for the StdoutPipe and +// StderrPipe method calls +type FakeStdIOPipeResponse struct { + ReadCloser io.ReadCloser + Error error +} + +// FakeAction is a function type +type FakeAction func() ([]byte, []byte, error) + +// SetDir sets the directory +func (fake *FakeCmd) SetDir(dir string) { + fake.Dirs = append(fake.Dirs, dir) +} + +// SetStdin sets the stdin +func (fake *FakeCmd) SetStdin(in io.Reader) { + fake.Stdin = in +} + +// SetStdout sets the stdout +func (fake *FakeCmd) SetStdout(out io.Writer) { + fake.Stdout = out +} + +// SetStderr sets the stderr +func (fake *FakeCmd) SetStderr(out io.Writer) { + fake.Stderr = out +} + +// SetEnv sets the environment variables +func (fake *FakeCmd) SetEnv(env []string) { + fake.Env = env +} + +// StdoutPipe returns an injected ReadCloser & error (via StdoutPipeResponse) +// to be able to inject an output stream on Stdout +func (fake *FakeCmd) StdoutPipe() (io.ReadCloser, error) { + return fake.StdoutPipeResponse.ReadCloser, fake.StdoutPipeResponse.Error +} + +// StderrPipe returns an injected ReadCloser & error (via StderrPipeResponse) +// to be able to inject an output stream on Stderr +func (fake *FakeCmd) StderrPipe() (io.ReadCloser, error) { + return fake.StderrPipeResponse.ReadCloser, fake.StderrPipeResponse.Error +} + +// Start mimicks starting the process (in the background) and returns the +// injected StartResponse +func (fake *FakeCmd) Start() error { + return fake.StartResponse +} + +// Wait mimicks waiting for the process to exit returns the +// injected WaitResponse +func (fake *FakeCmd) Wait() error { + return fake.WaitResponse +} + +// Run runs the command +func (fake *FakeCmd) Run() error { + if fake.DisableScripts { + return nil + } + if fake.RunCalls > len(fake.RunScript)-1 { + panic("ran out of Run() actions") + } + if fake.RunLog == nil { + fake.RunLog = [][]string{} + } + i := fake.RunCalls + fake.RunLog = append(fake.RunLog, append([]string{}, fake.Argv...)) + fake.RunCalls++ + stdout, stderr, err := fake.RunScript[i]() + if stdout != nil { + fake.Stdout.Write(stdout) + } + if stderr != nil { + fake.Stderr.Write(stderr) + } + return err +} + +// CombinedOutput returns the output from the command +func (fake *FakeCmd) CombinedOutput() ([]byte, error) { + if fake.DisableScripts { + return []byte{}, nil + } + if fake.CombinedOutputCalls > len(fake.CombinedOutputScript)-1 { + panic("ran out of CombinedOutput() actions") + } + if fake.CombinedOutputLog == nil { + fake.CombinedOutputLog = [][]string{} + } + i := fake.CombinedOutputCalls + fake.CombinedOutputLog = append(fake.CombinedOutputLog, append([]string{}, fake.Argv...)) + fake.CombinedOutputCalls++ + stdout, _, err := fake.CombinedOutputScript[i]() + return stdout, err +} + +// Output is the response from the command +func (fake *FakeCmd) Output() ([]byte, error) { + if fake.DisableScripts { + return []byte{}, nil + } + if fake.OutputCalls > len(fake.OutputScript)-1 { + panic("ran out of Output() actions") + } + if fake.OutputLog == nil { + fake.OutputLog = [][]string{} + } + i := fake.OutputCalls + fake.OutputLog = append(fake.OutputLog, append([]string{}, fake.Argv...)) + fake.OutputCalls++ + stdout, _, err := fake.OutputScript[i]() + return stdout, err +} + +// Stop is to stop the process +func (fake *FakeCmd) Stop() { + // no-op +} + +// FakeExitError is a simple fake ExitError type. +type FakeExitError struct { + Status int +} + +var _ exec.ExitError = FakeExitError{} + +func (fake FakeExitError) String() string { + return fmt.Sprintf("exit %d", fake.Status) +} + +func (fake FakeExitError) Error() string { + return fake.String() +} + +// Exited always returns true +func (fake FakeExitError) Exited() bool { + return true +} + +// ExitStatus returns the fake status +func (fake FakeExitError) ExitStatus() int { + return fake.Status +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b957c744fa..6414281ffe 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -883,6 +883,7 @@ k8s.io/mount-utils ## explicit k8s.io/utils/buffer k8s.io/utils/exec +k8s.io/utils/exec/testing k8s.io/utils/integer k8s.io/utils/io k8s.io/utils/keymutex