diff --git a/integrationtests/filesystem_test.go b/integrationtests/filesystem_test.go index 62bf30e2..5c8965cf 100644 --- a/integrationtests/filesystem_test.go +++ b/integrationtests/filesystem_test.go @@ -1,19 +1,245 @@ package integrationtests import ( + "context" + "errors" + "fmt" + "io/ioutil" + "math/rand" "os" + "path/filepath" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kubernetes-csi/csi-proxy/pkg/filesystem" + filesystemapi "github.com/kubernetes-csi/csi-proxy/pkg/filesystem/api" ) -func pathExists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err +func TestFilesystem(t *testing.T) { + t.Run("PathExists positive", func(t *testing.T) { + client, err := filesystem.New(filesystemapi.New()) + require.Nil(t, err) + + s1 := rand.NewSource(time.Now().UnixNano()) + r1 := rand.New(s1) + + // simulate FS operations around staging a volume on a node + stagepath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io\\volume%d", r1.Intn(100), r1.Intn(100)), t) + mkdirReq := &filesystem.MkdirRequest{ + Path: stagepath, + } + _, err = client.Mkdir(context.Background(), mkdirReq) + require.NoError(t, err) + + exists, err := pathExists(stagepath) + assert.True(t, exists, err) + + // simulate operations around publishing a volume to a pod + podpath := getKubeletPathForTest(fmt.Sprintf("test-pod-id\\volumes\\kubernetes.io~csi\\pvc-test%d", r1.Intn(100)), t) + mkdirReq = &filesystem.MkdirRequest{ + Path: podpath, + } + _, err = client.Mkdir(context.Background(), mkdirReq) + require.NoError(t, err) + + exists, err = pathExists(podpath) + assert.True(t, exists, err) + + sourcePath := stagepath + targetPath := filepath.Join(podpath, "rootvol") + // source <- target + linkReq := &filesystem.CreateSymlinkRequest{ + SourcePath: sourcePath, + TargetPath: targetPath, + } + _, err = client.CreateSymlink(context.Background(), linkReq) + require.NoError(t, err) + + exists, err = pathExists(podpath + "\\rootvol") + assert.True(t, exists, err) + + // cleanup pvpath + rmdirReq := &filesystem.RmdirRequest{ + Path: podpath, + Force: true, + } + _, err = client.Rmdir(context.Background(), rmdirReq) + require.NoError(t, err) + + exists, err = pathExists(podpath) + assert.False(t, exists, err) + + // cleanup plugin path + rmdirReq = &filesystem.RmdirRequest{ + Path: stagepath, + Force: true, + } + _, err = client.Rmdir(context.Background(), rmdirReq) + require.NoError(t, err) + + exists, err = pathExists(stagepath) + assert.False(t, exists, err) + }) + t.Run("IsMount", func(t *testing.T) { + client, err := filesystem.New(filesystemapi.New()) + require.Nil(t, err) + + s1 := rand.NewSource(time.Now().UnixNano()) + r1 := rand.New(s1) + rand1 := r1.Intn(100) + rand2 := r1.Intn(100) + + testDir := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io", rand1), t) + err = os.MkdirAll(testDir, os.ModeDir) + require.Nil(t, err) + defer os.RemoveAll(testDir) + + // 1. Check the isMount on a path which does not exist. Failure scenario. + stagepath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io\\volume%d", rand1, rand2), t) + IsSymlinkRequest := &filesystem.IsSymlinkRequest{ + Path: stagepath, + } + isSymlink, err := client.IsSymlink(context.Background(), IsSymlinkRequest) + require.NotNil(t, err) + + // 2. Create the directory. This time its not a mount point. Failure scenario. + err = os.Mkdir(stagepath, os.ModeDir) + require.Nil(t, err) + defer os.Remove(stagepath) + IsSymlinkRequest = &filesystem.IsSymlinkRequest{ + Path: stagepath, + } + isSymlink, err = client.IsSymlink(context.Background(), IsSymlinkRequest) + require.Nil(t, err) + require.Equal(t, isSymlink.IsSymlink, false) + + err = os.Remove(stagepath) + require.Nil(t, err) + targetStagePath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io\\volume%d-tgt", rand1, rand2), t) + lnTargetStagePath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io\\volume%d-tgt-ln", rand1, rand2), t) + + // 3. Create soft link to the directory and make sure target exists. Success scenario. + err = os.Mkdir(targetStagePath, os.ModeDir) + require.Nil(t, err) + defer os.Remove(targetStagePath) + // Create a symlink + err = os.Symlink(targetStagePath, lnTargetStagePath) + require.Nil(t, err) + defer os.Remove(lnTargetStagePath) + + IsSymlinkRequest = &filesystem.IsSymlinkRequest{ + Path: lnTargetStagePath, + } + isSymlink, err = client.IsSymlink(context.Background(), IsSymlinkRequest) + require.Nil(t, err) + require.Equal(t, isSymlink.IsSymlink, true) + + // 4. Remove the path. Failure scenario. + err = os.Remove(targetStagePath) + require.Nil(t, err) + IsSymlinkRequest = &filesystem.IsSymlinkRequest{ + Path: lnTargetStagePath, + } + isSymlink, err = client.IsSymlink(context.Background(), IsSymlinkRequest) + require.Nil(t, err) + require.Equal(t, isSymlink.IsSymlink, false) + }) + t.Run("RmdirContents", func(t *testing.T) { + client, err := filesystem.New(filesystemapi.New()) + require.Nil(t, err) + + r1 := rand.New(rand.NewSource(time.Now().UnixNano())) + rand1 := r1.Intn(100) + + rootPath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io", rand1), t) + // this line should delete the rootPath because only its content were deleted + defer os.RemoveAll(rootPath) + + paths := []string{ + filepath.Join(rootPath, "foo/goo/"), + filepath.Join(rootPath, "foo/bar/baz/"), + filepath.Join(rootPath, "alpha/beta/gamma/"), + } + for _, path := range paths { + err = os.MkdirAll(path, os.ModeDir) + require.Nil(t, err) + } + + rmdirContentsRequest := &filesystem.RmdirContentsRequest{ + Path: rootPath, + } + _, err = client.RmdirContents(context.Background(), rmdirContentsRequest) + require.Nil(t, err) + + // the root path should exist + exists, err := pathExists(rootPath) + assert.True(t, exists, err) + // the root path children shouldn't exist + for _, path := range paths { + exists, err = pathExists(path) + assert.False(t, exists, err) + } + }) + + t.Run("RmdirContentsNoFollowSymlink", func(t *testing.T) { + // RmdirContents should not delete the target of a symlink, only the symlink + client, err := filesystem.New(filesystemapi.New()) + require.Nil(t, err) + + r1 := rand.New(rand.NewSource(time.Now().UnixNano())) + rand1 := r1.Intn(100) + + rootPath := getKubeletPathForTest(fmt.Sprintf("testplugin-%d.csi.io", rand1), t) + // this line should delete the rootPath because only its content were deleted + defer os.RemoveAll(rootPath) + + insidePath := filepath.Join(rootPath, "inside/") + outsidePath := filepath.Join(rootPath, "outside/") + paths := []string{ + filepath.Join(insidePath, "foo/goo/"), + filepath.Join(insidePath, "foo/bar/baz/"), + filepath.Join(insidePath, "foo/beta/gamma/"), + outsidePath, + } + for _, path := range paths { + err = os.MkdirAll(path, os.ModeDir) + require.Nil(t, err) + } + + // create a temp file on the outside and make a symlink from the inside to the outside + outsideFile := filepath.Join(outsidePath, "target") + insideFile := filepath.Join(insidePath, "source") + + file, err := os.Create(outsideFile) + require.Nil(t, err) + defer file.Close() + err = os.Symlink(outsideFile, insideFile) + require.Nil(t, err) + + rmdirContentsRequest := &filesystem.RmdirContentsRequest{ + Path: insidePath, + } + _, err = client.RmdirContents(context.Background(), rmdirContentsRequest) + require.Nil(t, err) + + // the inside path should exist + exists, err := pathExists(insidePath) + require.Nil(t, err) + assert.True(t, exists, "The path shouldn't exist") + // it should have no children + children, err := ioutil.ReadDir(insidePath) + require.Nil(t, err) + assert.True(t, len(children) == 0, "The RmdirContents path to delete shouldn't have children") + // the symlink target should exist + _, err = os.Open(outsideFile) + if errors.Is(err, os.ErrNotExist) { + // the file should exist but it was deleted! + t.Fatalf("File outsideFile=%s doesn't exist", outsideFile) + } + }) } func TestFilesystemAPIGroup(t *testing.T) { diff --git a/integrationtests/utils.go b/integrationtests/utils.go index 6eadc1df..7a516992 100644 --- a/integrationtests/utils.go +++ b/integrationtests/utils.go @@ -273,3 +273,14 @@ func diskInit(t *testing.T) (*VirtualHardDisk, func()) { return vhd, cleanup } + +func pathExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} diff --git a/pkg/filesystem/api/api.go b/pkg/filesystem/api/api.go new file mode 100644 index 00000000..95a492bb --- /dev/null +++ b/pkg/filesystem/api/api.go @@ -0,0 +1,146 @@ +package api + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kubernetes-csi/csi-proxy/pkg/utils" +) + +// Implements the Filesystem OS API calls. All code here should be very simple +// pass-through to the OS APIs. Any logic around the APIs should go in +// pkg/filesystem/filesystem.go so that logic can be easily unit-tested +// without requiring specific OS environments. + +// API is the exposed Filesystem API +type API interface { + PathExists(path string) (bool, error) + PathValid(path string) (bool, error) + Mkdir(path string) error + Rmdir(path string, force bool) error + RmdirContents(path string) error + CreateSymlink(oldname string, newname string) error + IsSymlink(path string) (bool, error) +} + +type filesystemAPI struct{} + +// check that filesystemAPI implements API +var _ API = &filesystemAPI{} + +func New() API { + return filesystemAPI{} +} + +func pathExists(path string) (bool, error) { + _, err := os.Lstat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func (filesystemAPI) PathExists(path string) (bool, error) { + return pathExists(path) +} + +func pathValid(path string) (bool, error) { + cmd := `Test-Path $Env:remotepath` + cmdEnv := fmt.Sprintf("remotepath=%s", path) + output, err := utils.RunPowershellCmd(cmd, cmdEnv) + if err != nil { + return false, fmt.Errorf("returned output: %s, error: %v", string(output), err) + } + + return strings.HasPrefix(strings.ToLower(string(output)), "true"), nil +} + +// PathValid determines whether all elements of a path exist +// +// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/test-path?view=powershell-7 +// +// for a remote path, determines whether connection is ok +// +// e.g. in a SMB server connection, if password is changed, connection will be lost, this func will return false +func (filesystemAPI) PathValid(path string) (bool, error) { + return pathValid(path) +} + +// Mkdir makes a dir with `os.MkdirAll`. +func (filesystemAPI) Mkdir(path string) error { + return os.MkdirAll(path, 0755) +} + +// Rmdir removes a dir with `os.Remove`, if force is true then `os.RemoveAll` is used instead. +func (filesystemAPI) Rmdir(path string, force bool) error { + if force { + return os.RemoveAll(path) + } + return os.Remove(path) +} + +// RmdirContents removes the contents of a directory with `os.RemoveAll` +func (filesystemAPI) RmdirContents(path string) error { + dir, err := os.Open(path) + if err != nil { + return err + } + defer dir.Close() + + files, err := dir.Readdirnames(-1) + if err != nil { + return err + } + for _, file := range files { + candidatePath := filepath.Join(path, file) + err = os.RemoveAll(candidatePath) + if err != nil { + return err + } + } + + return nil +} + +// CreateSymlink creates newname as a symbolic link to oldname. +func (filesystemAPI) CreateSymlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + +// IsSymlink - returns true if tgt is a mount point. +// A path is considered a mount point if: +// - directory exists and +// - it is a soft link and +// - the target path of the link exists. +// +// If tgt path does not exist, it returns an error +// if tgt path exists, but the source path tgt points to does not exist, it returns false without error. +func (filesystemAPI) IsSymlink(tgt string) (bool, error) { + // This code is similar to k8s.io/kubernetes/pkg/util/mount except the pathExists usage. + // Also in a remote call environment the os error cannot be passed directly back, hence the callers + // are expected to perform the isExists check before calling this call in CSI proxy. + stat, err := os.Lstat(tgt) + if err != nil { + return false, err + } + + // If its a link and it points to an existing file then its a mount point. + if stat.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(tgt) + if err != nil { + return false, fmt.Errorf("readlink error: %v", err) + } + exists, err := pathExists(target) + if err != nil { + return false, err + } + return exists, nil + } + + return false, nil +} diff --git a/pkg/filesystem/api/api_test.go b/pkg/filesystem/api/api_test.go new file mode 100644 index 00000000..948a4ca4 --- /dev/null +++ b/pkg/filesystem/api/api_test.go @@ -0,0 +1,37 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPathValid(t *testing.T) { + tests := []struct { + remotepath string + expectedResult bool + expectError bool + }{ + { + "c:", + true, + false, + }, + { + "invalid-path", + false, + false, + }, + } + + for _, test := range tests { + result, err := pathValid(test.remotepath) + assert.Equal(t, result, test.expectedResult, "Expect result not equal with pathValid(%s) return: %q, expected: %q, error: %v", + test.remotepath, result, test.expectedResult, err) + if test.expectError { + assert.NotNil(t, err, "Expect error during pathValid(%s)", test.remotepath) + } else { + assert.Nil(t, err, "Expect error is nil during pathValid(%s)", test.remotepath) + } + } +} diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go new file mode 100644 index 00000000..fdddec6b --- /dev/null +++ b/pkg/filesystem/filesystem.go @@ -0,0 +1,166 @@ +package filesystem + +import ( + "context" + + filesystemapi "github.com/kubernetes-csi/csi-proxy/pkg/filesystem/api" + "k8s.io/klog/v2" +) + +type Filesystem struct { + hostAPI filesystemapi.API +} + +type Interface interface { + CreateSymlink(context.Context, *CreateSymlinkRequest) (*CreateSymlinkResponse, error) + IsMountPoint(context.Context, *IsMountPointRequest) (*IsMountPointResponse, error) + IsSymlink(context.Context, *IsSymlinkRequest) (*IsSymlinkResponse, error) + LinkPath(context.Context, *LinkPathRequest) (*LinkPathResponse, error) + Mkdir(context.Context, *MkdirRequest) (*MkdirResponse, error) + PathExists(context.Context, *PathExistsRequest) (*PathExistsResponse, error) + PathValid(context.Context, *PathValidRequest) (*PathValidResponse, error) + Rmdir(context.Context, *RmdirRequest) (*RmdirResponse, error) + RmdirContents(context.Context, *RmdirContentsRequest) (*RmdirContentsResponse, error) +} + +// check that Filesystem implements Interface +var _ Interface = &Filesystem{} + +func New(hostAPI filesystemapi.API) (*Filesystem, error) { + return &Filesystem{ + hostAPI: hostAPI, + }, nil +} + +// PathExists checks if the given path exists on the host. +func (f *Filesystem) PathExists(ctx context.Context, request *PathExistsRequest) (*PathExistsResponse, error) { + klog.V(2).Infof("Request: PathExists with path=%q", request.Path) + err := ValidatePathWindows(request.Path) + if err != nil { + klog.Errorf("failed validatePathWindows %v", err) + return nil, err + } + exists, err := f.hostAPI.PathExists(request.Path) + if err != nil { + klog.Errorf("failed check PathExists %v", err) + return nil, err + } + return &PathExistsResponse{ + Exists: exists, + }, err +} + +// PathValid checks if the given path is accessible. +func (f *Filesystem) PathValid(ctx context.Context, request *PathValidRequest) (*PathValidResponse, error) { + klog.V(2).Infof("Request: PathValid with path %q", request.Path) + valid, err := f.hostAPI.PathValid(request.Path) + return &PathValidResponse{ + Valid: valid, + }, err +} + +func (f *Filesystem) Mkdir(ctx context.Context, request *MkdirRequest) (*MkdirResponse, error) { + klog.V(2).Infof("Request: Mkdir with path=%q", request.Path) + err := ValidatePathWindows(request.Path) + if err != nil { + klog.Errorf("failed validatePathWindows %v", err) + return nil, err + } + err = f.hostAPI.Mkdir(request.Path) + if err != nil { + klog.Errorf("failed Mkdir %v", err) + return nil, err + } + + return &MkdirResponse{}, err +} + +func (f *Filesystem) Rmdir(ctx context.Context, request *RmdirRequest) (*RmdirResponse, error) { + klog.V(2).Infof("Request: Rmdir with path=%q", request.Path) + err := ValidatePathWindows(request.Path) + if err != nil { + klog.Errorf("failed validatePathWindows %v", err) + return nil, err + } + err = f.hostAPI.Rmdir(request.Path, request.Force) + if err != nil { + klog.Errorf("failed Rmdir %v", err) + return nil, err + } + return nil, err +} + +func (f *Filesystem) RmdirContents(ctx context.Context, request *RmdirContentsRequest) (*RmdirContentsResponse, error) { + klog.V(2).Infof("Request: RmdirContents with path=%q", request.Path) + err := ValidatePathWindows(request.Path) + if err != nil { + klog.Errorf("failed validatePathWindows %v", err) + return nil, err + } + err = f.hostAPI.RmdirContents(request.Path) + if err != nil { + klog.Errorf("failed RmdirContents %v", err) + return nil, err + } + return nil, err +} + +func (f *Filesystem) LinkPath(ctx context.Context, request *LinkPathRequest) (*LinkPathResponse, error) { + klog.V(2).Infof("Request: LinkPath with targetPath=%q sourcePath=%q", request.TargetPath, request.SourcePath) + createSymlinkRequest := &CreateSymlinkRequest{ + SourcePath: request.SourcePath, + TargetPath: request.TargetPath, + } + if _, err := f.CreateSymlink(ctx, createSymlinkRequest); err != nil { + klog.Errorf("Failed to forward to CreateSymlink: %v", err) + return nil, err + } + return &LinkPathResponse{}, nil +} + +func (f *Filesystem) CreateSymlink(ctx context.Context, request *CreateSymlinkRequest) (*CreateSymlinkResponse, error) { + klog.V(2).Infof("Request: CreateSymlink with targetPath=%q sourcePath=%q", request.TargetPath, request.SourcePath) + err := ValidatePathWindows(request.TargetPath) + if err != nil { + klog.Errorf("failed validatePathWindows for target path %v", err) + return nil, err + } + err = ValidatePathWindows(request.SourcePath) + if err != nil { + klog.Errorf("failed validatePathWindows for source path %v", err) + return nil, err + } + err = f.hostAPI.CreateSymlink(request.SourcePath, request.TargetPath) + if err != nil { + klog.Errorf("failed CreateSymlink: %v", err) + return nil, err + } + return &CreateSymlinkResponse{}, nil +} + +func (f *Filesystem) IsMountPoint(ctx context.Context, request *IsMountPointRequest) (*IsMountPointResponse, error) { + klog.V(2).Infof("Request: IsMountPoint with path=%q", request.Path) + isSymlinkRequest := &IsSymlinkRequest{ + Path: request.Path, + } + isSymlinkResponse, err := f.IsSymlink(ctx, isSymlinkRequest) + if err != nil { + klog.Errorf("Failed to forward to IsSymlink: %v", err) + return nil, err + } + return &IsMountPointResponse{ + IsMountPoint: isSymlinkResponse.IsSymlink, + }, nil +} + +func (f *Filesystem) IsSymlink(ctx context.Context, request *IsSymlinkRequest) (*IsSymlinkResponse, error) { + klog.V(2).Infof("Request: IsSymlink with path=%q", request.Path) + isSymlink, err := f.hostAPI.IsSymlink(request.Path) + if err != nil { + klog.Errorf("failed IsSymlink %v", err) + return nil, err + } + return &IsSymlinkResponse{ + IsSymlink: isSymlink, + }, nil +} diff --git a/pkg/filesystem/filesystem_test.go b/pkg/filesystem/filesystem_test.go new file mode 100644 index 00000000..06b6150e --- /dev/null +++ b/pkg/filesystem/filesystem_test.go @@ -0,0 +1,189 @@ +package filesystem + +import ( + "context" + "testing" + + fsapi "github.com/kubernetes-csi/csi-proxy/pkg/filesystem/api" +) + +type fakeFileSystemAPI struct{} + +var _ fsapi.API = &fakeFileSystemAPI{} + +func (fakeFileSystemAPI) PathExists(path string) (bool, error) { + return true, nil +} +func (fakeFileSystemAPI) PathValid(path string) (bool, error) { + return true, nil +} +func (fakeFileSystemAPI) Mkdir(path string) error { + return nil +} +func (fakeFileSystemAPI) Rmdir(path string, force bool) error { + return nil +} +func (fakeFileSystemAPI) RmdirContents(path string) error { + return nil +} +func (fakeFileSystemAPI) CreateSymlink(tgt string, src string) error { + return nil +} + +func (fakeFileSystemAPI) IsSymlink(path string) (bool, error) { + return true, nil +} + +func TestMkdirWindows(t *testing.T) { + testCases := []struct { + name string + path string + expectError bool + }{ + { + name: "path inside pod context with pod context set", + path: `C:\var\lib\kubelet\pods\pv1`, + expectError: false, + }, + { + name: "path inside plugin context with plugin context set", + path: `C:\var\lib\kubelet\plugins\pv1`, + expectError: false, + }, + { + name: "path with invalid character `:` beyond drive letter prefix", + path: `C:\var\lib\kubelet\plugins\csi-plugin\pv1:foo`, + expectError: true, + }, + { + name: "path with invalid character `/`", + path: `C:\var\lib\kubelet\pods\pv1/foo`, + expectError: true, + }, + { + name: "path with invalid character `*`", + path: `C:\var\lib\kubelet\plugins\csi-plugin\pv1*foo`, + expectError: true, + }, + { + name: "path with invalid character `?`", + path: `C:\var\lib\kubelet\pods\pv1?foo`, + expectError: true, + }, + { + name: "path with invalid character `|`", + path: `C:\var\lib\kubelet\plugins\csi-plugin|pv1\foo`, + expectError: true, + }, + { + name: "path with invalid characters `..`", + path: `C:\var\lib\kubelet\pods\pv1\..\..\..\system32`, + expectError: true, + }, + { + name: "path with invalid prefix `\\`", + path: `\\csi-plugin\..\..\..\system32`, + expectError: true, + }, + { + name: "relative path", + path: `pv1\foo`, + expectError: true, + }, + } + client, err := New(&fakeFileSystemAPI{}) + if err != nil { + t.Fatalf("FileSystem Server could not be initialized for testing: %v", err) + } + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + req := &MkdirRequest{ + Path: tc.path, + } + _, err := client.Mkdir(context.TODO(), req) + if tc.expectError && err == nil { + t.Errorf("Expected error but Mkdir returned a nil error") + } + if !tc.expectError && err != nil { + t.Errorf("Expected no errors but Mkdir returned error: %v", err) + } + } +} + +func TestRmdirWindows(t *testing.T) { + testCases := []struct { + name string + path string + expectError bool + force bool + }{ + { + name: "path inside pod context with pod context set", + path: `C:\var\lib\kubelet\pods\pv1`, + expectError: false, + }, + { + name: "path inside plugin context with plugin context set", + path: `C:\var\lib\kubelet\plugins\pv1`, + expectError: false, + }, + { + name: "path with invalid character `:` beyond drive letter prefix", + path: `C:\var\lib\kubelet\plugins\csi-plugin\pv1:foo`, + expectError: true, + }, + { + name: "path with invalid character `/`", + path: `C:\var\lib\kubelet\pods\pv1/foo`, + expectError: true, + }, + { + name: "path with invalid character `*`", + path: `C:\var\lib\kubelet\plugins\csi-plugin\pv1*foo`, + expectError: true, + }, + { + name: "path with invalid character `?`", + path: `C:\var\lib\kubelet\pods\pv1?foo`, + expectError: true, + }, + { + name: "path with invalid character `|`", + path: `C:\var\lib\kubelet\plugins\csi-plugin|pv1\foo`, + expectError: true, + }, + { + name: "path with invalid characters `..`", + path: `C:\var\lib\kubelet\pods\pv1\..\..\..\system32`, + expectError: true, + }, + { + name: "path with invalid prefix `\\`", + path: `\\csi-plugin\..\..\..\system32`, + expectError: true, + }, + { + name: "relative path", + path: `pv1\foo`, + expectError: true, + }, + } + client, err := New(&fakeFileSystemAPI{}) + if err != nil { + t.Fatalf("FileSystem Server could not be initialized for testing: %v", err) + } + for _, tc := range testCases { + t.Logf("test case: %s", tc.name) + req := &RmdirRequest{ + Path: tc.path, + Force: tc.force, + } + _, err := client.Rmdir(context.TODO(), req) + if tc.expectError && err == nil { + t.Errorf("Expected error but Rmdir returned a nil error") + } + if !tc.expectError && err != nil { + t.Errorf("Expected no errors but Rmdir returned error: %v", err) + } + } +} diff --git a/pkg/filesystem/types.go b/pkg/filesystem/types.go new file mode 100644 index 00000000..d00509a6 --- /dev/null +++ b/pkg/filesystem/types.go @@ -0,0 +1,184 @@ +package filesystem + +// PathExistsRequest is the internal representation of requests to the PathExists endpoint. +type PathExistsRequest struct { + // The path whose existence we want to check in the host's filesystem + Path string +} + +// PathExistsResponse is the internal representation of responses from the PathExists endpoint. +type PathExistsResponse struct { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + Exists bool +} + +// PathValidRequest is the internal representation of requests to the PathValid endpoint. +type PathValidRequest struct { + // The path whose validity we want to check in the host's filesystem + Path string +} + +// PathValidResponse is the internal representation of responses from the PathValid endpoint. +type PathValidResponse struct { + // Indicates whether the path in PathValidRequest is a valid path + Valid bool +} + +type MkdirRequest struct { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist on host filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + Path string +} + +type MkdirResponse struct { +} + +type RmdirRequest struct { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + Path string + // Force remove all contents under path (if any). + Force bool +} + +type RmdirResponse struct { +} + +type RmdirContentsRequest struct { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + Path string +} + +type RmdirContentsResponse struct { +} + +type CreateSymlinkRequest struct { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + SourcePath string + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + TargetPath string +} + +type CreateSymlinkResponse struct { +} + +type IsSymlinkRequest struct { + Path string +} + +type IsSymlinkResponse struct { + IsSymlink bool +} + +// Compatibility for pre v1beta2 APIs + +type LinkPathRequest struct { + // The path where the symlink is created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + SourcePath string + // Target path in the host's filesystem used for the symlink creation. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + TargetPath string +} + +type LinkPathResponse struct { +} + +type IsMountPointRequest struct { + Path string +} + +type IsMountPointResponse struct { + IsMountPoint bool +} diff --git a/pkg/filesystem/utils.go b/pkg/filesystem/utils.go new file mode 100644 index 00000000..85756afc --- /dev/null +++ b/pkg/filesystem/utils.go @@ -0,0 +1,69 @@ +package filesystem + +import ( + "fmt" + "regexp" + "strings" + + "github.com/kubernetes-csi/csi-proxy/pkg/utils" +) + +var invalidPathCharsRegexWindows = regexp.MustCompile(`["/\:\?\*|]`) +var absPathRegexWindows = regexp.MustCompile(`^[a-zA-Z]:\\`) + +func isUNCPathWindows(path string) bool { + // check for UNC/pipe prefixes like "\\" + if len(path) < 2 { + return false + } + if path[0] == '\\' && path[1] == '\\' { + return true + } + return false +} + +func isAbsWindows(path string) bool { + // for Windows check for C:\\.. prefix only + // UNC prefixes of the form \\ are not considered + // absolute in the context of CSI proxy + return absPathRegexWindows.MatchString(path) +} + +func containsInvalidCharactersWindows(path string) bool { + if isAbsWindows(path) { + path = path[3:] + } + if invalidPathCharsRegexWindows.MatchString(path) { + return true + } + if strings.Contains(path, `..`) { + return true + } + return false +} + +func ValidatePathWindows(path string) error { + pathlen := len(path) + + if pathlen > utils.MaxPathLengthWindows { + return fmt.Errorf("path length %d exceeds maximum characters: %d", pathlen, utils.MaxPathLengthWindows) + } + + if pathlen > 0 && (path[0] == '\\') { + return fmt.Errorf("invalid character \\ at beginning of path: %s", path) + } + + if isUNCPathWindows(path) { + return fmt.Errorf("unsupported UNC path prefix: %s", path) + } + + if containsInvalidCharactersWindows(path) { + return fmt.Errorf("path contains invalid characters: %s", path) + } + + if !isAbsWindows(path) { + return fmt.Errorf("not an absolute Windows path: %s", path) + } + + return nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 87c50339..ec956958 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "os" "os/exec" @@ -10,6 +11,7 @@ import ( const MaxPathLengthWindows = 260 func RunPowershellCmd(command string, envs ...string) ([]byte, error) { + command = fmt.Sprintf("$global:ProgressPreference = 'SilentlyContinue'; %s", command) cmd := exec.Command("powershell", "-Mta", "-NoProfile", "-Command", command) cmd.Env = append(os.Environ(), envs...) klog.V(8).Infof("Executing command: %q", cmd.String()) diff --git a/scripts/utils.sh b/scripts/utils.sh index f9aac53d..fa33a0a2 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -57,11 +57,7 @@ run_csi_proxy_integration_tests() { "& { $ErrorActionPreference = \"Stop\"; Import-Module (Resolve-Path(\"utils.psm1\")); - Run-CSIProxyIntegrationTests -test_args \"--test.v --test.run TestAPIGroups\"; - Run-CSIProxyIntegrationTests -test_args \"--test.v --test.run TestFilesystemAPIGroup\"; - Run-CSIProxyIntegrationTests -test_args \"--test.v --test.run TestDiskAPIGroup\"; - Run-CSIProxyIntegrationTests -test_args \"--test.v --test.run TestVolumeAPIs\"; - Run-CSIProxyIntegrationTests -test_args \"--test.v --test.run TestSmbAPIGroup\"; + Run-CSIProxyIntegrationTests -test_args \"--test.v\"; }" EOF );