drivers: Add common/virtiofs package
The package provides functions for parsing and validating mount string and setting up the mount inside the guest.pull/21388/head
parent
6f6c54d5e9
commit
35446b7bd5
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
Copyright 2025 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 virtiofs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/machine/libmachine/drivers"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Mount is a directory on the host shared with the guest using virtiofs.
|
||||
type Mount struct {
|
||||
// HostPath is an absolute path to existing directory to share with the
|
||||
// guest via virtiofs protocol. Also called "source" by some tools.
|
||||
HostPath string
|
||||
|
||||
// GuestPath is a path in the guest for mounting the shared directory using
|
||||
// virtiofs. Also called target or mountpoint by some tools.
|
||||
GuestPath string
|
||||
|
||||
// Tag is a string identifying the shared file system in the guest.
|
||||
// Generated by minikube.
|
||||
Tag string
|
||||
}
|
||||
|
||||
// ValidateMountString parses the mount-string flag and validates that the
|
||||
// specified paths can be used for virtiofs mount. Returns list with one
|
||||
// validated mount, ready for configuring the driver.
|
||||
// TODO: Drop when we have a flag supporting multiple mounts.
|
||||
func ValidateMountString(s string) ([]*Mount, error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return validateMounts([]string{s})
|
||||
}
|
||||
|
||||
func validateMounts(args []string) ([]*Mount, error) {
|
||||
var mounts []*Mount
|
||||
|
||||
seenHost := map[string]*Mount{}
|
||||
seenGuest := map[string]*Mount{}
|
||||
|
||||
for _, s := range args {
|
||||
mount, err := ParseMount(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mount.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existing, ok := seenHost[mount.HostPath]; ok {
|
||||
return nil, fmt.Errorf("host path %q is already shared at guest path %q", mount.HostPath, existing.GuestPath)
|
||||
}
|
||||
seenHost[mount.HostPath] = mount
|
||||
|
||||
if existing, ok := seenGuest[mount.GuestPath]; ok {
|
||||
return nil, fmt.Errorf("guest path %q is already shared from host path %q", mount.GuestPath, existing.HostPath)
|
||||
}
|
||||
seenGuest[mount.GuestPath] = mount
|
||||
|
||||
mounts = append(mounts, mount)
|
||||
}
|
||||
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
// ParseMount parses a string in the format "/host-path:/guest-path" and returns
|
||||
// a new Mount instance. The mount must be validated before using it to
|
||||
// configure the driver.
|
||||
func ParseMount(s string) (*Mount, error) {
|
||||
pair := strings.SplitN(s, ":", 2)
|
||||
if len(pair) != 2 {
|
||||
return nil, fmt.Errorf("invalid virtiofs mount %q: (expected '/host-path:/guest-path')", s)
|
||||
}
|
||||
|
||||
return &Mount{
|
||||
HostPath: pair[0],
|
||||
GuestPath: pair[1],
|
||||
Tag: uuid.NewString(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate that the mount can be used for virtiofs device configuration. Both
|
||||
// host and guest paths must be absolute. Host path must be a directory and must
|
||||
// not include virtiofs configuration separator (",").
|
||||
func (m *Mount) Validate() error {
|
||||
// "," is a --device configuration separator in vfkit and krunkit.
|
||||
if strings.Contains(m.HostPath, ",") {
|
||||
return fmt.Errorf("host path %q must not contain ','", m.HostPath)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(m.HostPath) {
|
||||
return fmt.Errorf("host path %q is not an absolute path", m.HostPath)
|
||||
}
|
||||
|
||||
if fs, err := os.Stat(m.HostPath); err != nil {
|
||||
return fmt.Errorf("failed to validate host path %q: %w", m.HostPath, err)
|
||||
} else if !fs.IsDir() {
|
||||
return fmt.Errorf("host path %q is not a directory", m.HostPath)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(m.GuestPath) {
|
||||
return fmt.Errorf("guest path %q is not an absolute path", m.GuestPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupMounts connects to the host via SSH, creates the mount directory if
|
||||
// needed, and mount the virtiofs file system. It should be called by
|
||||
// driver.Start().
|
||||
func SetupMounts(d drivers.Driver, mounts []*Mount) error {
|
||||
var script strings.Builder
|
||||
|
||||
script.WriteString("set -e\n")
|
||||
|
||||
for _, mount := range mounts {
|
||||
script.WriteString(fmt.Sprintf("sudo mkdir -p \"%s\"\n", mount.GuestPath))
|
||||
script.WriteString(fmt.Sprintf("sudo mount -t virtiofs %s \"%s\"\n", mount.Tag, mount.GuestPath))
|
||||
}
|
||||
|
||||
if _, err := drivers.RunSSHCommandFromDriver(d, script.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
Copyright 2025 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 virtiofs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"k8s.io/minikube/pkg/drivers/common/virtiofs"
|
||||
)
|
||||
|
||||
func TestVirtiofsValidateEmptyMountString(t *testing.T) {
|
||||
mounts, err := virtiofs.ValidateMountString("")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse empty mount string: %s", err)
|
||||
}
|
||||
if mounts != nil {
|
||||
t.Fatalf("expected nil mounts, got %v", mounts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtiofsValidateMountString(t *testing.T) {
|
||||
hostPath := t.TempDir()
|
||||
guestPath := "/mnt/models"
|
||||
mountString := fmt.Sprintf("%s:%s", hostPath, guestPath)
|
||||
|
||||
mounts, err := virtiofs.ValidateMountString(mountString)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse mountString %q: %s", mountString, err)
|
||||
}
|
||||
if len(mounts) != 1 {
|
||||
t.Fatalf("expected a single mount, got %v", mounts)
|
||||
}
|
||||
|
||||
mount := mounts[0]
|
||||
if mount.HostPath != hostPath {
|
||||
t.Fatalf("expected host path %q, got %q", hostPath, mount.HostPath)
|
||||
}
|
||||
if mount.GuestPath != guestPath {
|
||||
t.Fatalf("expected guest path %q, got %q", guestPath, mount.GuestPath)
|
||||
}
|
||||
|
||||
tag, err := uuid.Parse(mount.Tag)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse UUID from mount tag: %s", err)
|
||||
}
|
||||
if tag.Version() != 4 {
|
||||
t.Fatalf("mount tag is not a random UUID")
|
||||
}
|
||||
|
||||
if err := mount.Validate(); err != nil {
|
||||
t.Fatalf("mount is not valid: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtiofsParseInvalidMountString(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mountString string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
mountString: "",
|
||||
},
|
||||
{
|
||||
name: "guest path is missing",
|
||||
mountString: "host-path",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mount, err := virtiofs.ParseMount(tt.mountString)
|
||||
if err == nil {
|
||||
t.Fatalf("invalid mount string %q did not fail to parse", tt.mountString)
|
||||
}
|
||||
if mount != nil {
|
||||
t.Fatalf("expected nil mount for %q, got %v", tt.mountString, mount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtiofsValidateInvalidMount(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
missing := filepath.Join(dir, "missing")
|
||||
file := filepath.Join(dir, "file")
|
||||
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mountString string
|
||||
}{
|
||||
{
|
||||
name: "host path contains virtiofs config separator",
|
||||
mountString: "/host,path:/guest-path",
|
||||
},
|
||||
{
|
||||
name: "host path is relative",
|
||||
mountString: "host-path:/guest-path",
|
||||
},
|
||||
{
|
||||
name: "guest path is relative",
|
||||
mountString: fmt.Sprintf("%s:guest-path", dir),
|
||||
},
|
||||
{
|
||||
name: "host path is missing",
|
||||
mountString: fmt.Sprintf("%s:/guest-path", missing),
|
||||
},
|
||||
{
|
||||
name: "host path is not a directory",
|
||||
mountString: fmt.Sprintf("%s:/guest-path", file),
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mount, err := virtiofs.ParseMount(tt.mountString)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse mount string %q: %s", tt.mountString, err)
|
||||
}
|
||||
if err := mount.Validate(); err == nil {
|
||||
t.Fatalf("invalid mount %q did not failed validation", tt.mountString)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue