From ec06ac5586627ec67dd4e03d1015755f6246b66d Mon Sep 17 00:00:00 2001 From: bugwz Date: Fri, 11 Jul 2025 22:48:46 +0800 Subject: [PATCH 1/4] Support multi mount-string --- cmd/minikube/cmd/start.go | 29 ++++++++++--- cmd/minikube/cmd/start_flags.go | 31 +++++++++++-- pkg/drivers/kic/oci/types_test.go | 72 ++++++++++++++++--------------- pkg/minikube/config/types.go | 2 +- pkg/minikube/node/config.go | 3 +- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/cmd/minikube/cmd/start.go b/cmd/minikube/cmd/start.go index 82fb1386ab75..f42cda252eba 100644 --- a/cmd/minikube/cmd/start.go +++ b/cmd/minikube/cmd/start.go @@ -269,14 +269,33 @@ func runStart(cmd *cobra.Command, _ []string) { validateBuiltImageVersion(starter.Runner, ds.Name) if existing != nil && driver.IsKIC(existing.Driver) && viper.GetBool(createMount) { - old := "" - if len(existing.ContainerVolumeMounts) > 0 { - old = existing.ContainerVolumeMounts[0] + isSameMount := func(old, new []string) bool { + if len(old) != len(new) { + return false + } + + mountmap := make(map[string]int) + for _, item := range old { + mountmap[item]++ + } + + for _, item := range new { + cnt, exists := mountmap[item] + if !exists || cnt == 0 { + return false + } + mountmap[item]-- + } + + return true } - if mount := viper.GetString(mountString); old != mount { + + old := existing.ContainerVolumeMounts + new := getMountStrings() + if !isSameMount(old, new) { exit.Message(reason.GuestMountConflict, "Sorry, {{.driver}} does not allow mounts to be changed after container creation (previous mount: '{{.old}}', new mount: '{{.new}})'", out.V{ "driver": existing.Driver, - "new": mount, + "new": new, "old": old, }) } diff --git a/cmd/minikube/cmd/start_flags.go b/cmd/minikube/cmd/start_flags.go index 0028d5031047..b479ab8055d3 100644 --- a/cmd/minikube/cmd/start_flags.go +++ b/cmd/minikube/cmd/start_flags.go @@ -173,7 +173,7 @@ func initMinikubeFlags() { startCmd.Flags().Bool(embedCerts, false, "if true, will embed the certs in kubeconfig.") startCmd.Flags().StringP(containerRuntime, "c", constants.DefaultContainerRuntime, fmt.Sprintf("The container runtime to be used. Valid options: %s (default: auto)", strings.Join(cruntime.ValidRuntimes(), ", "))) startCmd.Flags().Bool(createMount, false, "This will start the mount daemon and automatically mount files into minikube.") - startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start.") + startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start, in a semicolon-separated format.") startCmd.Flags().String(mount9PVersion, defaultMount9PVersion, mount9PVersionDescription) startCmd.Flags().String(mountGID, defaultMountGID, mountGIDDescription) startCmd.Flags().String(mountIPFlag, defaultMountIP, mountIPDescription) @@ -492,6 +492,15 @@ func getNetwork(driverName string) string { return n } +func getMountStrings() []string { + mountStrings := strings.Split(viper.GetString(mountString), ";") + if !validateMountStrings(mountStrings) { + exit.Message(reason.Usage, "The mount-string contains duplicate items.") + } + + return mountStrings +} + func validateQemuNetwork(n string) string { switch n { case "socket_vmnet": @@ -546,6 +555,17 @@ func validateVfkitNetwork(n string) string { return n } +func validateMountStrings(mountStrings []string) bool { + mountmap := make(map[string]bool) + for _, mountstring := range mountStrings { + if _, exists := mountmap[mountstring]; exists { + return false + } + mountmap[mountstring] = true + } + return true +} + // generateNewConfigFromFlags generate a config.ClusterConfig based on flags func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime string, drvName string) config.ClusterConfig { var cc config.ClusterConfig @@ -613,7 +633,7 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime str ExtraDisks: viper.GetInt(extraDisks), CertExpiration: viper.GetDuration(certExpiration), Mount: viper.GetBool(createMount), - MountString: viper.GetString(mountString), + MountStrings: getMountStrings(), Mount9PVersion: viper.GetString(mount9PVersion), MountGID: viper.GetString(mountGID), MountIP: viper.GetString(mountIPFlag), @@ -653,7 +673,7 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime str } cc.VerifyComponents = interpretWaitFlag(*cmd) if viper.GetBool(createMount) && driver.IsKIC(drvName) { - cc.ContainerVolumeMounts = []string{viper.GetString(mountString)} + cc.ContainerVolumeMounts = getMountStrings() } if driver.IsKIC(drvName) { @@ -694,6 +714,10 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime str } } + if cmd.Flags().Changed(mountString) { + cc.MountStrings = getMountStrings() + } + return cc } @@ -865,7 +889,6 @@ func updateExistingConfigFromFlags(cmd *cobra.Command, existing *config.ClusterC updateBoolFromFlag(cmd, &cc.KubernetesConfig.ShouldLoadCachedImages, cacheImages) updateDurationFromFlag(cmd, &cc.CertExpiration, certExpiration) updateBoolFromFlag(cmd, &cc.Mount, createMount) - updateStringFromFlag(cmd, &cc.MountString, mountString) updateStringFromFlag(cmd, &cc.Mount9PVersion, mount9PVersion) updateStringFromFlag(cmd, &cc.MountGID, mountGID) updateStringFromFlag(cmd, &cc.MountIP, mountIPFlag) diff --git a/pkg/drivers/kic/oci/types_test.go b/pkg/drivers/kic/oci/types_test.go index 2e2d698025f0..1bca9223b39c 100644 --- a/pkg/drivers/kic/oci/types_test.go +++ b/pkg/drivers/kic/oci/types_test.go @@ -23,23 +23,23 @@ import ( func TestParseMountString(t *testing.T) { testCases := []struct { Name string - MountString string + MountStrings []string ExpectErr bool ExpectedMount Mount }{ { - Name: "basic linux", - MountString: "/foo:/bar", - ExpectErr: false, + Name: "basic linux", + MountStrings: []string{"/foo:/bar"}, + ExpectErr: false, ExpectedMount: Mount{ HostPath: "/foo", ContainerPath: "/bar", }, }, { - Name: "linux read only", - MountString: "/foo:/bar:ro", - ExpectErr: false, + Name: "linux read only", + MountStrings: []string{"/foo:/bar:ro"}, + ExpectErr: false, ExpectedMount: Mount{ HostPath: "/foo", ContainerPath: "/bar", @@ -47,18 +47,18 @@ func TestParseMountString(t *testing.T) { }, }, { - Name: "windows style", - MountString: "C:\\Windows\\Path:/foo", - ExpectErr: false, + Name: "windows style", + MountStrings: []string{"C:\\Windows\\Path:/foo"}, + ExpectErr: false, ExpectedMount: Mount{ HostPath: "C:\\Windows\\Path", ContainerPath: "/foo", }, }, { - Name: "windows style read/write", - MountString: "C:\\Windows\\Path:/foo:rw", - ExpectErr: false, + Name: "windows style read/write", + MountStrings: []string{"C:\\Windows\\Path:/foo:rw"}, + ExpectErr: false, ExpectedMount: Mount{ HostPath: "C:\\Windows\\Path", ContainerPath: "/foo", @@ -66,17 +66,17 @@ func TestParseMountString(t *testing.T) { }, }, { - Name: "container only", - MountString: "/foo", - ExpectErr: false, + Name: "container only", + MountStrings: []string{"/foo"}, + ExpectErr: false, ExpectedMount: Mount{ ContainerPath: "/foo", }, }, { - Name: "selinux relabel & bidirectional propagation", - MountString: "/foo:/bar/baz:Z,rshared", - ExpectErr: false, + Name: "selinux relabel & bidirectional propagation", + MountStrings: []string{"/foo:/bar/baz:Z,rshared"}, + ExpectErr: false, ExpectedMount: Mount{ HostPath: "/foo", ContainerPath: "/bar/baz", @@ -85,9 +85,9 @@ func TestParseMountString(t *testing.T) { }, }, { - Name: "invalid mount option", - MountString: "/foo:/bar:Z,bat", - ExpectErr: true, + Name: "invalid mount option", + MountStrings: []string{"/foo:/bar:Z,bat"}, + ExpectErr: true, ExpectedMount: Mount{ HostPath: "/foo", ContainerPath: "/bar", @@ -96,14 +96,14 @@ func TestParseMountString(t *testing.T) { }, { Name: "empty spec", - MountString: "", + MountStrings: []string{""}, ExpectErr: false, ExpectedMount: Mount{}, }, { - Name: "relative container path", - MountString: "/foo/bar:baz/bat:private", - ExpectErr: true, + Name: "relative container path", + MountStrings: []string{"/foo/bar:baz/bat:private"}, + ExpectErr: true, ExpectedMount: Mount{ HostPath: "/foo/bar", ContainerPath: "baz/bat", @@ -113,15 +113,17 @@ func TestParseMountString(t *testing.T) { } for _, tc := range testCases { - mount, err := ParseMountString(tc.MountString) - if err != nil && !tc.ExpectErr { - t.Errorf("Unexpected error for \"%s\": %v", tc.Name, err) - } - if err == nil && tc.ExpectErr { - t.Errorf("Expected error for \"%s\" but didn't get any: %v %v", tc.Name, mount, err) - } - if mount != tc.ExpectedMount { - t.Errorf("Unexpected mount for \"%s\":\n expected %+v\ngot %+v", tc.Name, tc.ExpectedMount, mount) + for _, mountString := range tc.MountStrings { + mount, err := ParseMountString(mountString) + if err != nil && !tc.ExpectErr { + t.Errorf("Unexpected error for \"%s\": %v", tc.Name, err) + } + if err == nil && tc.ExpectErr { + t.Errorf("Expected error for \"%s\" but didn't get any: %v %v", tc.Name, mount, err) + } + if mount != tc.ExpectedMount { + t.Errorf("Unexpected mount for \"%s\":\n expected %+v\ngot %+v", tc.Name, tc.ExpectedMount, mount) + } } } } diff --git a/pkg/minikube/config/types.go b/pkg/minikube/config/types.go index d8dc05281e78..1b033f215471 100644 --- a/pkg/minikube/config/types.go +++ b/pkg/minikube/config/types.go @@ -89,7 +89,7 @@ type ClusterConfig struct { ExtraDisks int // currently only implemented for hyperkit and kvm2 CertExpiration time.Duration Mount bool - MountString string + MountStrings []string Mount9PVersion string MountGID string MountIP string diff --git a/pkg/minikube/node/config.go b/pkg/minikube/node/config.go index 442808745adf..ac1dcd1267a5 100644 --- a/pkg/minikube/node/config.go +++ b/pkg/minikube/node/config.go @@ -102,7 +102,8 @@ func generateMountArgs(profile string, cc config.ClusterConfig) []string { mountDebugVal = 1 } - args := []string{"mount", cc.MountString} + args := []string{"mount"} + args = append(args, cc.MountStrings...) flags := []struct { name string value string From 14c679030368174bde9cbd24753f8c141c6e6da0 Mon Sep 17 00:00:00 2001 From: bugwz Date: Fri, 11 Jul 2025 22:52:51 +0800 Subject: [PATCH 2/4] Fix multi mount error --- pkg/minikube/node/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/minikube/node/config.go b/pkg/minikube/node/config.go index ac1dcd1267a5..84b423b72636 100644 --- a/pkg/minikube/node/config.go +++ b/pkg/minikube/node/config.go @@ -77,7 +77,7 @@ func configureMounts(wg *sync.WaitGroup, cc config.ClusterConfig) { return } - out.Step(style.Mounting, "Creating mount {{.name}} ...", out.V{"name": cc.MountString}) + out.Step(style.Mounting, "Creating mount {{.name}} ...", out.V{"name": cc.MountStrings}) path := os.Args[0] profile := viper.GetString("profile") From ce2bea2441324f77d9502712817f4368f40ac312 Mon Sep 17 00:00:00 2001 From: bugwz Date: Mon, 14 Jul 2025 23:22:14 +0800 Subject: [PATCH 3/4] fix errors and add test --- cmd/minikube/cmd/start.go | 18 ++--- cmd/minikube/cmd/start_flags.go | 2 +- pkg/drivers/kic/oci/types_test.go | 126 ++++++++++++++++++++---------- 3 files changed, 93 insertions(+), 53 deletions(-) diff --git a/cmd/minikube/cmd/start.go b/cmd/minikube/cmd/start.go index f42cda252eba..7bbe87a5191b 100644 --- a/cmd/minikube/cmd/start.go +++ b/cmd/minikube/cmd/start.go @@ -269,17 +269,17 @@ func runStart(cmd *cobra.Command, _ []string) { validateBuiltImageVersion(starter.Runner, ds.Name) if existing != nil && driver.IsKIC(existing.Driver) && viper.GetBool(createMount) { - isSameMount := func(old, new []string) bool { - if len(old) != len(new) { + isSameMount := func(oldm, newm []string) bool { + if len(oldm) != len(newm) { return false } mountmap := make(map[string]int) - for _, item := range old { + for _, item := range oldm { mountmap[item]++ } - for _, item := range new { + for _, item := range newm { cnt, exists := mountmap[item] if !exists || cnt == 0 { return false @@ -290,13 +290,13 @@ func runStart(cmd *cobra.Command, _ []string) { return true } - old := existing.ContainerVolumeMounts - new := getMountStrings() - if !isSameMount(old, new) { + oldMounts := existing.ContainerVolumeMounts + newMounts := getMountStrings() + if !isSameMount(oldMounts, newMounts) { exit.Message(reason.GuestMountConflict, "Sorry, {{.driver}} does not allow mounts to be changed after container creation (previous mount: '{{.old}}', new mount: '{{.new}})'", out.V{ "driver": existing.Driver, - "new": new, - "old": old, + "new": newMounts, + "old": oldMounts, }) } } diff --git a/cmd/minikube/cmd/start_flags.go b/cmd/minikube/cmd/start_flags.go index b479ab8055d3..8a87ad6c74f0 100644 --- a/cmd/minikube/cmd/start_flags.go +++ b/cmd/minikube/cmd/start_flags.go @@ -173,7 +173,7 @@ func initMinikubeFlags() { startCmd.Flags().Bool(embedCerts, false, "if true, will embed the certs in kubeconfig.") startCmd.Flags().StringP(containerRuntime, "c", constants.DefaultContainerRuntime, fmt.Sprintf("The container runtime to be used. Valid options: %s (default: auto)", strings.Join(cruntime.ValidRuntimes(), ", "))) startCmd.Flags().Bool(createMount, false, "This will start the mount daemon and automatically mount files into minikube.") - startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start, in a semicolon-separated format.") + startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start, in a semicolon-separated format(eg. /foo:/foo;/bar:/bar:Z,rshared).") startCmd.Flags().String(mount9PVersion, defaultMount9PVersion, mount9PVersionDescription) startCmd.Flags().String(mountGID, defaultMountGID, mountGIDDescription) startCmd.Flags().String(mountIPFlag, defaultMountIP, mountIPDescription) diff --git a/pkg/drivers/kic/oci/types_test.go b/pkg/drivers/kic/oci/types_test.go index 1bca9223b39c..baaf268ce46c 100644 --- a/pkg/drivers/kic/oci/types_test.go +++ b/pkg/drivers/kic/oci/types_test.go @@ -17,103 +17,143 @@ limitations under the License. package oci import ( + "strings" "testing" ) func TestParseMountString(t *testing.T) { testCases := []struct { Name string - MountStrings []string + MountStrings string ExpectErr bool - ExpectedMount Mount + ExpectedMount []Mount }{ { Name: "basic linux", - MountStrings: []string{"/foo:/bar"}, + MountStrings: "/foo:/bar", ExpectErr: false, - ExpectedMount: Mount{ - HostPath: "/foo", - ContainerPath: "/bar", + ExpectedMount: []Mount{ + { + HostPath: "/foo", + ContainerPath: "/bar", + }, }, }, { Name: "linux read only", - MountStrings: []string{"/foo:/bar:ro"}, + MountStrings: "/foo:/bar:ro", ExpectErr: false, - ExpectedMount: Mount{ - HostPath: "/foo", - ContainerPath: "/bar", - Readonly: true, + ExpectedMount: []Mount{ + { + HostPath: "/foo", + ContainerPath: "/bar", + Readonly: true, + }, }, }, { Name: "windows style", - MountStrings: []string{"C:\\Windows\\Path:/foo"}, + MountStrings: "C:\\Windows\\Path:/foo", ExpectErr: false, - ExpectedMount: Mount{ - HostPath: "C:\\Windows\\Path", - ContainerPath: "/foo", + ExpectedMount: []Mount{ + { + HostPath: "C:\\Windows\\Path", + ContainerPath: "/foo", + }, }, }, { Name: "windows style read/write", - MountStrings: []string{"C:\\Windows\\Path:/foo:rw"}, + MountStrings: "C:\\Windows\\Path:/foo:rw", ExpectErr: false, - ExpectedMount: Mount{ - HostPath: "C:\\Windows\\Path", - ContainerPath: "/foo", - Readonly: false, + ExpectedMount: []Mount{ + { + HostPath: "C:\\Windows\\Path", + ContainerPath: "/foo", + Readonly: false, + }, }, }, { Name: "container only", - MountStrings: []string{"/foo"}, + MountStrings: "/foo", ExpectErr: false, - ExpectedMount: Mount{ - ContainerPath: "/foo", + ExpectedMount: []Mount{ + { + ContainerPath: "/foo", + }, }, }, { Name: "selinux relabel & bidirectional propagation", - MountStrings: []string{"/foo:/bar/baz:Z,rshared"}, + MountStrings: "/foo:/bar/baz:Z,rshared", ExpectErr: false, - ExpectedMount: Mount{ - HostPath: "/foo", - ContainerPath: "/bar/baz", - SelinuxRelabel: true, - Propagation: MountPropagationBidirectional, + ExpectedMount: []Mount{ + { + HostPath: "/foo", + ContainerPath: "/bar/baz", + SelinuxRelabel: true, + Propagation: MountPropagationBidirectional, + }, }, }, { Name: "invalid mount option", - MountStrings: []string{"/foo:/bar:Z,bat"}, + MountStrings: "/foo:/bar:Z,bat", ExpectErr: true, - ExpectedMount: Mount{ - HostPath: "/foo", - ContainerPath: "/bar", - SelinuxRelabel: true, + ExpectedMount: []Mount{ + { + HostPath: "/foo", + ContainerPath: "/bar", + SelinuxRelabel: true, + }, }, }, { Name: "empty spec", - MountStrings: []string{""}, + MountStrings: "", ExpectErr: false, - ExpectedMount: Mount{}, + ExpectedMount: []Mount{{}}, }, { Name: "relative container path", - MountStrings: []string{"/foo/bar:baz/bat:private"}, + MountStrings: "/foo/bar:baz/bat:private", ExpectErr: true, - ExpectedMount: Mount{ - HostPath: "/foo/bar", - ContainerPath: "baz/bat", - Propagation: MountPropagationNone, + ExpectedMount: []Mount{ + { + HostPath: "/foo/bar", + ContainerPath: "baz/bat", + Propagation: MountPropagationNone, + }, + }, + }, + { + Name: "multi mount string", + MountStrings: "/foo:/foo:ro;/bar:/bar:Z,rshared", + ExpectErr: false, + ExpectedMount: []Mount{ + { + HostPath: "/foo", + ContainerPath: "/foo", + Readonly: true, + }, + { + HostPath: "/bar", + ContainerPath: "/bar", + SelinuxRelabel: true, + Propagation: MountPropagationBidirectional, + }, }, }, } for _, tc := range testCases { - for _, mountString := range tc.MountStrings { + mountStrings := strings.Split(tc.MountStrings, ";") + if len(mountStrings) != len(tc.ExpectedMount) { + t.Errorf("Unexpected length error for \"%s\":\n expected %d\ngot %d", tc.Name, len(tc.ExpectedMount), len(mountStrings)) + } + + for id, mountString := range mountStrings { mount, err := ParseMountString(mountString) if err != nil && !tc.ExpectErr { t.Errorf("Unexpected error for \"%s\": %v", tc.Name, err) @@ -121,7 +161,7 @@ func TestParseMountString(t *testing.T) { if err == nil && tc.ExpectErr { t.Errorf("Expected error for \"%s\" but didn't get any: %v %v", tc.Name, mount, err) } - if mount != tc.ExpectedMount { + if mount != tc.ExpectedMount[id] { t.Errorf("Unexpected mount for \"%s\":\n expected %+v\ngot %+v", tc.Name, tc.ExpectedMount, mount) } } From 82c5e0ec7ef1534ec2b8b85ad6446903c408b18c Mon Sep 17 00:00:00 2001 From: bugwz Date: Sat, 26 Jul 2025 22:31:17 +0800 Subject: [PATCH 4/4] Support multi mount for mount --- cmd/minikube/cmd/mount.go | 61 ++++++++++++++++---------- cmd/minikube/cmd/start_flags.go | 2 +- pkg/generate/rewrite.go | 2 +- pkg/minikube/cluster/mount.go | 40 ++++++++++------- site/content/en/docs/commands/mount.md | 6 ++- site/content/en/docs/commands/start.md | 2 +- third_party/go9p/srv_srv.go | 1 + third_party/go9p/ufs.go | 14 +++++- third_party/go9p/ufs/ufs.go | 5 ++- third_party/go9p/ufs_darwin.go | 2 +- third_party/go9p/ufs_freebsd.go | 2 +- third_party/go9p/ufs_linux.go | 2 +- third_party/go9p/ufs_windows.go | 2 +- 13 files changed, 89 insertions(+), 52 deletions(-) diff --git a/cmd/minikube/cmd/mount.go b/cmd/minikube/cmd/mount.go index f8b1ab80ade1..84f9f5842178 100644 --- a/cmd/minikube/cmd/mount.go +++ b/cmd/minikube/cmd/mount.go @@ -88,9 +88,10 @@ var supportedFilesystems = map[string]bool{nineP: true} // mountCmd represents the mount command var mountCmd = &cobra.Command{ - Use: "mount [flags] :", - Short: "Mounts the specified directory into minikube", - Long: `Mounts the specified directory into minikube.`, + Use: "mount [flags] :[;:;...]", + Short: "Mounts the specified directories into minikube", + Long: `Mounts the specified directories into minikube.`, + Example: "minikube mount /hostdir1:/vmdir1\nminikube mount \"/hostdir1:/vmdir1;/hostdir2:/vmdir2\"", Run: func(_ *cobra.Command, args []string) { if isKill { if err := killMountProcess(); err != nil { @@ -104,22 +105,36 @@ var mountCmd = &cobra.Command{ minikube mount : (example: "/host-home:/vm-home")`) } mountString := args[0] - idx := strings.LastIndex(mountString, ":") - if idx == -1 { // no ":" was present - exit.Message(reason.Usage, `mount argument "{{.value}}" must be in form: :`, out.V{"value": mountString}) - } - hostPath := mountString[:idx] - vmPath := mountString[idx+1:] - if _, err := os.Stat(hostPath); err != nil { - if os.IsNotExist(err) { - exit.Message(reason.HostPathMissing, "Cannot find directory {{.path}} for mount", out.V{"path": hostPath}) - } else { - exit.Error(reason.HostPathStat, "stat failed", err) + var hostPaths []string + var vmPaths []string + vmPathMap := make(map[string]string) + for _, item := range strings.Split(mountString, ";") { + paths := strings.Split(item, ":") + // no ":" was present + if len(paths) != 2 { + exit.Message(reason.Usage, `mount argument "{{.value}}" must be in form: :[;:;...]`, out.V{"value": mountString}) } + + // check host and vm path + if _, err := os.Stat(paths[0]); err != nil { + if os.IsNotExist(err) { + exit.Message(reason.HostPathMissing, "Cannot find directory {{.path}} for mount", out.V{"path": paths[0]}) + } else { + exit.Error(reason.HostPathStat, "stat failed", err) + } + } + if len(paths[1]) == 0 || !strings.HasPrefix(paths[1], "/") { + exit.Message(reason.Usage, "Target directory {{.path}} must be an absolute path", out.V{"path": paths[1]}) + } + if _, exists := vmPathMap[paths[1]]; exists { + exit.Message(reason.Usage, "Target directory {{.path}} must be unique", out.V{"path": paths[1]}) + } + vmPathMap[paths[1]] = paths[0] + + hostPaths = append(hostPaths, paths[0]) + vmPaths = append(vmPaths, paths[1]) } - if len(vmPath) == 0 || !strings.HasPrefix(vmPath, "/") { - exit.Message(reason.Usage, "Target directory {{.path}} must be an absolute path", out.V{"path": vmPath}) - } + var debugVal int if klog.V(1).Enabled() { debugVal = 1 // ufs.StartServer takes int debug param @@ -200,7 +215,7 @@ var mountCmd = &cobra.Command{ if driver.IsKIC(co.CP.Host.Driver.DriverName()) && runtime.GOOS != "linux" { bindIP = "127.0.0.1" } - out.Step(style.Mounting, "Mounting host path {{.sourcePath}} into VM as {{.destinationPath}} ...", out.V{"sourcePath": hostPath, "destinationPath": vmPath}) + out.Step(style.Mounting, "Mounting host path {{.sourcePath}} into VM as {{.destinationPath}} ...", out.V{"sourcePath": hostPaths, "destinationPath": vmPaths}) out.Infof("Mount type: {{.name}}", out.V{"name": cfg.Type}) out.Infof("User ID: {{.userID}}", out.V{"userID": cfg.UID}) out.Infof("Group ID: {{.groupID}}", out.V{"groupID": cfg.GID}) @@ -216,7 +231,7 @@ var mountCmd = &cobra.Command{ go func(pid chan int) { pid <- os.Getpid() out.Styled(style.Fileserver, "Userspace file server: ") - ufs.StartServer(net.JoinHostPort(bindIP, strconv.Itoa(port)), debugVal, hostPath) + ufs.StartServer(net.JoinHostPort(bindIP, strconv.Itoa(port)), debugVal, hostPaths) out.Step(style.Stopped, "Userspace file server is shutdown") wg.Done() }(pidchan) @@ -228,8 +243,8 @@ var mountCmd = &cobra.Command{ signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { for sig := range c { - out.Step(style.Unmount, "Unmounting {{.path}} ...", out.V{"path": vmPath}) - err := cluster.Unmount(co.CP.Runner, vmPath) + out.Step(style.Unmount, "Unmounting {{.path}} ...", out.V{"path": vmPaths}) + err := cluster.Unmount(co.CP.Runner, vmPaths) if err != nil { out.FailureT("Failed unmount: {{.error}}", out.V{"error": err}) } @@ -243,14 +258,14 @@ var mountCmd = &cobra.Command{ } }() - err = cluster.Mount(co.CP.Runner, ip.String(), vmPath, cfg, pid) + err = cluster.Mount(co.CP.Runner, ip.String(), vmPaths, cfg, pid) if err != nil { if rtErr, ok := err.(*cluster.MountError); ok && rtErr.ErrorType == cluster.MountErrorConnect { exit.Error(reason.GuestMountCouldNotConnect, "mount could not connect", rtErr) } exit.Error(reason.GuestMount, "mount failed", err) } - out.Step(style.Success, "Successfully mounted {{.sourcePath}} to {{.destinationPath}}", out.V{"sourcePath": hostPath, "destinationPath": vmPath}) + out.Step(style.Success, "Successfully mounted {{.sourcePath}} to {{.destinationPath}}", out.V{"sourcePath": hostPaths, "destinationPath": vmPaths}) out.Ln("") out.Styled(style.Notice, "NOTE: This process must stay alive for the mount to be accessible ...") wg.Wait() diff --git a/cmd/minikube/cmd/start_flags.go b/cmd/minikube/cmd/start_flags.go index 8a87ad6c74f0..9cf529238798 100644 --- a/cmd/minikube/cmd/start_flags.go +++ b/cmd/minikube/cmd/start_flags.go @@ -173,7 +173,7 @@ func initMinikubeFlags() { startCmd.Flags().Bool(embedCerts, false, "if true, will embed the certs in kubeconfig.") startCmd.Flags().StringP(containerRuntime, "c", constants.DefaultContainerRuntime, fmt.Sprintf("The container runtime to be used. Valid options: %s (default: auto)", strings.Join(cruntime.ValidRuntimes(), ", "))) startCmd.Flags().Bool(createMount, false, "This will start the mount daemon and automatically mount files into minikube.") - startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start, in a semicolon-separated format(eg. /foo:/foo;/bar:/bar:Z,rshared).") + startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start, in a semicolon-separated format. (eg. /foo:/foo;/bar:/bar:Z,rshared)") startCmd.Flags().String(mount9PVersion, defaultMount9PVersion, mount9PVersionDescription) startCmd.Flags().String(mountGID, defaultMountGID, mountGIDDescription) startCmd.Flags().String(mountIPFlag, defaultMountIP, mountIPDescription) diff --git a/pkg/generate/rewrite.go b/pkg/generate/rewrite.go index c3e2f1d69451..a4821db94d82 100644 --- a/pkg/generate/rewrite.go +++ b/pkg/generate/rewrite.go @@ -54,7 +54,7 @@ func rewriteFlags(command *cobra.Command) error { usage: "Used to specify the driver to run Kubernetes in. The list of available drivers depends on operating system.", }, { flag: "mount-string", - usage: "The argument to pass the minikube mount command on start.", + usage: "The argument to pass the minikube mount command on start, in a semicolon-separated format. (eg. /foo:/foo;/bar:/bar:Z,rshared)", }, { flag: "iso-url", usage: "Locations to fetch the minikube ISO from. The list depends on the machine architecture.", diff --git a/pkg/minikube/cluster/mount.go b/pkg/minikube/cluster/mount.go index 147729905f30..0174949f283e 100644 --- a/pkg/minikube/cluster/mount.go +++ b/pkg/minikube/cluster/mount.go @@ -80,21 +80,25 @@ func (m *MountError) Error() string { } // Mount runs the mount command from the 9p client on the VM to the 9p server on the host -func Mount(r mountRunner, source string, target string, c *MountConfig, pid int) error { - if err := Unmount(r, target); err != nil { +func Mount(r mountRunner, source string, targets []string, c *MountConfig, pid int) error { + if err := Unmount(r, targets); err != nil { return &MountError{ErrorType: MountErrorUnknown, UnderlyingError: errors.Wrap(err, "umount")} } - if _, err := r.RunCmd(exec.Command("/bin/bash", "-c", fmt.Sprintf("sudo mkdir -p %s", target))); err != nil { - return &MountError{ErrorType: MountErrorUnknown, UnderlyingError: errors.Wrap(err, "create folder pre-mount")} - } + for _, target := range targets { + if _, err := r.RunCmd(exec.Command("/bin/bash", "-c", fmt.Sprintf("sudo mkdir -p %s", target))); err != nil { + return &MountError{ErrorType: MountErrorUnknown, UnderlyingError: errors.Wrap(err, "create folder pre-mount")} + } - rr, err := r.RunCmd(exec.Command("/bin/bash", "-c", mntCmd(source, target, c))) - if err != nil { - if strings.Contains(rr.Stderr.String(), "Connection timed out") { - return &MountError{ErrorType: MountErrorConnect, UnderlyingError: err} + rr, err := r.RunCmd(exec.Command("/bin/bash", "-c", mntCmd(source, target, c))) + if err != nil { + if strings.Contains(rr.Stderr.String(), "Connection timed out") { + return &MountError{ErrorType: MountErrorConnect, UnderlyingError: err} + } + return &MountError{ErrorType: MountErrorUnknown, UnderlyingError: errors.Wrapf(err, "mount with cmd %s ", rr.Command())} } - return &MountError{ErrorType: MountErrorUnknown, UnderlyingError: errors.Wrapf(err, "mount with cmd %s ", rr.Command())} + + klog.Infof("mount successful: %q", rr.Output()) } profile := viper.GetString("profile") @@ -102,7 +106,7 @@ func Mount(r mountRunner, source string, target string, c *MountConfig, pid int) exit.Error(reason.HostMountPid, "Error writing mount pid", err) } - klog.Infof("mount successful: %q", rr.Output()) + klog.Infof("mount successful") return nil } @@ -171,12 +175,14 @@ func mntCmd(source string, target string, c *MountConfig) string { } // Unmount unmounts a path -func Unmount(r mountRunner, target string) error { - // grep because findmnt will also display the parent! - c := exec.Command("/bin/bash", "-c", fmt.Sprintf("[ \"x$(findmnt -T %s | grep %s)\" != \"x\" ] && sudo umount -f %s || echo ", target, target, target)) - if _, err := r.RunCmd(c); err != nil { - return errors.Wrap(err, "unmount") +func Unmount(r mountRunner, targets []string) error { + for _, target := range targets { + // grep because findmnt will also display the parent! + c := exec.Command("/bin/bash", "-c", fmt.Sprintf("[ \"x$(findmnt -T %s | grep %s)\" != \"x\" ] && sudo umount -f %s || echo ", target, target, target)) + if _, err := r.RunCmd(c); err != nil { + return errors.Wrap(err, "unmount") + } + klog.Infof("unmount for %s ran successfully", target) } - klog.Infof("unmount for %s ran successfully", target) return nil } diff --git a/site/content/en/docs/commands/mount.md b/site/content/en/docs/commands/mount.md index e816f61e69d1..e210a4f461af 100644 --- a/site/content/en/docs/commands/mount.md +++ b/site/content/en/docs/commands/mount.md @@ -14,7 +14,11 @@ Mounts the specified directory into minikube Mounts the specified directory into minikube. ```shell -minikube mount [flags] : +minikube mount [flags] :[;:;...] + +Example: + minikube mount /hostdir1:/vmdir1 + minikube mount "/hostdir1:/vmdir1;/hostdir2:/vmdir2" ``` ### Options diff --git a/site/content/en/docs/commands/start.md b/site/content/en/docs/commands/start.md index 0480e7798041..456011141913 100644 --- a/site/content/en/docs/commands/start.md +++ b/site/content/en/docs/commands/start.md @@ -88,7 +88,7 @@ minikube start [flags] --mount-msize int The number of bytes to use for 9p packet payload (default 262144) --mount-options strings Additional mount options, such as cache=fscache --mount-port uint16 Specify the port that the mount should be setup on, where 0 means any free port. - --mount-string string The argument to pass the minikube mount command on start. + --mount-string string The argument to pass the minikube mount command on start, in a semicolon-separated format. (eg. /foo:/foo;/bar:/bar:Z,rshared) --mount-type string Specify the mount filesystem type (supported types: 9p) (default "9p") --mount-uid string Default user id used for the mount (default "docker") --namespace string The named space to activate after start (default "default") diff --git a/third_party/go9p/srv_srv.go b/third_party/go9p/srv_srv.go index db51c129933d..7b97e8067040 100644 --- a/third_party/go9p/srv_srv.go +++ b/third_party/go9p/srv_srv.go @@ -33,6 +33,7 @@ var Ebadoffset error = &Error{"bad offset in directory read", EINVAL} var Edirchange error = &Error{"cannot convert between files and directories", EINVAL} var Enouser error = &Error{"unknown user", EINVAL} var Enotimpl error = &Error{"not implemented", EINVAL} +var Einvalidrootid error = &Error{"invalid root index", EINVAL} // Authentication operations. The file server should implement them if // it requires user authentication. The authentication in 9P2000 is diff --git a/third_party/go9p/ufs.go b/third_party/go9p/ufs.go index a3110aefa8ca..d270f51cd792 100644 --- a/third_party/go9p/ufs.go +++ b/third_party/go9p/ufs.go @@ -16,6 +16,7 @@ import ( ) type ufsFid struct { + root string path string file *os.File dirs []os.FileInfo @@ -27,7 +28,8 @@ type ufsFid struct { type Ufs struct { Srv - Root string + Root []string + RootIdx int } func toError(err error) *Error { @@ -166,13 +168,18 @@ func (ufs *Ufs) Attach(req *SrvReq) { req.RespondError(Enoauth) return } + if ufs.RootIdx >= len(ufs.Root) { + req.RespondError(Einvalidrootid) + return + } tc := req.Tc fid := new(ufsFid) // You can think of the ufs.Root as a 'chroot' of a sort. // clients attach are not allowed to go outside the // directory represented by ufs.Root - fid.path = path.Join(ufs.Root, tc.Aname) + fid.root = ufs.Root[ufs.RootIdx] + fid.path = path.Join(fid.root, tc.Aname) req.Fid.Aux = fid err := fid.stat() @@ -181,6 +188,7 @@ func (ufs *Ufs) Attach(req *SrvReq) { return } + ufs.RootIdx += 1 qid := dir2Qid(fid.st) req.RespondRattach(qid) } @@ -203,6 +211,7 @@ func (*Ufs) Walk(req *SrvReq) { nfid := req.Newfid.Aux.(*ufsFid) wqids := make([]Qid, len(tc.Wname)) + root := fid.root path := fid.path i := 0 for ; i < len(tc.Wname); i++ { @@ -221,6 +230,7 @@ func (*Ufs) Walk(req *SrvReq) { path = p } + nfid.root = root nfid.path = path req.RespondRwalk(wqids[0:i]) } diff --git a/third_party/go9p/ufs/ufs.go b/third_party/go9p/ufs/ufs.go index c5f46a68f6ff..608f3e95d0fa 100644 --- a/third_party/go9p/ufs/ufs.go +++ b/third_party/go9p/ufs/ufs.go @@ -11,11 +11,12 @@ import ( "k8s.io/minikube/third_party/go9p" ) -func StartServer(addrVal string, debugVal int, rootVal string) { +func StartServer(addrVal string, debugVal int, rootVals []string) { ufs := new(go9p.Ufs) ufs.Dotu = true ufs.Id = "ufs" - ufs.Root = rootVal + ufs.Root = rootVals + ufs.RootIdx = 0 ufs.Debuglevel = debugVal ufs.Start(ufs) diff --git a/third_party/go9p/ufs_darwin.go b/third_party/go9p/ufs_darwin.go index 42b1c2c25208..48fd2b971318 100644 --- a/third_party/go9p/ufs_darwin.go +++ b/third_party/go9p/ufs_darwin.go @@ -185,7 +185,7 @@ func (u *Ufs) Wstat(req *SrvReq) { // cwd. var destpath string if dir.Name[0] == '/' { - destpath = path.Join(u.Root, dir.Name) + destpath = path.Join(fid.root, dir.Name) fmt.Printf("/ results in %s\n", destpath) } else { fiddir, _ := path.Split(fid.path) diff --git a/third_party/go9p/ufs_freebsd.go b/third_party/go9p/ufs_freebsd.go index da9a10fae2a3..e67bde0c3743 100644 --- a/third_party/go9p/ufs_freebsd.go +++ b/third_party/go9p/ufs_freebsd.go @@ -185,7 +185,7 @@ func (u *Ufs) Wstat(req *SrvReq) { // cwd. var destpath string if dir.Name[0] == '/' { - destpath = path.Join(u.Root, dir.Name) + destpath = path.Join(fid.root, dir.Name) fmt.Printf("/ results in %s\n", destpath) } else { fiddir, _ := path.Split(fid.path) diff --git a/third_party/go9p/ufs_linux.go b/third_party/go9p/ufs_linux.go index 387c999d7f75..3de90be6666a 100644 --- a/third_party/go9p/ufs_linux.go +++ b/third_party/go9p/ufs_linux.go @@ -181,7 +181,7 @@ func (u *Ufs) Wstat(req *SrvReq) { // cwd. var destpath string if dir.Name[0] == '/' { - destpath = path.Join(u.Root, dir.Name) + destpath = path.Join(fid.root, dir.Name) fmt.Printf("/ results in %s\n", destpath) } else { fiddir, _ := path.Split(fid.path) diff --git a/third_party/go9p/ufs_windows.go b/third_party/go9p/ufs_windows.go index 5bb501fd59f4..c615c331f84e 100644 --- a/third_party/go9p/ufs_windows.go +++ b/third_party/go9p/ufs_windows.go @@ -197,7 +197,7 @@ func (u *Ufs) Wstat(req *SrvReq) { // cwd. var destpath string if dir.Name[0] == '/' { - destpath = path.Join(u.Root, dir.Name) + destpath = path.Join(fid.root, dir.Name) fmt.Printf("/ results in %s\n", destpath) } else { fiddir, _ := path.Split(fid.path)