Skip to content

Commit b6c63a9

Browse files
committed
drivers: Add Virtiofs mounts for vfkit and krunkit
Replace 9p mounts with virtiofs for vfkit and krunkit. Testing shows that virtiofs mount is 23 times faster with krunkit, and 8 times faster with vfkit. vfkit and krunkit support multiple virtiofs mounts but minikube --mount-* flags are not ready for multiple mounts. We have the same issue with KIC drivers, supporting multiple mounts but using only one. We hope to improve this in the next release. Example usage: minikube start --mount-string ~/models:/mnt/models We don't use the --mount flag since it does not make sense. We want to eliminate this flag also for 9p mounts soon. The arguments are parsed and validated when configuring the driver, so invalid arguments fail quickly without starting the driver. The validated mounts are stored in the machine config: $ jq '.Driver.VirtiofsMounts' < ~/.minikube/machines/minikube/config.json [ { "HostPath": "/Users/joe/models", "GuestPath": "/mnt/models", "Tag": "f845b54d-00e3-493d-9541-3b37490b96db" } ] Minikube generates a new random UUID for every virtiofs mount to identify the file system inside the guest. In krunkit and vfkit, every mount is add as: --device virtio-fs,sharedDir=/host-path,mountTag=f845b54d-00e3-493d-9541-3b37490b96db When the guest is started the shared directory is mounted via SSH using: sudo mkdir -p /mnt/models sudo mount -t virtiofs f845b54d-00e3-493d-9541-3b37490b96db /mnt/models Example mount: $ minikube ssh findmnt /mnt/models TARGET SOURCE FSTYPE OPTIONS /mnt/models f845b54d-00e3-493d-9541-3b37490b96db virtiofs rw,relatime More work is needed to add VirtioFS to qemu. I'm starting with vfkit and krunkit since they have identical interface (krunkit was designed as drop-in replacement for vfkit).
1 parent 1052718 commit b6c63a9

File tree

8 files changed

+220
-17
lines changed

8 files changed

+220
-17
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
Copyright 2025 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 virtiofs
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/docker/machine/libmachine/drivers"
26+
"github.com/google/uuid"
27+
)
28+
29+
// Mount is a directory on the host shared with the guest using
30+
// virtiofs.
31+
type Mount struct {
32+
// HostPath is an absolute path to existing directory to share with the guest.
33+
HostPath string
34+
// GuestPath is a path in the guest for mounting the shared directory.
35+
GuestPath string
36+
// Tag is a string identifying the shared file system in the guest.
37+
Tag string
38+
}
39+
40+
// ValidateMountString parses the mount-string flag and validates that the
41+
// specified paths can be used for virtiofs mount. Returns list with one
42+
// validated mount, ready for configuring the driver.
43+
// TODO: Drop when we have a flag supporting multiple mounts.
44+
func ValidateMountString(s string) ([]*Mount, error) {
45+
if s == "" {
46+
return nil, nil
47+
}
48+
return validateMounts([]string{s})
49+
}
50+
51+
func validateMounts(args []string) ([]*Mount, error) {
52+
var mounts []*Mount
53+
54+
seenHost := map[string]*Mount{}
55+
seenGuest := map[string]*Mount{}
56+
57+
for _, s := range args {
58+
mount, err := ParseMount(s)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
if err := mount.Validate(); err != nil {
64+
return nil, err
65+
}
66+
67+
if existing, ok := seenHost[mount.HostPath]; ok {
68+
return nil, fmt.Errorf("host path %q is already shared at guest path %q", mount.HostPath, existing.GuestPath)
69+
}
70+
seenHost[mount.HostPath] = mount
71+
72+
if existing, ok := seenGuest[mount.GuestPath]; ok {
73+
return nil, fmt.Errorf("guest path %q is already shared from host path %q", mount.GuestPath, existing.HostPath)
74+
}
75+
seenGuest[mount.GuestPath] = mount
76+
77+
mounts = append(mounts, mount)
78+
}
79+
80+
return mounts, nil
81+
}
82+
83+
// ParseMount parses a string in the format "/host-path:/guest-path" and returns
84+
// a new Mount instance. The mount must be validated before using it to
85+
// configure the driver.
86+
func ParseMount(s string) (*Mount, error) {
87+
pair := strings.SplitN(s, ":", 2)
88+
if len(pair) != 2 {
89+
return nil, fmt.Errorf("invalid virtiofs mount %q: (expected '/host-path:/guest-path')", s)
90+
}
91+
92+
return &Mount{
93+
HostPath: pair[0],
94+
GuestPath: pair[1],
95+
Tag: uuid.NewString(),
96+
}, nil
97+
}
98+
99+
// Validate that the mount can be used for virtiofs device configuration. Both
100+
// host and guest paths must be absolute. Host path must be a directory and must
101+
// not include virtiofs configuration separator (",").
102+
func (m *Mount) Validate() error {
103+
// "," is a --device configuration separator in vfkit and krunkit.
104+
if strings.Contains(m.HostPath, ",") {
105+
return fmt.Errorf("host path %q must not contain ','", m.HostPath)
106+
}
107+
108+
if !filepath.IsAbs(m.HostPath) {
109+
return fmt.Errorf("host path %q is not an absolute path", m.HostPath)
110+
}
111+
112+
if fs, err := os.Stat(m.HostPath); err != nil {
113+
return fmt.Errorf("failed to validate host path %q: %w", m.HostPath, err)
114+
} else if !fs.IsDir() {
115+
return fmt.Errorf("host path %q is not a directory", m.HostPath)
116+
}
117+
118+
if !filepath.IsAbs(m.GuestPath) {
119+
return fmt.Errorf("guest path %q is not an absolute path", m.GuestPath)
120+
}
121+
122+
return nil
123+
}
124+
125+
// SetupMounts connects to the host via SSH, creates the mount directory if
126+
// needed, and mount the virtiofs file system. It should be called by
127+
// driver.Start().
128+
func SetupMounts(d drivers.Driver, mounts []*Mount) error {
129+
var script strings.Builder
130+
131+
script.WriteString("set -e\n")
132+
133+
for _, mount := range mounts {
134+
script.WriteString(fmt.Sprintf("sudo mkdir -p \"%s\"\n", mount.GuestPath))
135+
script.WriteString(fmt.Sprintf("sudo mount -t virtiofs %s \"%s\"\n", mount.Tag, mount.GuestPath))
136+
}
137+
138+
if _, err := drivers.RunSSHCommandFromDriver(d, script.String()); err != nil {
139+
return err
140+
}
141+
142+
return nil
143+
}

pkg/drivers/krunkit/krunkit.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242

4343
"k8s.io/klog/v2"
4444
"k8s.io/minikube/pkg/drivers/common"
45+
"k8s.io/minikube/pkg/drivers/common/virtiofs"
4546
"k8s.io/minikube/pkg/drivers/common/vmnet"
4647
"k8s.io/minikube/pkg/minikube/exit"
4748
"k8s.io/minikube/pkg/minikube/firewall"
@@ -71,6 +72,7 @@ type Driver struct {
7172
CPU int
7273
Memory int
7374
ExtraDisks int
75+
VirtiofsMounts []*virtiofs.Mount
7476
MACAddress string
7577
VmnetHelper vmnet.Helper
7678
}
@@ -207,8 +209,18 @@ func (d *Driver) Start() error {
207209
}
208210

209211
log.Infof("Waiting for VM to start (ssh -p %d docker@%s)...", d.SSHPort, d.IPAddress)
212+
if err := WaitForTCPWithDelay(fmt.Sprintf("%s:%d", d.IPAddress, d.SSHPort), time.Second); err != nil {
213+
return err
214+
}
215+
216+
if len(d.VirtiofsMounts) > 0 {
217+
log.Infof("Setup virtiofs mounts ...")
218+
if err := virtiofs.SetupMounts(d, d.VirtiofsMounts); err != nil {
219+
return err
220+
}
221+
}
210222

211-
return WaitForTCPWithDelay(fmt.Sprintf("%s:%d", d.IPAddress, d.SSHPort), time.Second)
223+
return nil
212224
}
213225

214226
// startKrunkit starts the krunkit child process.
@@ -231,6 +243,11 @@ func (d *Driver) startKrunkit(socketPath string) error {
231243
"--device", fmt.Sprintf("virtio-blk,path=%s", common.ExtraDiskPath(d.BaseDriver, i)))
232244
}
233245

246+
for _, mount := range d.VirtiofsMounts {
247+
args = append(args,
248+
"--device", fmt.Sprintf("virtio-fs,sharedDir=%s,mountTag=%s", mount.HostPath, mount.Tag))
249+
}
250+
234251
log.Debugf("executing: krunkit %s", strings.Join(args, " "))
235252
cmd := exec.Command(driverName, args...)
236253

pkg/drivers/vfkit/vfkit.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444

4545
"k8s.io/klog/v2"
4646
"k8s.io/minikube/pkg/drivers/common"
47+
"k8s.io/minikube/pkg/drivers/common/virtiofs"
4748
"k8s.io/minikube/pkg/drivers/common/vmnet"
4849
"k8s.io/minikube/pkg/minikube/detect"
4950
"k8s.io/minikube/pkg/minikube/exit"
@@ -72,6 +73,7 @@ type Driver struct {
7273
CPU int
7374
Memory int
7475
ExtraDisks int
76+
VirtiofsMounts []*virtiofs.Mount
7577
Network string // "", "nat", "vmnet-shared"
7678
MACAddress string // For network=nat, network=""
7779
VmnetHelper *vmnet.Helper // For network=vmnet-shared
@@ -250,8 +252,18 @@ func (d *Driver) Start() error {
250252
}
251253

252254
log.Infof("Waiting for VM to start (ssh -p %d docker@%s)...", d.SSHPort, d.IPAddress)
255+
if err := WaitForTCPWithDelay(fmt.Sprintf("%s:%d", d.IPAddress, d.SSHPort), time.Second); err != nil {
256+
return err
257+
}
258+
259+
if len(d.VirtiofsMounts) > 0 {
260+
log.Infof("Setup virtiofs mounts ...")
261+
if err := virtiofs.SetupMounts(d, d.VirtiofsMounts); err != nil {
262+
return err
263+
}
264+
}
253265

254-
return WaitForTCPWithDelay(fmt.Sprintf("%s:%d", d.IPAddress, d.SSHPort), time.Second)
266+
return nil
255267
}
256268

257269
// startVfkit starts the vfkit child process. If socketPath is not empty, vfkit
@@ -308,6 +320,12 @@ func (d *Driver) startVfkit(socketPath string) error {
308320
startCmd = append(startCmd,
309321
"--device", fmt.Sprintf("virtio-serial,logFilePath=%s", serialPath))
310322

323+
for _, mount := range d.VirtiofsMounts {
324+
startCmd = append(startCmd,
325+
"--device", fmt.Sprintf("virtio-fs,sharedDir=%s,mountTag=%s", mount.HostPath, mount.Tag))
326+
327+
}
328+
311329
log.Debugf("executing: vfkit %s", strings.Join(startCmd, " "))
312330
os.Remove(d.sockfilePath())
313331
cmd := exec.Command("vfkit", startCmd...)

pkg/minikube/driver/driver.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ func SupportsNetworkFlag(name string) bool {
228228
return IsKIC(name) || IsKVM(name) || IsQEMU(name) || IsVFKit(name)
229229
}
230230

231+
// SupportsVirtiofsMounts returns if driver supports virtiofs mounts
232+
func SupportsVirtiofsMounts(name string) bool {
233+
return IsVFKit(name) || IsKrunkit(name)
234+
}
235+
231236
// AllowsPreload returns if preload is allowed for the driver
232237
func AllowsPreload(driverName string) bool {
233238
return !BareMetal(driverName) && !IsSSH(driverName)

pkg/minikube/node/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func configureMounts(wg *sync.WaitGroup, cc config.ClusterConfig) {
7373
wg.Add(1)
7474
defer wg.Done()
7575

76-
if cc.MountString == "" || driver.IsKIC(cc.Driver) {
76+
if cc.MountString == "" || driver.IsKIC(cc.Driver) || driver.SupportsVirtiofsMounts(cc.Driver) {
7777
return
7878
}
7979

pkg/minikube/registry/drvs/krunkit/krunkit.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/docker/machine/libmachine/drivers"
2929
"github.com/google/uuid"
3030

31+
"k8s.io/minikube/pkg/drivers/common/virtiofs"
3132
"k8s.io/minikube/pkg/drivers/common/vmnet"
3233
"k8s.io/minikube/pkg/drivers/krunkit"
3334
"k8s.io/minikube/pkg/minikube/config"
@@ -65,6 +66,11 @@ func configure(cfg config.ClusterConfig, n config.Node) (interface{}, error) {
6566
u = uuid.NewString()
6667
}
6768

69+
mounts, err := virtiofs.ValidateMountString(cfg.MountString)
70+
if err != nil {
71+
return nil, err
72+
}
73+
6874
return &krunkit.Driver{
6975
BaseDriver: &drivers.BaseDriver{
7076
MachineName: machineName,
@@ -76,6 +82,7 @@ func configure(cfg config.ClusterConfig, n config.Node) (interface{}, error) {
7682
Memory: cfg.Memory,
7783
CPU: cfg.CPUs,
7884
ExtraDisks: cfg.ExtraDisks,
85+
VirtiofsMounts: mounts,
7986
VmnetHelper: vmnet.Helper{
8087
MachineDir: filepath.Join(storePath, "machines", machineName),
8188
InterfaceID: u,

pkg/minikube/registry/drvs/vfkit/vfkit.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/docker/machine/libmachine/drivers"
2929
"github.com/google/uuid"
3030

31+
"k8s.io/minikube/pkg/drivers/common/virtiofs"
3132
"k8s.io/minikube/pkg/drivers/common/vmnet"
3233
"k8s.io/minikube/pkg/drivers/vfkit"
3334
"k8s.io/minikube/pkg/minikube/config"
@@ -88,6 +89,11 @@ func configure(cfg config.ClusterConfig, n config.Node) (interface{}, error) {
8889
return nil, fmt.Errorf("unsupported network: %q", cfg.Network)
8990
}
9091

92+
mounts, err := virtiofs.ValidateMountString(cfg.MountString)
93+
if err != nil {
94+
return nil, err
95+
}
96+
9197
return &vfkit.Driver{
9298
BaseDriver: &drivers.BaseDriver{
9399
MachineName: machineName,
@@ -99,6 +105,7 @@ func configure(cfg config.ClusterConfig, n config.Node) (interface{}, error) {
99105
Memory: cfg.Memory,
100106
CPU: cfg.CPUs,
101107
ExtraDisks: cfg.ExtraDisks,
108+
VirtiofsMounts: mounts,
102109
Network: cfg.Network,
103110
MACAddress: mac,
104111
VmnetHelper: helper,

site/content/en/docs/handbook/mount.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,26 @@ This directory may then be referenced from a Kubernetes manifest, for example:
6767

6868
Some hypervisors, have built-in host folder sharing. Driver mounts are reliable with good performance, but the paths are not predictable across operating systems or hypervisors:
6969

70-
| Driver | OS | HostFolder | VM |
71-
| --- | --- | --- | --- |
72-
| VirtualBox | Linux | /home | /hosthome |
73-
| VirtualBox | macOS | /Users | /Users |
74-
| VirtualBox | Windows | C://Users | /c/Users |
75-
| VMware Fusion | macOS | /Users | /mnt/hgfs/Users |
76-
| KVM | Linux | Unsupported | |
77-
| HyperKit | macOS | Supported | |
78-
79-
These mounts can be disabled by passing `--disable-driver-mounts` to `minikube start`.
80-
81-
HyperKit mounts can use the following flags:
82-
`--nfs-share=[]`: Local folders to share with Guest via NFS mounts
83-
`--nfs-shares-root='/nfsshares'`: Where to root the NFS Shares, defaults to /nfsshares
70+
| Driver | OS | Host | Guest |
71+
|----------------|---------|--------------|-------------------|
72+
| VirtualBox | Linux | /home | /hosthome |
73+
| VirtualBox | macOS | /Users | /Users |
74+
| VirtualBox | Windows | C://Users | /c/Users |
75+
| VMware Fusion | macOS | /Users | /mnt/hgfs/Users |
76+
| Vfkit | macOS | configurable | configurable |
77+
| Krunkit | macOS | configurable | configurable |
78+
| KVM | Linux | unsupported | |
79+
| HyperKit | macOS | configurable | configurable |
80+
81+
Built-in mounts can be disabled by passing `--disable-driver-mounts` to `minikube start`.
82+
83+
Vfkit and Krunkit drivers can use the following start flags:
84+
- `--mount-string`: Local folder to share with guest via Virtiofs. The folder is
85+
specified with a pair: `/host-path:/guest-path`.
86+
87+
HyperKit driver can use the following start flags:
88+
- `--nfs-share=[]`: Local folders to share with Guest via NFS mounts
89+
- `--nfs-shares-root='/nfsshares'`: Where to root the NFS Shares, defaults to /nfsshares
8490

8591
## File Sync
8692

0 commit comments

Comments
 (0)