Skip to content

Commit 35446b7

Browse files
nirsmedyagh
authored andcommitted
drivers: Add common/virtiofs package
The package provides functions for parsing and validating mount string and setting up the mount inside the guest.
1 parent 6f6c54d commit 35446b7

File tree

2 files changed

+293
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)