From bcc6818ef3de4a0bc30b98c20bd96c53a177759a Mon Sep 17 00:00:00 2001 From: Ji An Liu Date: Fri, 10 Feb 2023 00:48:59 +0000 Subject: [PATCH] support to enable windows process Signed-off-by: Ji An Liu --- pkg/azurefile/azurefile.go | 5 +- pkg/azurefile/fake_mounter.go | 2 +- pkg/azurefileplugin/main.go | 2 + .../safe_mounter_host_process_windows.go | 268 ++++++++++++++++++ pkg/mounter/safe_mounter_unix.go | 2 +- pkg/mounter/safe_mounter_unix_test.go | 5 +- pkg/mounter/safe_mounter_windows.go | 9 +- pkg/os/filesystem/filesystem.go | 251 ++++++++++++++++ pkg/os/filesystem/types.go | 143 ++++++++++ pkg/os/smb/smb.go | 99 +++++++ pkg/util/util.go | 3 +- 11 files changed, 782 insertions(+), 7 deletions(-) create mode 100644 pkg/mounter/safe_mounter_host_process_windows.go create mode 100644 pkg/os/filesystem/filesystem.go create mode 100644 pkg/os/filesystem/types.go create mode 100644 pkg/os/smb/smb.go diff --git a/pkg/azurefile/azurefile.go b/pkg/azurefile/azurefile.go index b888287019..d4c19d8cd5 100644 --- a/pkg/azurefile/azurefile.go +++ b/pkg/azurefile/azurefile.go @@ -197,6 +197,7 @@ type DriverOptions struct { FSGroupChangePolicy string KubeAPIQPS float64 KubeAPIBurst int + EnableWindowsHostProcess bool } // Driver implements all interfaces of CSI drivers @@ -216,6 +217,7 @@ type Driver struct { mountPermissions uint64 kubeAPIQPS float64 kubeAPIBurst int + enableWindowsHostProcess bool fileClient *azureFileClient mounter *mount.SafeFormatAndMount // lock per volume attach (only for vhd disk feature) @@ -261,6 +263,7 @@ func NewDriver(options *DriverOptions) *Driver { driver.fsGroupChangePolicy = options.FSGroupChangePolicy driver.kubeAPIQPS = options.KubeAPIQPS driver.kubeAPIBurst = options.KubeAPIBurst + driver.enableWindowsHostProcess = options.EnableWindowsHostProcess driver.volLockMap = newLockMap() driver.subnetLockMap = newLockMap() driver.volumeLocks = newVolumeLocks() @@ -310,7 +313,7 @@ func (d *Driver) Run(endpoint, kubeconfig string, testBool bool) { // todo: set backoff from cloud provider config d.fileClient = newAzureFileClient(&d.cloud.Environment, &retry.Backoff{Steps: 1}) - d.mounter, err = mounter.NewSafeMounter() + d.mounter, err = mounter.NewSafeMounter(d.enableWindowsHostProcess) if err != nil { klog.Fatalf("Failed to get safe mounter. Error: %v", err) } diff --git a/pkg/azurefile/fake_mounter.go b/pkg/azurefile/fake_mounter.go index 7652ca164f..730ae87022 100644 --- a/pkg/azurefile/fake_mounter.go +++ b/pkg/azurefile/fake_mounter.go @@ -74,7 +74,7 @@ func (f *fakeMounter) IsMountPoint(file string) (bool, error) { // NewFakeMounter fake mounter func NewFakeMounter() (*mount.SafeFormatAndMount, error) { if runtime.GOOS == "windows" { - return mounter.NewSafeMounter() + return mounter.NewSafeMounter(false) } return &mount.SafeFormatAndMount{ Interface: &fakeMounter{}, diff --git a/pkg/azurefileplugin/main.go b/pkg/azurefileplugin/main.go index 146ee02a40..55f7144b39 100644 --- a/pkg/azurefileplugin/main.go +++ b/pkg/azurefileplugin/main.go @@ -55,6 +55,7 @@ var ( kubeAPIQPS = flag.Float64("kube-api-qps", 25.0, "QPS to use while communicating with the kubernetes apiserver.") kubeAPIBurst = flag.Int("kube-api-burst", 50, "Burst to use while communicating with the kubernetes apiserver.") appendMountErrorHelpLink = flag.Bool("append-mount-error-help-link", true, "Whether to include a link for help with mount errors when a mount error occurs.") + enableWindowsHostProcess = flag.Bool("enable-windows-host-process", false, "enable windows host process") ) func main() { @@ -95,6 +96,7 @@ func handle() { AppendMountErrorHelpLink: *appendMountErrorHelpLink, KubeAPIQPS: *kubeAPIQPS, KubeAPIBurst: *kubeAPIBurst, + EnableWindowsHostProcess: *enableWindowsHostProcess, } driver := azurefile.NewDriver(&driverOptions) if driver == nil { diff --git a/pkg/mounter/safe_mounter_host_process_windows.go b/pkg/mounter/safe_mounter_host_process_windows.go new file mode 100644 index 0000000000..bacb57baaa --- /dev/null +++ b/pkg/mounter/safe_mounter_host_process_windows.go @@ -0,0 +1,268 @@ +//go:build windows +// +build windows + +/* +Copyright 2022 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 ( + "context" + "fmt" + "os" + filepath "path/filepath" + "strings" + + "k8s.io/klog/v2" + mount "k8s.io/mount-utils" + + "sigs.k8s.io/azurefile-csi-driver/pkg/os/filesystem" + "sigs.k8s.io/azurefile-csi-driver/pkg/os/smb" +) + +var _ CSIProxyMounter = &winMounter{} + +type winMounter struct{} + +func NewWinMounter() *winMounter { + return &winMounter{} +} + +func (mounter *winMounter) SMBMount(source, target, fsType string, mountOptions, sensitiveMountOptions []string) error { + klog.V(2).Infof("SMBMount: remote path: %s local path: %s", source, target) + + if len(mountOptions) == 0 || len(sensitiveMountOptions) == 0 { + return fmt.Errorf("empty mountOptions(len: %d) or sensitiveMountOptions(len: %d) is not allowed", len(mountOptions), len(sensitiveMountOptions)) + } + + parentDir := filepath.Dir(target) + parentExists, err := mounter.ExistsPath(parentDir) + if err != nil { + return fmt.Errorf("parent dir: %s exist check failed with err: %v", parentDir, err) + } + + if !parentExists { + klog.V(2).Infof("Parent directory %s does not exists. Creating the directory", parentDir) + if err := mounter.MakeDir(parentDir); err != nil { + return fmt.Errorf("create of parent dir: %s dailed with error: %v", parentDir, err) + } + } + + source = strings.Replace(source, "/", "\\", -1) + normalizedTarget := normalizeWindowsPath(target) + + klog.V(2).Infof("begin to mount %s on %s", source, normalizedTarget) + + remotePath := source + localPath := normalizedTarget + + if remotePath == "" { + klog.Errorf("remote path is empty") + return fmt.Errorf("remote path is empty") + } + + isMapped, err := smb.IsSmbMapped(remotePath) + if err != nil { + isMapped = false + } + + if isMapped { + valid, err := filesystem.PathValid(context.Background(), remotePath) + if err != nil { + klog.Warningf("PathValid(%s) failed with %v, ignore error", remotePath, err) + } + + if !valid { + klog.V(4).Infof("RemotePath %s is not valid, removing now", remotePath) + err := smb.RemoveSmbGlobalMapping(remotePath) + if err != nil { + klog.Errorf("RemoveSmbGlobalMapping(%s) failed with %v", remotePath, err) + return err + } + isMapped = false + } + } + + if !isMapped { + klog.V(4).Infof("Remote %s not mapped. Mapping now!", remotePath) + username := mountOptions[0] + password := sensitiveMountOptions[0] + err := smb.NewSmbGlobalMapping(remotePath, username, password) + if err != nil { + klog.Errorf("failed NewSmbGlobalMapping %v", err) + return err + } + } + + if len(localPath) != 0 { + err = filesystem.ValidatePathWindows(localPath) + if err != nil { + klog.Errorf("failed validate plugin path %v", err) + return err + } + err = smb.NewSmbLink(remotePath, localPath) + if err != nil { + klog.Errorf("failed NewSmbLink %v", err) + return fmt.Errorf("creating link %s to %s failed with error: %v", localPath, remotePath, err) + } + } + + klog.V(2).Infof("mount %s on %s successfully", source, normalizedTarget) + + return nil +} + +func (mounter *winMounter) SMBUnmount(target string) error { + klog.V(4).Infof("SMBUnmount: local path: %s", target) + return mounter.Rmdir(target) +} + +// Mount just creates a soft link at target pointing to source. +func (mounter *winMounter) Mount(source string, target string, fstype string, options []string) error { + klog.V(4).Infof("Mount: old name: %s. new name: %s", source, target) + // Mount is called after the format is done. + // TODO: Confirm that fstype is empty. + linkRequest := &filesystem.LinkPathRequest{ + SourcePath: normalizeWindowsPath(source), + TargetPath: normalizeWindowsPath(target), + } + if err := filesystem.LinkPath(context.Background(), linkRequest); err != nil { + return err + } + return nil +} + +// Rmdir - delete the given directory +func (mounter *winMounter) Rmdir(path string) error { + klog.V(4).Infof("Remove directory: %s", path) + rmdirRequest := &filesystem.RmdirRequest{ + Path: normalizeWindowsPath(path), + Force: true, + } + if err := filesystem.Rmdir(context.Background(), rmdirRequest); err != nil { + return err + } + return nil +} + +// Unmount - Removes the directory - equivalent to unmount on Linux. +func (mounter *winMounter) Unmount(target string) error { + klog.V(4).Infof("Unmount: %s", target) + return mounter.Rmdir(target) +} + +func (mounter *winMounter) List() ([]mount.MountPoint, error) { + return []mount.MountPoint{}, fmt.Errorf("List not implemented for CSIProxyMounter") +} + +func (mounter *winMounter) IsMountPoint(file string) (bool, error) { + isNotMnt, err := mounter.IsLikelyNotMountPoint(file) + if err != nil { + return false, err + } + return !isNotMnt, nil +} + +func (mounter *winMounter) IsMountPointMatch(mp mount.MountPoint, dir string) bool { + return mp.Path == dir +} + +// IsLikelyMountPoint - If the directory does not exists, the function will return os.ErrNotExist error. +// If the path exists, will check if its a link, if its a link then existence of target path is checked. +func (mounter *winMounter) IsLikelyNotMountPoint(path string) (bool, error) { + klog.V(4).Infof("IsLikelyNotMountPoint: %s", path) + isExists, err := mounter.ExistsPath(path) + if err != nil { + return false, err + } + if !isExists { + return true, os.ErrNotExist + } + + response, err := filesystem.IsMountPoint(context.Background(), + &filesystem.IsMountPointRequest{ + Path: normalizeWindowsPath(path), + }) + if err != nil { + return false, err + } + return !response, nil +} + +// MakeDir - Creates a directory. +// Currently the make dir is only used from the staging code path, hence we call it +// with Plugin context.. +func (mounter *winMounter) MakeDir(path string) error { + klog.V(4).Infof("Make directory: %s", path) + mkdirReq := &filesystem.MkdirRequest{ + Path: normalizeWindowsPath(path), + } + if err := filesystem.Mkdir(context.Background(), mkdirReq); err != nil { + return err + } + + return nil +} + +// ExistsPath - Checks if a path exists. Unlike util ExistsPath, this call does not perform follow link. +func (mounter *winMounter) ExistsPath(path string) (bool, error) { + klog.V(4).Infof("Exists path: %s", path) + return filesystem.PathExists(context.Background(), + &filesystem.PathExistsRequest{ + Path: normalizeWindowsPath(path), + }) +} + +func (mounter *winMounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { + return fmt.Errorf("MountSensitive not implemented for winMounter") +} + +func (mounter *winMounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { + return fmt.Errorf("MountSensitiveWithoutSystemd not implemented for winMounter") +} + +func (mounter *winMounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { + return mounter.MountSensitive(source, target, fstype, options, sensitiveOptions /* sensitiveOptions */) +} + +func (mounter *winMounter) GetMountRefs(pathname string) ([]string, error) { + return []string{}, fmt.Errorf("GetMountRefs not implemented for winMounter") +} + +func (mounter *winMounter) EvalHostSymlinks(pathname string) (string, error) { + return "", fmt.Errorf("EvalHostSymlinks not implemented for winMounter") +} + +func (mounter *winMounter) GetFSGroup(pathname string) (int64, error) { + return -1, fmt.Errorf("GetFSGroup not implemented for winMounter") +} + +func (mounter *winMounter) GetSELinuxSupport(pathname string) (bool, error) { + return false, fmt.Errorf("GetSELinuxSupport not implemented for winMounter") +} + +func (mounter *winMounter) GetMode(pathname string) (os.FileMode, error) { + return 0, fmt.Errorf("GetMode not implemented for winMounter") +} + +// GetAPIVersions returns the versions of the client APIs this mounter is using. +func (mounter *winMounter) GetAPIVersions() string { + return "" +} + +func (mounter *winMounter) CanSafelySkipMountPointCheck() bool { + return false +} diff --git a/pkg/mounter/safe_mounter_unix.go b/pkg/mounter/safe_mounter_unix.go index d5ee4d14a8..38b94b2a0c 100644 --- a/pkg/mounter/safe_mounter_unix.go +++ b/pkg/mounter/safe_mounter_unix.go @@ -24,7 +24,7 @@ import ( utilexec "k8s.io/utils/exec" ) -func NewSafeMounter() (*mount.SafeFormatAndMount, error) { +func NewSafeMounter(enableWindowsHostProcess bool) (*mount.SafeFormatAndMount, error) { return &mount.SafeFormatAndMount{ Interface: mount.New(""), Exec: utilexec.New(), diff --git a/pkg/mounter/safe_mounter_unix_test.go b/pkg/mounter/safe_mounter_unix_test.go index 9acf46780d..913d5d5693 100644 --- a/pkg/mounter/safe_mounter_unix_test.go +++ b/pkg/mounter/safe_mounter_unix_test.go @@ -17,12 +17,13 @@ limitations under the License. package mounter import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestNewSafeMounter(t *testing.T) { - resp, err := NewSafeMounter() + resp, err := NewSafeMounter(false) assert.NotNil(t, resp) assert.Nil(t, err) } diff --git a/pkg/mounter/safe_mounter_windows.go b/pkg/mounter/safe_mounter_windows.go index 61d76b0156..6c99c9a027 100644 --- a/pkg/mounter/safe_mounter_windows.go +++ b/pkg/mounter/safe_mounter_windows.go @@ -291,7 +291,14 @@ func NewCSIProxyMounter() (*csiProxyMounter, error) { }, nil } -func NewSafeMounter() (*mount.SafeFormatAndMount, error) { +func NewSafeMounter(enableWindowsHostProcess bool) (*mount.SafeFormatAndMount, error) { + if enableWindowsHostProcess { + klog.V(2).Infof("using windows host process mounter") + return &mount.SafeFormatAndMount{ + Interface: NewWinMounter(), + Exec: utilexec.New(), + }, nil + } csiProxyMounter, err := NewCSIProxyMounter() if err == nil { klog.V(2).Infof("using CSIProxyMounterV1, %s", csiProxyMounter.GetAPIVersions()) diff --git a/pkg/os/filesystem/filesystem.go b/pkg/os/filesystem/filesystem.go new file mode 100644 index 0000000000..0db89d8552 --- /dev/null +++ b/pkg/os/filesystem/filesystem.go @@ -0,0 +1,251 @@ +/* +Copyright 2023 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 filesystem + +import ( + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + "k8s.io/klog/v2" + "sigs.k8s.io/azurefile-csi-driver/pkg/util" +) + +var invalidPathCharsRegexWindows = regexp.MustCompile(`["/\:\?\*|]`) +var absPathRegexWindows = regexp.MustCompile(`^[a-zA-Z]:\\`) + +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 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 + return absPathRegexWindows.MatchString(path) +} + +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 +} + +// PathExists checks if the given path exists on the host. +func PathExists(ctx context.Context, request *PathExistsRequest) (bool, 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 false, err + } + exists, err := pathExists(request.Path) + if err != nil { + klog.Errorf("failed check PathExists %v", err) + } + return exists, err +} + +func PathValid(ctx context.Context, path string) (bool, error) { + cmd := exec.Command("powershell", "/c", `Test-Path $Env:remotepath`) + cmd.Env = append(os.Environ(), fmt.Sprintf("remotepath=%s", path)) + output, err := cmd.CombinedOutput() + if err != nil { + return false, fmt.Errorf("returned output: %s, error: %v", string(output), err) + } + + return strings.HasPrefix(strings.ToLower(string(output)), "true"), nil +} + +func ValidatePathWindows(path string) error { + prefix := `C:\var\lib\kubelet` + + pathlen := len(path) + + if pathlen > util.MaxPathLengthWindows { + return fmt.Errorf("path length %d exceeds maximum characters: %d", pathlen, util.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) + } + + if !strings.HasPrefix(strings.ToLower(path), strings.ToLower(prefix)) { + return fmt.Errorf("path: %s is not within context path: %s", path, prefix) + } + + return nil +} + +func Mkdir(ctx context.Context, request *MkdirRequest) 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 err + } + err = os.MkdirAll(request.Path, 0755) + if err != nil { + klog.Errorf("failed Mkdir %v", err) + return err + } + + return err +} + +func Rmdir(ctx context.Context, request *RmdirRequest) 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 err + } + + if request.Force { + err = os.RemoveAll(request.Path) + } else { + err = os.Remove(request.Path) + } + if err != nil { + klog.Errorf("failed Rmdir %v", err) + return err + } + + return err +} +func LinkPath(ctx context.Context, request *LinkPathRequest) 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 := CreateSymlink(ctx, createSymlinkRequest); err != nil { + klog.Errorf("Failed to forward to CreateSymlink: %v", err) + return err + } + return nil +} + +func CreateSymlink(ctx context.Context, request *CreateSymlinkRequest) 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 err + } + err = ValidatePathWindows(request.SourcePath) + if err != nil { + klog.Errorf("failed validatePathWindows for source path %v", err) + return err + } + err = os.Symlink(request.SourcePath, request.TargetPath) + if err != nil { + klog.Errorf("failed CreateSymlink: %v", err) + return err + } + return nil +} + +func IsMountPoint(ctx context.Context, request *IsMountPointRequest) (bool, error) { + klog.V(2).Infof("Request: IsMountPoint with path=%q", request.Path) + isSymlinkRequest := &IsSymlinkRequest{ + Path: request.Path, + } + isSymlink, err := IsSymlink(ctx, isSymlinkRequest) + if err != nil { + klog.Errorf("Failed to forward to IsSymlink: %v", err) + } + return isSymlink, err +} + +func IsSymlink(ctx context.Context, request *IsSymlinkRequest) (bool, error) { + klog.V(2).Infof("Request: IsSymlink with path=%q", request.Path) + isSymlink, err := isSymlink(request.Path) + if err != nil { + klog.Errorf("failed IsSymlink %v", err) + } + return isSymlink, err +} + +// 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 isSymlink(tgt string) (bool, error) { + // This code is similar to k8s.io/kubernetes/pkg/util/mount except the pathExists usage. + 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/os/filesystem/types.go b/pkg/os/filesystem/types.go new file mode 100644 index 0000000000..208fe50f25 --- /dev/null +++ b/pkg/os/filesystem/types.go @@ -0,0 +1,143 @@ +/* +Copyright 2023 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 filesystem + +type PathExistsRequest struct { + // The path whose existence we want to check in the host's filesystem + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` +} + +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 in the host's 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 `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` +} + +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 `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // Force remove all contents under path (if any). + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` +} + +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. + // The path prefix 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 `protobuf:"bytes,1,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"` + // 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. + // 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 `protobuf:"bytes,2,opt,name=target_path,json=targetPath,proto3" json:"target_path,omitempty"` +} + +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 `protobuf:"bytes,1,opt,name=source_path,json=sourcePath,proto3" json:"source_path,omitempty"` + // 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 `protobuf:"bytes,2,opt,name=target_path,json=targetPath,proto3" json:"target_path,omitempty"` +} + +type IsMountPointRequest struct { + // The path whose existence we want to check in the host's filesystem + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` +} + +type IsSymlinkRequest struct { + // The path whose existence as a symlink we want to check in the host's filesystem. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` +} diff --git a/pkg/os/smb/smb.go b/pkg/os/smb/smb.go new file mode 100644 index 0000000000..7aa7ecb831 --- /dev/null +++ b/pkg/os/smb/smb.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 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 smb + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func IsSmbMapped(remotePath string) (bool, error) { + cmdLine := fmt.Sprintf(`$(Get-SmbGlobalMapping -RemotePath $Env:smbremotepath -ErrorAction Stop).Status `) + cmd := exec.Command("powershell", "/c", cmdLine) + cmd.Env = append(os.Environ(), + fmt.Sprintf("smbremotepath=%s", remotePath)) + + out, err := cmd.CombinedOutput() + if err != nil { + return false, fmt.Errorf("error checking smb mapping. cmd %s, output: %s, err: %v", remotePath, string(out), err) + } + + if len(out) == 0 || !strings.EqualFold(strings.TrimSpace(string(out)), "OK") { + return false, nil + } + return true, nil +} + +// NewSmbLink - creates a directory symbolic link to the remote share. +// The os.Symlink was having issue for cases where the destination was an SMB share - the container +// runtime would complain stating "Access Denied". Because of this, we had to perform +// this operation with powershell commandlet creating an directory softlink. +// Since os.Symlink is currently being used in working code paths, no attempt is made in +// alpha to merge the paths. +func NewSmbLink(remotePath, localPath string) error { + + if !strings.HasSuffix(remotePath, "\\") { + // Golang has issues resolving paths mapped to file shares if they do not end in a trailing \ + // so add one if needed. + remotePath = remotePath + "\\" + } + + cmdLine := fmt.Sprintf(`New-Item -ItemType SymbolicLink $Env:smblocalPath -Target $Env:smbremotepath`) + cmd := exec.Command("powershell", "/c", cmdLine) + cmd.Env = append(os.Environ(), + fmt.Sprintf("smbremotepath=%s", remotePath), + fmt.Sprintf("smblocalpath=%s", localPath), + ) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error linking %s to %s. output: %s, err: %v", remotePath, localPath, string(output), err) + } + + return nil +} + +func NewSmbGlobalMapping(remotePath, username, password string) error { + + // use PowerShell Environment Variables to store user input string to prevent command line injection + // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 + cmdLine := fmt.Sprintf(`$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` + + `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` + + `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential`) + + cmd := exec.Command("powershell", "/c", cmdLine) + cmd.Env = append(os.Environ(), + fmt.Sprintf("smbuser=%s", username), + fmt.Sprintf("smbpassword=%s", password), + fmt.Sprintf("smbremotepath=%s", remotePath)) + + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("NewSmbGlobalMapping failed. output: %q, err: %v\n[debug]smbuser=%s,smbpassword=%s,smbremotepath=%s", string(output), err, username, password, remotePath) + } + + return nil +} + +func RemoveSmbGlobalMapping(remotePath string) error { + cmd := exec.Command("powershell", "/c", `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force`) + cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotePath)) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("UnmountSmbShare failed. output: %q, err: %v", string(output), err) + } + return nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go index aa906cea9a..65e331f9b3 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -17,7 +17,8 @@ limitations under the License. package util const ( - GiB = 1024 * 1024 * 1024 + GiB = 1024 * 1024 * 1024 + MaxPathLengthWindows = 260 ) // RoundUpBytes rounds up the volume size in bytes up to multiplications of GiB