diff --git a/Makefile b/Makefile index 36da33ee4c3..6002646be0e 100644 --- a/Makefile +++ b/Makefile @@ -102,11 +102,16 @@ e2e-test: .PHONY: e2e-bootstrap e2e-bootstrap: install-helm +ifdef WINDOWS_USE_HOST_PROCESS_CONTAINERS + (docker pull $(IMAGE_TAG) && docker pull $(IMAGE_TAG)-windows-hp) || make container-all push-manifest +else docker pull $(IMAGE_TAG) || make container-all push-manifest +endif ifdef TEST_WINDOWS helm upgrade csi-driver-smb charts/$(VERSION)/csi-driver-smb --namespace kube-system --wait --timeout=15m -v=5 --debug --install \ ${E2E_HELM_OPTIONS} \ --set windows.enabled=true \ + --set windows.useHostProcessContainers=${WINDOWS_USE_HOST_PROCESS_CONTAINERS} \ --set linux.enabled=false \ --set controller.replicas=1 \ --set controller.logLevel=6 \ @@ -162,6 +167,24 @@ container-windows: -t $(IMAGE_TAG)-windows-$(OSVERSION)-$(ARCH) --build-arg OSVERSION=$(OSVERSION) \ --provenance=false --sbom=false \ --build-arg ARCH=$(ARCH) -f ./cmd/smbplugin/Dockerfile.Windows . +# workaround: only build hostprocess image once +ifdef WINDOWS_USE_HOST_PROCESS_CONTAINERS +ifeq ($(OSVERSION),ltsc2022) + $(MAKE) container-windows-hostprocess + $(MAKE) container-windows-hostprocess-latest +endif +endif + +# Set --provenance=false to not generate the provenance (which is what causes the multi-platform index to be generated, even for a single platform). +.PHONY: container-windows-hostprocess +container-windows-hostprocess: + docker buildx build --pull --output=type=$(OUTPUT_TYPE) --platform="windows/$(ARCH)" --provenance=false --sbom=false \ + -t $(IMAGE_TAG)-windows-hp -f ./cmd/smbplugin/Dockerfile.WindowsHostProcess . + +.PHONY: container-windows-hostprocess-latest +container-windows-hostprocess-latest: + docker buildx build --pull --output=type=$(OUTPUT_TYPE) --platform="windows/$(ARCH)" --provenance=false --sbom=false \ + -t $(IMAGE_TAG_LATEST)-windows-hp -f ./cmd/smbplugin/Dockerfile.WindowsHostProcess . .PHONY: container-all container-all: smb-windows diff --git a/charts/latest/csi-driver-smb-v0.0.0.tgz b/charts/latest/csi-driver-smb-v0.0.0.tgz index e3104435105..87fe3c8272a 100644 Binary files a/charts/latest/csi-driver-smb-v0.0.0.tgz and b/charts/latest/csi-driver-smb-v0.0.0.tgz differ diff --git a/charts/latest/csi-driver-smb/templates/csi-smb-node-windows-hostprocess.yaml b/charts/latest/csi-driver-smb/templates/csi-smb-node-windows-hostprocess.yaml new file mode 100644 index 00000000000..055172cf8c3 --- /dev/null +++ b/charts/latest/csi-driver-smb/templates/csi-smb-node-windows-hostprocess.yaml @@ -0,0 +1,124 @@ +{{- if and .Values.windows.enabled .Values.windows.useHostProcessContainers }} +kind: DaemonSet +apiVersion: apps/v1 +metadata: + name: {{ .Values.windows.dsName }} + namespace: {{ .Release.Namespace }} +{{ include "smb.labels" . | indent 2 }} +spec: + updateStrategy: + rollingUpdate: + maxUnavailable: {{ .Values.node.maxUnavailable }} + type: RollingUpdate + selector: + matchLabels: + app: {{ .Values.windows.dsName }} + template: + metadata: +{{ include "smb.labels" . | indent 6 }} + app: {{ .Values.windows.dsName }} + spec: +{{- with .Values.windows.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} +{{- end }} + nodeSelector: + kubernetes.io/os: windows +{{- with .Values.node.nodeSelector }} +{{ toYaml . | indent 8 }} +{{- end }} +{{- with .Values.node.affinity }} + affinity: +{{ toYaml . | indent 8 }} +{{- end }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- if .Values.securityContext }} + securityContext: {{- toYaml .Values.securityContext | nindent 8 }} + {{- end }} + serviceAccountName: {{ .Values.serviceAccount.node }} + {{- include "smb.pullSecrets" . | indent 6 }} + securityContext: + seccompProfile: + type: RuntimeDefault + windowsOptions: + hostProcess: true + runAsUserName: "NT AUTHORITY\\SYSTEM" + hostNetwork: true + initContainers: + - name: init +{{- if hasPrefix "/" .Values.image.smb.repository }} + image: "{{ .Values.image.baseRepo }}{{ .Values.image.smb.repository }}:{{ .Values.image.smb.tag }}-windows-hp" +{{- else }} + image: "{{ .Values.image.smb.repository }}:{{ .Values.image.smb.tag }}-windows-hp" +{{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - "powershell.exe" + - "-c" + - "New-Item -ItemType Directory -Path C:\\var\\lib\\kubelet\\plugins\\{{ .Values.driver.name }}\\ -Force" + securityContext: + capabilities: + drop: + - ALL + containers: + - name: node-driver-registrar +{{- if hasPrefix "/" .Values.image.nodeDriverRegistrar.repository }} + image: "{{ .Values.image.baseRepo }}{{ .Values.image.nodeDriverRegistrar.repository }}:{{ .Values.image.nodeDriverRegistrar.tag }}" +{{- else }} + image: "{{ .Values.image.nodeDriverRegistrar.repository }}:{{ .Values.image.nodeDriverRegistrar.tag }}" +{{- end }} + command: + - "csi-node-driver-registrar.exe" + args: + - "--csi-address=$(CSI_ENDPOINT)" + - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)" + - "--plugin-registration-path=$(PLUGIN_REG_DIR)" + - "--v=2" + env: + - name: CSI_ENDPOINT + value: unix://{{ .Values.windows.kubelet }}\plugins\{{ .Values.driver.name }}\csi.sock + - name: DRIVER_REG_SOCK_PATH + value: C:\\var\\lib\\kubelet\\plugins\\{{ .Values.driver.name }}\\csi.sock + - name: PLUGIN_REG_DIR + value: C:\\var\\lib\\kubelet\\plugins_registry\\ + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + imagePullPolicy: {{ .Values.image.nodeDriverRegistrar.pullPolicy }} + resources: {{- toYaml .Values.windows.resources.nodeDriverRegistrar | nindent 12 }} + securityContext: + capabilities: + drop: + - ALL + - name: smb +{{- if hasPrefix "/" .Values.image.smb.repository }} + image: "{{ .Values.image.baseRepo }}{{ .Values.image.smb.repository }}:{{ .Values.image.smb.tag }}" +{{- else }} + image: "{{ .Values.image.smb.repository }}:{{ .Values.image.smb.tag }}" +{{- end }} + imagePullPolicy: {{ .Values.image.smb.pullPolicy }} + command: + - "azurefileplugin.exe" + args: + - "--v={{ .Values.node.logLevel }}" + - "--drivername={{ .Values.driver.name }}" + - --endpoint=$(CSI_ENDPOINT) + - --nodeid=$(KUBE_NODE_NAME) + - "--enable-get-volume-stats={{ .Values.feature.enableGetVolumeStats }}" + - "--remove-smb-mapping-during-unmount={{ .Values.windows.removeSMBMappingDuringUnmount }}" + env: + - name: CSI_ENDPOINT + value: unix://{{ .Values.windows.kubelet }}\plugins\{{ .Values.driver.name }}\csi.sock + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: {{- toYaml .Values.windows.resources.smb | nindent 12 }} + securityContext: + capabilities: + drop: + - ALL +{{- end -}} diff --git a/charts/latest/csi-driver-smb/values.yaml b/charts/latest/csi-driver-smb/values.yaml index aa828a0acb5..8ad83df9d17 100755 --- a/charts/latest/csi-driver-smb/values.yaml +++ b/charts/latest/csi-driver-smb/values.yaml @@ -131,6 +131,7 @@ linux: windows: enabled: false # Unless you already had csi proxy installed, windows.csiproxy.enabled=true is required + useHostProcessContainers: false dsName: csi-smb-node-win # daemonset name kubelet: 'C:\var\lib\kubelet' removeSMBMappingDuringUnmount: true diff --git a/cmd/smbplugin/Dockerfile.WindowsHostProcess b/cmd/smbplugin/Dockerfile.WindowsHostProcess new file mode 100644 index 00000000000..d8131b7dc66 --- /dev/null +++ b/cmd/smbplugin/Dockerfile.WindowsHostProcess @@ -0,0 +1,24 @@ +# 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. + +# these arguments come from BUILD_PLATFORMS used in release-tools +FROM mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 +LABEL description="CSI SMB plugin" + +ARG ARCH=amd64 +ARG binary=./_output/${ARCH}/smbplugin.exe +COPY ${binary} /smbplugin.exe +ENV PATH="C:\Windows\system32;C:\Windows;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;" +USER ContainerAdministrator +ENTRYPOINT ["/smbplugin.exe"] diff --git a/cmd/smbplugin/main.go b/cmd/smbplugin/main.go index efa04e0756f..cda6208f747 100644 --- a/cmd/smbplugin/main.go +++ b/cmd/smbplugin/main.go @@ -49,6 +49,7 @@ var ( krb5Prefix = flag.String("krb5-prefix", smb.DefaultKrb5CCName, "The prefix for kerberos cache") defaultOnDeletePolicy = flag.String("default-ondelete-policy", "", "default policy for deleting subdirectory when deleting a volume") removeArchivedVolumePath = flag.Bool("remove-archived-volume-path", true, "remove archived volume path in DeleteVolume") + enableWindowsHostProcess = flag.Bool("enable-windows-host-process", false, "enable windows host process") ) // exit is a separate function to handle program termination 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 00000000000..32331238ae5 --- /dev/null +++ b/pkg/mounter/safe_mounter_host_process_windows.go @@ -0,0 +1,247 @@ +//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" + + "github.com/kubernetes-csi/csi-driver-smb/pkg/os/filesystem" + "github.com/kubernetes-csi/csi-driver-smb/pkg/os/smb" +) + +var driverGlobalMountPath = "C:\\var\\lib\\kubelet\\plugins\\kubernetes.io\\csi\\file.csi.azure.com" + +var _ CSIProxyMounter = &winMounter{} + +type winMounter struct{} + +func NewWinMounter() *winMounter { + return &winMounter{} +} + +func (mounter *winMounter) SMBMount(source, target, fsType string, mountOptions, sensitiveMountOptions []string, _ 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 == "" { + 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.Warningf("RemotePath %s is not valid, removing now", remotePath) + if err := smb.RemoveSmbGlobalMapping(remotePath); err != nil { + klog.Errorf("RemoveSmbGlobalMapping(%s) failed with %v", remotePath, err) + return err + } + isMapped = false + } + } + + if !isMapped { + klog.V(2).Infof("Remote %s not mapped. Mapping now!", remotePath) + username := mountOptions[0] + password := sensitiveMountOptions[0] + if err := smb.NewSmbGlobalMapping(remotePath, username, password); err != nil { + klog.Errorf("NewSmbGlobalMapping(%s) failed with %v", remotePath, err) + return err + } + } + + if len(localPath) != 0 { + if err := filesystem.ValidatePathWindows(localPath); err != nil { + return err + } + if err := os.Symlink(remotePath, localPath); err != nil { + return fmt.Errorf("os.Symlink(%s, %s) failed with %v", remotePath, localPath, err) + } + } + klog.V(2).Infof("mount %s on %s successfully", source, normalizedTarget) + return nil +} + +// Mount just creates a soft link at target pointing to source. +func (mounter *winMounter) Mount(source, target, fstype string, options []string) error { + return os.Symlink(normalizeWindowsPath(source), normalizeWindowsPath(target)) +} + +// Rmdir - delete the given directory +func (mounter *winMounter) Rmdir(path string) error { + return filesystem.Rmdir(normalizeWindowsPath(path), true) +} + +// 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) +} + +// Unmount - Removes the directory - equivalent to unmount on Linux. +func (mounter *winMounter) SMBUnmount(target, _ string) error { + target = normalizeWindowsPath(target) + remoteServer, err := smb.GetRemoteServerFromTarget(target) + if err == nil { + klog.V(2).Infof("remote server path: %s, local path: %s", remoteServer, target) + if hasDupSMBMount, err := smb.CheckForDuplicateSMBMounts(driverGlobalMountPath, target, remoteServer); err == nil { + if !hasDupSMBMount { + if err := smb.RemoveSmbGlobalMapping(remoteServer); err != nil { + klog.Errorf("RemoveSmbGlobalMapping(%s) failed with %v", target, err) + } + } else { + klog.V(2).Infof("skip unmount as there are other SMB mounts on the same remote server %s", remoteServer) + } + } else { + klog.Errorf("CheckForDuplicateSMBMounts(%s, %s) failed with %v", target, remoteServer, err) + } + } else { + klog.Errorf("GetRemoteServerFromTarget(%s) failed with %v", target, err) + } + + klog.V(2).Infof("Unmount: remote path: %s local path: %s", remoteServer, 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) { + isExists, err := mounter.ExistsPath(path) + if err != nil { + return false, err + } + if !isExists { + return true, os.ErrNotExist + } + + response, err := filesystem.IsMountPoint(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 { + return os.MkdirAll(normalizeWindowsPath(path), 0755) +} + +// ExistsPath - Checks if a path exists. Unlike util ExistsPath, this call does not perform follow link. +func (mounter *winMounter) ExistsPath(path string) (bool, error) { + return filesystem.PathExists(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 36a386d8069..efbf6f64e51 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(_ bool) (*mount.SafeFormatAndMount, error) { +func NewSafeMounter(_, _ 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 7991c4736b9..e620101f63c 100644 --- a/pkg/mounter/safe_mounter_unix_test.go +++ b/pkg/mounter/safe_mounter_unix_test.go @@ -23,7 +23,7 @@ import ( ) func TestNewSafeMounter(t *testing.T) { - resp, err := NewSafeMounter(true) + resp, err := NewSafeMounter(true, true) 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 52f0da75562..57e7694a188 100644 --- a/pkg/mounter/safe_mounter_windows.go +++ b/pkg/mounter/safe_mounter_windows.go @@ -131,7 +131,7 @@ func (mounter *csiProxyMounter) SMBMount(source, target, fsType string, mountOpt return nil } -func (mounter *csiProxyMounter) SMBUnmount(target string, volumeID string) error { +func (mounter *csiProxyMounter) SMBUnmount(target, volumeID string) error { klog.V(4).Infof("SMBUnmount: local path: %s", target) if remotePath, err := os.Readlink(target); err != nil { @@ -369,7 +369,14 @@ func NewCSIProxyMounter(removeSMBMappingDuringUnmount bool) (*csiProxyMounter, e }, nil } -func NewSafeMounter(removeSMBMappingDuringUnmount bool) (*mount.SafeFormatAndMount, error) { +func NewSafeMounter(enableWindowsHostProcess, removeSMBMappingDuringUnmount 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(removeSMBMappingDuringUnmount) 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 00000000000..47de07d8c61 --- /dev/null +++ b/pkg/os/filesystem/filesystem.go @@ -0,0 +1,166 @@ +//go:build windows +// +build windows + +/* +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" + "regexp" + "strings" + + "github.com/kubernetes-csi/csi-driver-smb/pkg/util" + "k8s.io/klog/v2" +) + +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(path string) (bool, error) { + if err := ValidatePathWindows(path); err != nil { + klog.Errorf("failed validatePathWindows %v", err) + return false, err + } + return pathExists(path) +} + +func PathValid(_ context.Context, path string) (bool, error) { + cmd := `Test-Path $Env:remotepath` + cmdEnv := fmt.Sprintf("remotepath=%s", path) + output, err := util.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 +} + +func ValidatePathWindows(path string) error { + 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) + } + + return nil +} + +func Rmdir(path string, force bool) error { + if err := ValidatePathWindows(path); err != nil { + return err + } + + if force { + return os.RemoveAll(path) + } + return os.Remove(path) +} + +func IsMountPoint(path string) (bool, error) { + return IsSymlink(path) +} + +// 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/smb/smb.go b/pkg/os/smb/smb.go new file mode 100644 index 00000000000..5784037b652 --- /dev/null +++ b/pkg/os/smb/smb.go @@ -0,0 +1,113 @@ +//go:build windows +// +build windows + +/* +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" + "path/filepath" + "strings" + + "github.com/kubernetes-csi/csi-driver-smb/pkg/util" + "k8s.io/klog/v2" +) + +func IsSmbMapped(remotePath string) (bool, error) { + cmdLine := `$(Get-SmbGlobalMapping -RemotePath $Env:smbremotepath -ErrorAction Stop).Status` + cmdEnv := fmt.Sprintf("smbremotepath=%s", remotePath) + out, err := util.RunPowershellCmd(cmdLine, cmdEnv) + 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 +} + +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 -RequirePrivacy $true`) + + klog.V(2).Infof("begin to run NewSmbGlobalMapping with %s, %s", remotePath, username) + if output, err := util.RunPowershellCmd(cmdLine, fmt.Sprintf("smbuser=%s", username), + fmt.Sprintf("smbpassword=%s", password), + fmt.Sprintf("smbremotepath=%s", remotePath)); err != nil { + return fmt.Errorf("NewSmbGlobalMapping failed. output: %q, err: %v", string(output), err) + } + return nil +} + +func RemoveSmbGlobalMapping(remotePath string) error { + remotePath = strings.TrimSuffix(remotePath, `\`) + cmd := `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force` + klog.V(2).Infof("begin to run RemoveSmbGlobalMapping with %s", remotePath) + if output, err := util.RunPowershellCmd(cmd, fmt.Sprintf("smbremotepath=%s", remotePath)); err != nil { + return fmt.Errorf("UnmountSmbShare failed. output: %q, err: %v", string(output), err) + } + return nil +} + +// GetRemoteServerFromTarget- gets the remote server path given a mount point, the function is recursive until it find the remote server or errors out +func GetRemoteServerFromTarget(mount string) (string, error) { + target, err := os.Readlink(mount) + klog.V(2).Infof("read link for mount %s, target: %s", mount, target) + if err != nil || len(target) == 0 { + return "", fmt.Errorf("error reading link for mount %s. target %s err: %v", mount, target, err) + } + return strings.TrimSpace(target), nil +} + +// CheckForDuplicateSMBMounts checks if there is any other SMB mount exists on the same remote server +func CheckForDuplicateSMBMounts(dir, mount, remoteServer string) (bool, error) { + files, err := os.ReadDir(dir) + if err != nil { + return false, err + } + + for _, file := range files { + klog.V(6).Infof("checking file %s", file.Name()) + if file.IsDir() { + globalMountPath := filepath.Join(dir, file.Name(), "globalmount") + if strings.EqualFold(filepath.Clean(globalMountPath), filepath.Clean(mount)) { + klog.V(2).Infof("skip current mount path %s", mount) + } else { + fileInfo, err := os.Lstat(globalMountPath) + // check if the file is a symlink, if yes, check if it is pointing to the same remote server + if err == nil && fileInfo.Mode()&os.ModeSymlink != 0 { + remoteServerPath, err := GetRemoteServerFromTarget(globalMountPath) + klog.V(2).Infof("checking remote server path %s on local path %s", remoteServerPath, globalMountPath) + if err == nil { + if remoteServerPath == remoteServer { + return true, nil + } + } else { + klog.Errorf("GetRemoteServerFromTarget(%s) failed with %v", globalMountPath, err) + } + } + } + } + } + return false, err +} diff --git a/pkg/os/smb/smb_test.go b/pkg/os/smb/smb_test.go new file mode 100644 index 00000000000..98dc67406c4 --- /dev/null +++ b/pkg/os/smb/smb_test.go @@ -0,0 +1,59 @@ +//go:build windows +// +build windows + +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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" + "testing" +) + +func TestCheckForDuplicateSMBMounts(t *testing.T) { + tests := []struct { + name string + dir string + mount string + remoteServer string + expectedResult bool + expectedError error + }{ + { + name: "directory does not exist", + dir: "non-existing-mount", + expectedResult: false, + expectedError: fmt.Errorf("open non-existing-mount: The system cannot find the file specified."), + }, + } + + for _, test := range tests { + result, err := CheckForDuplicateSMBMounts(test.dir, test.mount, test.remoteServer) + if result != test.expectedResult { + t.Errorf("Expected %v, got %v", test.expectedResult, result) + } + if err == nil && test.expectedError != nil { + t.Errorf("Expected error %v, got nil", test.expectedError) + } + if err != nil && test.expectedError == nil { + t.Errorf("Expected nil, got %v", err) + } + if err != nil && test.expectedError != nil && err.Error() != test.expectedError.Error() { + t.Errorf("Expected error %v, got %v", test.expectedError, err) + } + } +} diff --git a/pkg/smb/fake_mounter.go b/pkg/smb/fake_mounter.go index c874ed6a33a..c06fdd2a6a0 100644 --- a/pkg/smb/fake_mounter.go +++ b/pkg/smb/fake_mounter.go @@ -65,7 +65,7 @@ func (f *fakeMounter) IsLikelyNotMountPoint(file string) (bool, error) { func NewFakeMounter() (*mount.SafeFormatAndMount, error) { if runtime.GOOS == "windows" { - return mounter.NewSafeMounter(true) + return mounter.NewSafeMounter(true, true) } return &mount.SafeFormatAndMount{ Interface: &fakeMounter{}, diff --git a/pkg/smb/smb.go b/pkg/smb/smb.go index ffb18ca0d8e..a4e27abf061 100644 --- a/pkg/smb/smb.go +++ b/pkg/smb/smb.go @@ -73,6 +73,7 @@ type DriverOptions struct { Krb5Prefix string DefaultOnDeletePolicy string RemoveArchivedVolumePath bool + EnableWindowsHostProcess bool } // Driver implements all interfaces of CSI drivers @@ -100,6 +101,7 @@ type Driver struct { krb5Prefix string defaultOnDeletePolicy string removeArchivedVolumePath bool + enableWindowsHostProcess bool } // NewDriver Creates a NewCSIDriver object. Assumes vendor version is equal to driver version & @@ -113,6 +115,7 @@ func NewDriver(options *DriverOptions) *Driver { driver.removeSMBMappingDuringUnmount = options.RemoveSMBMappingDuringUnmount driver.removeArchivedVolumePath = options.RemoveArchivedVolumePath driver.workingMountDir = options.WorkingMountDir + driver.enableWindowsHostProcess = options.EnableWindowsHostProcess driver.volumeLocks = newVolumeLocks() driver.krb5CacheDirectory = options.Krb5CacheDirectory @@ -146,7 +149,7 @@ func (d *Driver) Run(endpoint, _ string, testMode bool) { } klog.V(2).Infof("\nDRIVER INFORMATION:\n-------------------\n%s\n\nStreaming logs below:", versionMeta) - d.mounter, err = mounter.NewSafeMounter(d.removeSMBMappingDuringUnmount) + d.mounter, err = mounter.NewSafeMounter(d.enableWindowsHostProcess, d.removeSMBMappingDuringUnmount) if err != nil { klog.Fatalf("Failed to get safe mounter. Error: %v", err) } diff --git a/pkg/util/util.go b/pkg/util/util.go index 06b626cb849..638e8391bf3 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -17,9 +17,17 @@ limitations under the License. package util import ( + "k8s.io/klog/v2" + "os" + "os/exec" "time" ) +const MaxPathLengthWindows = 260 + +// control the number of concurrent powershell commands running on Windows node +var powershellCmdSem = make(chan struct{}, 3) + // ExecFunc returns a exec function's output and error type ExecFunc func() (err error) @@ -46,3 +54,14 @@ func WaitUntilTimeout(timeout time.Duration, execFunc ExecFunc, timeoutFunc Time return timeoutFunc() } } + +func RunPowershellCmd(command string, envs ...string) ([]byte, error) { + // acquire a semaphore to limit the number of concurrent operations + powershellCmdSem <- struct{}{} + defer func() { <-powershellCmdSem }() + + cmd := exec.Command("powershell", "-Mta", "-NoProfile", "-Command", command) + cmd.Env = append(os.Environ(), envs...) + klog.V(6).Infof("Executing command: %q", cmd.String()) + return cmd.CombinedOutput() +}