Skip to content

Commit 4b0bafb

Browse files
authored
Merge pull request #3855 from tstromberg/mount-intr
mount: unmount on sigint/sigterm, add --options and --mode, improve UI
2 parents d1a5f13 + 034c5f0 commit 4b0bafb

File tree

7 files changed

+312
-79
lines changed

7 files changed

+312
-79
lines changed

cmd/minikube/cmd/mount.go

+82-14
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ package cmd
1919
import (
2020
"net"
2121
"os"
22+
"os/signal"
23+
"strconv"
2224
"strings"
2325
"sync"
26+
"syscall"
2427

2528
"github.com/golang/glog"
2629
"github.com/spf13/cobra"
@@ -34,16 +37,26 @@ import (
3437
"k8s.io/minikube/third_party/go9p/ufs"
3538
)
3639

40+
// nineP is the value of --type used for the 9p filesystem.
41+
const nineP = "9p"
42+
43+
// placeholders for flag values
3744
var mountIP string
3845
var mountVersion string
46+
var mountType string
3947
var isKill bool
4048
var uid int
4149
var gid int
42-
var msize int
50+
var mSize int
51+
var options []string
52+
var mode uint
53+
54+
// supportedFilesystems is a map of filesystem types to not warn against.
55+
var supportedFilesystems = map[string]bool{nineP: true}
4356

4457
// mountCmd represents the mount command
4558
var mountCmd = &cobra.Command{
46-
Use: "mount [flags] MOUNT_DIRECTORY(ex:\"/home\")",
59+
Use: "mount [flags] <source directory>:<target directory>",
4760
Short: "Mounts the specified directory into minikube",
4861
Long: `Mounts the specified directory into minikube.`,
4962
Run: func(cmd *cobra.Command, args []string) {
@@ -56,13 +69,12 @@ var mountCmd = &cobra.Command{
5669

5770
if len(args) != 1 {
5871
exit.Usage(`Please specify the directory to be mounted:
59-
minikube mount HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY(ex:"/host-home:/vm-home")`)
72+
minikube mount <source directory>:<target directory> (example: "/host-home:/vm-home")`)
6073
}
6174
mountString := args[0]
6275
idx := strings.LastIndex(mountString, ":")
6376
if idx == -1 { // no ":" was present
64-
exit.Usage(`Mount directory must be in the form:
65-
HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY`)
77+
exit.Usage(`mount argument %q must be in form: <source directory>:<target directory>`, mountString)
6678
}
6779
hostPath := mountString[:idx]
6880
vmPath := mountString[idx+1:]
@@ -74,7 +86,7 @@ var mountCmd = &cobra.Command{
7486
}
7587
}
7688
if len(vmPath) == 0 || !strings.HasPrefix(vmPath, "/") {
77-
exit.Usage("The :VM_MOUNT_DIRECTORY must be an absolute path")
89+
exit.Usage("Target directory %q must be an absolute path", vmPath)
7890
}
7991
var debugVal int
8092
if glog.V(1) {
@@ -104,32 +116,88 @@ var mountCmd = &cobra.Command{
104116
exit.WithCode(exit.Data, "error parsing the input ip address for mount")
105117
}
106118
}
107-
console.OutStyle("mounting", "Mounting %s into %s on the minikube VM", hostPath, vmPath)
108-
console.OutStyle("notice", "This daemon process needs to stay alive for the mount to be accessible ...")
109119
port, err := cmdUtil.GetPort()
110120
if err != nil {
111121
exit.WithError("Error finding port for mount", err)
112122
}
123+
124+
cfg := &cluster.MountConfig{
125+
Type: mountType,
126+
UID: uid,
127+
GID: gid,
128+
Version: mountVersion,
129+
MSize: mSize,
130+
Port: port,
131+
Mode: os.FileMode(mode),
132+
Options: map[string]string{},
133+
}
134+
135+
for _, o := range options {
136+
if !strings.Contains(o, "=") {
137+
cfg.Options[o] = ""
138+
continue
139+
}
140+
parts := strings.Split(o, "=")
141+
cfg.Options[parts[0]] = parts[1]
142+
}
143+
144+
console.OutStyle("mounting", "Mounting host path %s into VM as %s ...", hostPath, vmPath)
145+
console.OutStyle("mount-options", "Mount options:")
146+
console.OutStyle("option", "Type: %s", cfg.Type)
147+
console.OutStyle("option", "UID: %d", cfg.UID)
148+
console.OutStyle("option", "GID: %d", cfg.GID)
149+
console.OutStyle("option", "Version: %s", cfg.Version)
150+
console.OutStyle("option", "MSize: %d", cfg.MSize)
151+
console.OutStyle("option", "Mode: %o (%s)", cfg.Mode, cfg.Mode)
152+
console.OutStyle("option", "Options: %s", cfg.Options)
153+
154+
// An escape valve to allow future hackers to try NFS, VirtFS, or other FS types.
155+
if !supportedFilesystems[cfg.Type] {
156+
console.OutLn("")
157+
console.OutStyle("warning", "%s is not yet a supported filesystem. We will try anyways!", cfg.Type)
158+
}
159+
113160
var wg sync.WaitGroup
114-
wg.Add(1)
161+
if cfg.Type == nineP {
162+
wg.Add(1)
163+
go func() {
164+
console.OutStyle("fileserver", "Userspace file server: ")
165+
ufs.StartServer(net.JoinHostPort(ip.String(), strconv.Itoa(port)), debugVal, hostPath)
166+
wg.Done()
167+
}()
168+
}
169+
170+
// Unmount if Ctrl-C or kill request is received.
171+
c := make(chan os.Signal, 1)
172+
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
115173
go func() {
116-
ufs.StartServer(net.JoinHostPort(ip.String(), port), debugVal, hostPath)
117-
wg.Done()
174+
for sig := range c {
175+
console.OutStyle("unmount", "Unmounting %s ...", vmPath)
176+
cluster.Unmount(host, vmPath)
177+
exit.WithCode(exit.Interrupted, "Exiting due to %s signal", sig)
178+
}
118179
}()
119-
err = cluster.MountHost(api, ip, vmPath, port, mountVersion, uid, gid, msize)
180+
181+
err = cluster.Mount(host, ip.String(), vmPath, cfg)
120182
if err != nil {
121-
exit.WithError("failed to mount host", err)
183+
exit.WithError("mount failed", err)
122184
}
185+
console.OutStyle("success", "Successfully mounted %s to %s", hostPath, vmPath)
186+
console.OutLn("")
187+
console.OutStyle("notice", "NOTE: This process must stay alive for the mount to be accessible ...")
123188
wg.Wait()
124189
},
125190
}
126191

127192
func init() {
128193
mountCmd.Flags().StringVar(&mountIP, "ip", "", "Specify the ip that the mount should be setup on")
194+
mountCmd.Flags().StringVar(&mountType, "type", nineP, "Specify the mount filesystem type (supported types: 9p)")
129195
mountCmd.Flags().StringVar(&mountVersion, "9p-version", constants.DefaultMountVersion, "Specify the 9p version that the mount should use")
130196
mountCmd.Flags().BoolVar(&isKill, "kill", false, "Kill the mount process spawned by minikube start")
131197
mountCmd.Flags().IntVar(&uid, "uid", 1001, "Default user id used for the mount")
132198
mountCmd.Flags().IntVar(&gid, "gid", 1001, "Default group id used for the mount")
133-
mountCmd.Flags().IntVar(&msize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload")
199+
mountCmd.Flags().UintVar(&mode, "mode", 0755, "File permissions used for the mount")
200+
mountCmd.Flags().StringSliceVar(&options, "options", []string{}, "Additional mount options, such as cache=fscache")
201+
mountCmd.Flags().IntVar(&mSize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload")
134202
RootCmd.AddCommand(mountCmd)
135203
}

cmd/util/util.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ import (
2929
)
3030

3131
// Ask the kernel for a free open port that is ready to use
32-
func GetPort() (string, error) {
32+
func GetPort() (int, error) {
3333
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
3434
if err != nil {
3535
panic(err)
3636
}
3737

3838
l, err := net.ListenTCP("tcp", addr)
3939
if err != nil {
40-
return "", errors.Errorf("Error accessing port %d", addr.Port)
40+
return -1, errors.Errorf("Error accessing port %d", addr.Port)
4141
}
4242
defer l.Close()
43-
return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
43+
return l.Addr().(*net.TCPAddr).Port, nil
4444
}
4545

4646
func KillMountProcess() error {

pkg/minikube/cluster/cluster.go

-61
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ limitations under the License.
1717
package cluster
1818

1919
import (
20-
"bytes"
2120
"encoding/json"
2221
"flag"
2322
"fmt"
24-
"html/template"
2523
"net"
2624
"os/exec"
2725
"regexp"
@@ -338,30 +336,6 @@ func GetHostDockerEnv(api libmachine.API) (map[string]string, error) {
338336
return envMap, nil
339337
}
340338

341-
// MountHost runs the mount command from the 9p client on the VM to the 9p server on the host
342-
func MountHost(api libmachine.API, ip net.IP, path, port, mountVersion string, uid, gid, msize int) error {
343-
host, err := CheckIfHostExistsAndLoad(api, cfg.GetMachineName())
344-
if err != nil {
345-
return errors.Wrap(err, "Error checking that api exists and loading it")
346-
}
347-
if ip == nil {
348-
ip, err = GetVMHostIP(host)
349-
if err != nil {
350-
return errors.Wrap(err, "Error getting the host IP address to use from within the VM")
351-
}
352-
}
353-
host.RunSSHCommand(GetMountCleanupCommand(path))
354-
mountCmd, err := GetMountCommand(ip, path, port, mountVersion, uid, gid, msize)
355-
if err != nil {
356-
return errors.Wrap(err, "mount command")
357-
}
358-
_, err = host.RunSSHCommand(mountCmd)
359-
if err != nil {
360-
return errors.Wrap(err, "running mount")
361-
}
362-
return nil
363-
}
364-
365339
// GetVMHostIP gets the ip address to be used for mapping host -> VM and VM -> host
366340
func GetVMHostIP(host *host.Host) (net.IP, error) {
367341
switch host.DriverName {
@@ -461,38 +435,3 @@ func EnsureMinikubeRunningOrExit(api libmachine.API, exitStatus int) {
461435
exit.WithCode(exit.Unavailable, "minikube is not running, so the service cannot be accessed")
462436
}
463437
}
464-
465-
func GetMountCleanupCommand(path string) string {
466-
return fmt.Sprintf("sudo umount %s;", path)
467-
}
468-
469-
var mountTemplate = `
470-
sudo mkdir -p {{.Path}} || true;
471-
sudo mount -t 9p -o trans=tcp,port={{.Port}},dfltuid={{.UID}},dfltgid={{.GID}},version={{.Version}},msize={{.Msize}} {{.IP}} {{.Path}};
472-
sudo chmod 775 {{.Path}} || true;`
473-
474-
func GetMountCommand(ip net.IP, path, port, mountVersion string, uid, gid, msize int) (string, error) {
475-
t := template.Must(template.New("mountCommand").Parse(mountTemplate))
476-
buf := bytes.Buffer{}
477-
data := struct {
478-
IP string
479-
Path string
480-
Port string
481-
Version string
482-
UID int
483-
GID int
484-
Msize int
485-
}{
486-
IP: ip.String(),
487-
Path: path,
488-
Port: port,
489-
Version: mountVersion,
490-
UID: uid,
491-
GID: gid,
492-
Msize: msize,
493-
}
494-
if err := t.Execute(&buf, data); err != nil {
495-
return "", err
496-
}
497-
return buf.String(), nil
498-
}

pkg/minikube/cluster/mount.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cluster
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"sort"
23+
"strconv"
24+
"strings"
25+
26+
"github.com/pkg/errors"
27+
)
28+
29+
// MountConfig defines the options available to the Mount command
30+
type MountConfig struct {
31+
// Type is the filesystem type (Typically 9p)
32+
Type string
33+
// UID is the User ID which this path will be mounted as
34+
UID int
35+
// GID is the Group ID which this path will be mounted as
36+
GID int
37+
// Version is the 9P protocol version. Valid options: 9p2000, 9p200.u, 9p2000.L
38+
Version string
39+
// MSize is the number of bytes to use for 9p packet payload
40+
MSize int
41+
// Port is the port to connect to on the host
42+
Port int
43+
// Mode is the file permissions to set the mount to (octals)
44+
Mode os.FileMode
45+
// Extra mount options. See https://www.kernel.org/doc/Documentation/filesystems/9p.txt
46+
Options map[string]string
47+
}
48+
49+
// hostRunner is the subset of host.Host used for mounting
50+
type hostRunner interface {
51+
RunSSHCommand(cmd string) (string, error)
52+
}
53+
54+
// Mount runs the mount command from the 9p client on the VM to the 9p server on the host
55+
func Mount(h hostRunner, source string, target string, c *MountConfig) error {
56+
if err := Unmount(h, target); err != nil {
57+
return errors.Wrap(err, "umount")
58+
}
59+
60+
cmd := fmt.Sprintf("sudo mkdir -m %o -p %s && %s", c.Mode, target, mntCmd(source, target, c))
61+
out, err := h.RunSSHCommand(cmd)
62+
if err != nil {
63+
return errors.Wrap(err, out)
64+
}
65+
return nil
66+
}
67+
68+
// mntCmd returns a mount command based on a config.
69+
func mntCmd(source string, target string, c *MountConfig) string {
70+
options := map[string]string{
71+
"dfltgid": strconv.Itoa(c.GID),
72+
"dfltuid": strconv.Itoa(c.UID),
73+
}
74+
if c.Port != 0 {
75+
options["port"] = strconv.Itoa(c.Port)
76+
}
77+
if c.Version != "" {
78+
options["version"] = c.Version
79+
}
80+
if c.MSize != 0 {
81+
options["msize"] = strconv.Itoa(c.MSize)
82+
}
83+
84+
// Copy in all of the user-supplied keys and values
85+
for k, v := range c.Options {
86+
options[k] = v
87+
}
88+
89+
// Convert everything into a sorted list for better test results
90+
opts := []string{}
91+
for k, v := range options {
92+
// Mount option with no value, such as "noextend"
93+
if v == "" {
94+
opts = append(opts, k)
95+
continue
96+
}
97+
opts = append(opts, fmt.Sprintf("%s=%s", k, v))
98+
}
99+
sort.Strings(opts)
100+
return fmt.Sprintf("sudo mount -t %s -o %s %s %s", c.Type, strings.Join(opts, ","), source, target)
101+
}
102+
103+
// Unmount unmounts a path
104+
func Unmount(h hostRunner, target string) error {
105+
out, err := h.RunSSHCommand(fmt.Sprintf("findmnt -T %s && sudo umount %s || true", target, target))
106+
if err != nil {
107+
return errors.Wrap(err, out)
108+
}
109+
return nil
110+
}

0 commit comments

Comments
 (0)