diff --git a/cmd/minikube/cmd/mount.go b/cmd/minikube/cmd/mount.go index ff56d0caceb9..4260178135af 100644 --- a/cmd/minikube/cmd/mount.go +++ b/cmd/minikube/cmd/mount.go @@ -19,8 +19,11 @@ package cmd import ( "net" "os" + "os/signal" + "strconv" "strings" "sync" + "syscall" "github.com/golang/glog" "github.com/spf13/cobra" @@ -34,16 +37,26 @@ import ( "k8s.io/minikube/third_party/go9p/ufs" ) +// nineP is the value of --type used for the 9p filesystem. +const nineP = "9p" + +// placeholders for flag values var mountIP string var mountVersion string +var mountType string var isKill bool var uid int var gid int -var msize int +var mSize int +var options []string +var mode uint + +// supportedFilesystems is a map of filesystem types to not warn against. +var supportedFilesystems = map[string]bool{nineP: true} // mountCmd represents the mount command var mountCmd = &cobra.Command{ - Use: "mount [flags] MOUNT_DIRECTORY(ex:\"/home\")", + Use: "mount [flags] :", Short: "Mounts the specified directory into minikube", Long: `Mounts the specified directory into minikube.`, Run: func(cmd *cobra.Command, args []string) { @@ -56,13 +69,12 @@ var mountCmd = &cobra.Command{ if len(args) != 1 { exit.Usage(`Please specify the directory to be mounted: - minikube mount HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY(ex:"/host-home:/vm-home")`) + minikube mount : (example: "/host-home:/vm-home")`) } mountString := args[0] idx := strings.LastIndex(mountString, ":") if idx == -1 { // no ":" was present - exit.Usage(`Mount directory must be in the form: - HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY`) + exit.Usage(`mount argument %q must be in form: :`, mountString) } hostPath := mountString[:idx] vmPath := mountString[idx+1:] @@ -74,7 +86,7 @@ var mountCmd = &cobra.Command{ } } if len(vmPath) == 0 || !strings.HasPrefix(vmPath, "/") { - exit.Usage("The :VM_MOUNT_DIRECTORY must be an absolute path") + exit.Usage("Target directory %q must be an absolute path", vmPath) } var debugVal int if glog.V(1) { @@ -104,32 +116,88 @@ var mountCmd = &cobra.Command{ exit.WithCode(exit.Data, "error parsing the input ip address for mount") } } - console.OutStyle("mounting", "Mounting %s into %s on the minikube VM", hostPath, vmPath) - console.OutStyle("notice", "This daemon process needs to stay alive for the mount to be accessible ...") port, err := cmdUtil.GetPort() if err != nil { exit.WithError("Error finding port for mount", err) } + + cfg := &cluster.MountConfig{ + Type: mountType, + UID: uid, + GID: gid, + Version: mountVersion, + MSize: mSize, + Port: port, + Mode: os.FileMode(mode), + Options: map[string]string{}, + } + + for _, o := range options { + if !strings.Contains(o, "=") { + cfg.Options[o] = "" + continue + } + parts := strings.Split(o, "=") + cfg.Options[parts[0]] = parts[1] + } + + console.OutStyle("mounting", "Mounting host path %s into VM as %s ...", hostPath, vmPath) + console.OutStyle("mount-options", "Mount options:") + console.OutStyle("option", "Type: %s", cfg.Type) + console.OutStyle("option", "UID: %d", cfg.UID) + console.OutStyle("option", "GID: %d", cfg.GID) + console.OutStyle("option", "Version: %s", cfg.Version) + console.OutStyle("option", "MSize: %d", cfg.MSize) + console.OutStyle("option", "Mode: %o (%s)", cfg.Mode, cfg.Mode) + console.OutStyle("option", "Options: %s", cfg.Options) + + // An escape valve to allow future hackers to try NFS, VirtFS, or other FS types. + if !supportedFilesystems[cfg.Type] { + console.OutLn("") + console.OutStyle("warning", "%s is not yet a supported filesystem. We will try anyways!", cfg.Type) + } + var wg sync.WaitGroup - wg.Add(1) + if cfg.Type == nineP { + wg.Add(1) + go func() { + console.OutStyle("fileserver", "Userspace file server: ") + ufs.StartServer(net.JoinHostPort(ip.String(), strconv.Itoa(port)), debugVal, hostPath) + wg.Done() + }() + } + + // Unmount if Ctrl-C or kill request is received. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { - ufs.StartServer(net.JoinHostPort(ip.String(), port), debugVal, hostPath) - wg.Done() + for sig := range c { + console.OutStyle("unmount", "Unmounting %s ...", vmPath) + cluster.Unmount(host, vmPath) + exit.WithCode(exit.Interrupted, "Exiting due to %s signal", sig) + } }() - err = cluster.MountHost(api, ip, vmPath, port, mountVersion, uid, gid, msize) + + err = cluster.Mount(host, ip.String(), vmPath, cfg) if err != nil { - exit.WithError("failed to mount host", err) + exit.WithError("mount failed", err) } + console.OutStyle("success", "Successfully mounted %s to %s", hostPath, vmPath) + console.OutLn("") + console.OutStyle("notice", "NOTE: This process must stay alive for the mount to be accessible ...") wg.Wait() }, } func init() { mountCmd.Flags().StringVar(&mountIP, "ip", "", "Specify the ip that the mount should be setup on") + mountCmd.Flags().StringVar(&mountType, "type", nineP, "Specify the mount filesystem type (supported types: 9p)") mountCmd.Flags().StringVar(&mountVersion, "9p-version", constants.DefaultMountVersion, "Specify the 9p version that the mount should use") mountCmd.Flags().BoolVar(&isKill, "kill", false, "Kill the mount process spawned by minikube start") mountCmd.Flags().IntVar(&uid, "uid", 1001, "Default user id used for the mount") mountCmd.Flags().IntVar(&gid, "gid", 1001, "Default group id used for the mount") - mountCmd.Flags().IntVar(&msize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload") + mountCmd.Flags().UintVar(&mode, "mode", 0755, "File permissions used for the mount") + mountCmd.Flags().StringSliceVar(&options, "options", []string{}, "Additional mount options, such as cache=fscache") + mountCmd.Flags().IntVar(&mSize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload") RootCmd.AddCommand(mountCmd) } diff --git a/cmd/util/util.go b/cmd/util/util.go index 28105e727231..5774240e8f6d 100644 --- a/cmd/util/util.go +++ b/cmd/util/util.go @@ -29,7 +29,7 @@ import ( ) // Ask the kernel for a free open port that is ready to use -func GetPort() (string, error) { +func GetPort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "localhost:0") if err != nil { panic(err) @@ -37,10 +37,10 @@ func GetPort() (string, error) { l, err := net.ListenTCP("tcp", addr) if err != nil { - return "", errors.Errorf("Error accessing port %d", addr.Port) + return -1, errors.Errorf("Error accessing port %d", addr.Port) } defer l.Close() - return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil + return l.Addr().(*net.TCPAddr).Port, nil } func KillMountProcess() error { diff --git a/pkg/minikube/cluster/cluster.go b/pkg/minikube/cluster/cluster.go index 116822c0055a..078c4b64a854 100644 --- a/pkg/minikube/cluster/cluster.go +++ b/pkg/minikube/cluster/cluster.go @@ -17,11 +17,9 @@ limitations under the License. package cluster import ( - "bytes" "encoding/json" "flag" "fmt" - "html/template" "net" "os/exec" "regexp" @@ -338,30 +336,6 @@ func GetHostDockerEnv(api libmachine.API) (map[string]string, error) { return envMap, nil } -// MountHost runs the mount command from the 9p client on the VM to the 9p server on the host -func MountHost(api libmachine.API, ip net.IP, path, port, mountVersion string, uid, gid, msize int) error { - host, err := CheckIfHostExistsAndLoad(api, cfg.GetMachineName()) - if err != nil { - return errors.Wrap(err, "Error checking that api exists and loading it") - } - if ip == nil { - ip, err = GetVMHostIP(host) - if err != nil { - return errors.Wrap(err, "Error getting the host IP address to use from within the VM") - } - } - host.RunSSHCommand(GetMountCleanupCommand(path)) - mountCmd, err := GetMountCommand(ip, path, port, mountVersion, uid, gid, msize) - if err != nil { - return errors.Wrap(err, "mount command") - } - _, err = host.RunSSHCommand(mountCmd) - if err != nil { - return errors.Wrap(err, "running mount") - } - return nil -} - // GetVMHostIP gets the ip address to be used for mapping host -> VM and VM -> host func GetVMHostIP(host *host.Host) (net.IP, error) { switch host.DriverName { @@ -461,38 +435,3 @@ func EnsureMinikubeRunningOrExit(api libmachine.API, exitStatus int) { exit.WithCode(exit.Unavailable, "minikube is not running, so the service cannot be accessed") } } - -func GetMountCleanupCommand(path string) string { - return fmt.Sprintf("sudo umount %s;", path) -} - -var mountTemplate = ` -sudo mkdir -p {{.Path}} || true; -sudo mount -t 9p -o trans=tcp,port={{.Port}},dfltuid={{.UID}},dfltgid={{.GID}},version={{.Version}},msize={{.Msize}} {{.IP}} {{.Path}}; -sudo chmod 775 {{.Path}} || true;` - -func GetMountCommand(ip net.IP, path, port, mountVersion string, uid, gid, msize int) (string, error) { - t := template.Must(template.New("mountCommand").Parse(mountTemplate)) - buf := bytes.Buffer{} - data := struct { - IP string - Path string - Port string - Version string - UID int - GID int - Msize int - }{ - IP: ip.String(), - Path: path, - Port: port, - Version: mountVersion, - UID: uid, - GID: gid, - Msize: msize, - } - if err := t.Execute(&buf, data); err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/pkg/minikube/cluster/mount.go b/pkg/minikube/cluster/mount.go new file mode 100644 index 000000000000..41181330b880 --- /dev/null +++ b/pkg/minikube/cluster/mount.go @@ -0,0 +1,110 @@ +/* +Copyright 2019 The Kubernetes Authors All rights reserved. + +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 cluster + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// MountConfig defines the options available to the Mount command +type MountConfig struct { + // Type is the filesystem type (Typically 9p) + Type string + // UID is the User ID which this path will be mounted as + UID int + // GID is the Group ID which this path will be mounted as + GID int + // Version is the 9P protocol version. Valid options: 9p2000, 9p200.u, 9p2000.L + Version string + // MSize is the number of bytes to use for 9p packet payload + MSize int + // Port is the port to connect to on the host + Port int + // Mode is the file permissions to set the mount to (octals) + Mode os.FileMode + // Extra mount options. See https://www.kernel.org/doc/Documentation/filesystems/9p.txt + Options map[string]string +} + +// hostRunner is the subset of host.Host used for mounting +type hostRunner interface { + RunSSHCommand(cmd string) (string, error) +} + +// Mount runs the mount command from the 9p client on the VM to the 9p server on the host +func Mount(h hostRunner, source string, target string, c *MountConfig) error { + if err := Unmount(h, target); err != nil { + return errors.Wrap(err, "umount") + } + + cmd := fmt.Sprintf("sudo mkdir -m %o -p %s && %s", c.Mode, target, mntCmd(source, target, c)) + out, err := h.RunSSHCommand(cmd) + if err != nil { + return errors.Wrap(err, out) + } + return nil +} + +// mntCmd returns a mount command based on a config. +func mntCmd(source string, target string, c *MountConfig) string { + options := map[string]string{ + "dfltgid": strconv.Itoa(c.GID), + "dfltuid": strconv.Itoa(c.UID), + } + if c.Port != 0 { + options["port"] = strconv.Itoa(c.Port) + } + if c.Version != "" { + options["version"] = c.Version + } + if c.MSize != 0 { + options["msize"] = strconv.Itoa(c.MSize) + } + + // Copy in all of the user-supplied keys and values + for k, v := range c.Options { + options[k] = v + } + + // Convert everything into a sorted list for better test results + opts := []string{} + for k, v := range options { + // Mount option with no value, such as "noextend" + if v == "" { + opts = append(opts, k) + continue + } + opts = append(opts, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(opts) + return fmt.Sprintf("sudo mount -t %s -o %s %s %s", c.Type, strings.Join(opts, ","), source, target) +} + +// Unmount unmounts a path +func Unmount(h hostRunner, target string) error { + out, err := h.RunSSHCommand(fmt.Sprintf("findmnt -T %s && sudo umount %s || true", target, target)) + if err != nil { + return errors.Wrap(err, out) + } + return nil +} diff --git a/pkg/minikube/cluster/mount_test.go b/pkg/minikube/cluster/mount_test.go new file mode 100644 index 000000000000..cd10d1d10461 --- /dev/null +++ b/pkg/minikube/cluster/mount_test.go @@ -0,0 +1,112 @@ +/* +Copyright 2019 The Kubernetes Authors All rights reserved. + +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 cluster + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +type mockMountHost struct { + cmds []string + T *testing.T +} + +func NewMockMountHost(t *testing.T) *mockMountHost { + return &mockMountHost{ + T: t, + cmds: []string{}, + } +} + +func (m *mockMountHost) RunSSHCommand(cmd string) (string, error) { + m.cmds = append(m.cmds, cmd) + return "", nil +} + +func TestMount(t *testing.T) { + var tests = []struct { + name string + source string + target string + cfg *MountConfig + want []string + }{ + { + name: "simple", + source: "src", + target: "target", + cfg: &MountConfig{Type: "9p", Mode: os.FileMode(0700)}, + want: []string{ + "findmnt -T target && sudo umount target || true", + "sudo mkdir -m 700 -p target && sudo mount -t 9p -o dfltgid=0,dfltuid=0 src target", + }, + }, + { + name: "everything", + source: "10.0.0.1", + target: "/target", + cfg: &MountConfig{Type: "9p", Mode: os.FileMode(0777), UID: 82, GID: 72, Version: "9p2000.u", Options: map[string]string{ + "noextend": "", + "cache": "fscache", + }}, + want: []string{ + "findmnt -T /target && sudo umount /target || true", + "sudo mkdir -m 777 -p /target && sudo mount -t 9p -o cache=fscache,dfltgid=72,dfltuid=82,noextend,version=9p2000.u 10.0.0.1 /target", + }, + }, + { + name: "version-conflict", + source: "src", + target: "tgt", + cfg: &MountConfig{Type: "9p", Mode: os.FileMode(0700), Version: "9p2000.u", Options: map[string]string{ + "version": "9p2000.L", + }}, + want: []string{ + "findmnt -T tgt && sudo umount tgt || true", + "sudo mkdir -m 700 -p tgt && sudo mount -t 9p -o dfltgid=0,dfltuid=0,version=9p2000.L src tgt", + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := NewMockMountHost(t) + err := Mount(h, tc.source, tc.target, tc.cfg) + if err != nil { + t.Fatalf("Mount(%s, %s, %+v): %v", tc.source, tc.target, tc.cfg, err) + } + if diff := cmp.Diff(h.cmds, tc.want); diff != "" { + t.Errorf("command diff (-want +got): %s", diff) + } + }) + } +} + +func TestUnmount(t *testing.T) { + h := NewMockMountHost(t) + err := Unmount(h, "/mnt") + if err != nil { + t.Fatalf("Unmount(/mnt): %v", err) + } + + want := []string{"findmnt -T /mnt && sudo umount /mnt || true"} + if diff := cmp.Diff(h.cmds, want); diff != "" { + t.Errorf("command diff (-want +got): %s", diff) + } +} diff --git a/pkg/minikube/console/style.go b/pkg/minikube/console/style.go index 366ad2769963..c64eaf46a20f 100644 --- a/pkg/minikube/console/style.go +++ b/pkg/minikube/console/style.go @@ -95,6 +95,9 @@ var styles = map[string]style{ "meh": {Prefix: "🙄 ", LowPrefix: "? "}, "embarassed": {Prefix: "🤦 ", LowPrefix: "* "}, "tip": {Prefix: "💡 ", LowPrefix: "i "}, + "unmount": {Prefix: "🔥 ", LowPrefix: "x "}, + "mount-options": {Prefix: "💾 ", LowPrefix: "o "}, + "fileserver": {Prefix: "🚀 ", LowPrefix: "@ ", OmitNewline: true}, } // Add a prefix to a string diff --git a/pkg/minikube/exit/exit.go b/pkg/minikube/exit/exit.go index 3b5508d983f6..afff9bf0c83a 100644 --- a/pkg/minikube/exit/exit.go +++ b/pkg/minikube/exit/exit.go @@ -30,13 +30,14 @@ import ( // Exit codes based on sysexits(3) const ( Failure = 1 // Failure represents a general failure code + Interrupted = 2 // Ctrl-C (SIGINT) BadUsage = 64 // Usage represents an incorrect command line Data = 65 // Data represents incorrect data supplied by the user NoInput = 66 // NoInput represents that the input file did not exist or was not readable Unavailable = 69 // Unavailable represents when a service was unavailable Software = 70 // Software represents an internal software error. IO = 74 // IO represents an I/O error - Config = 78 // Config represents an unconfigured or miscon­figured state + Config = 78 // Config represents an unconfigured or misconfigured state Permissions = 77 // Permissions represents a permissions error // MaxProblems controls the number of problems to show for each source