Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/resource/definitions/enums/enums.proto
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,17 @@ enum BlockFilesystemType {
FILESYSTEM_TYPE_EXT4 = 3;
FILESYSTEM_TYPE_ISO9660 = 4;
FILESYSTEM_TYPE_SWAP = 5;
FILESYSTEM_TYPE_NFS = 6;
FILESYSTEM_TYPE_VIRTIOFS = 7;
}

// BlockNFSVersionType describes NFS version type.
enum BlockNFSVersionType {
NFS_VERSION_TYPE4_2 = 0;
NFS_VERSION_TYPE4_1 = 1;
NFS_VERSION_TYPE4 = 2;
NFS_VERSION_TYPE3 = 3;
NFS_VERSION_TYPE2 = 4;
}

// BlockVolumePhase describes volume phase.
Expand All @@ -727,6 +738,7 @@ enum BlockVolumeType {
VOLUME_TYPE_DIRECTORY = 3;
VOLUME_TYPE_SYMLINK = 4;
VOLUME_TYPE_OVERLAY = 5;
VOLUME_TYPE_EXTERNAL = 6;
}

// CriImageCacheStatus describes image cache status type.
Expand Down
89 changes: 89 additions & 0 deletions cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package flags

import (
"errors"
"fmt"
"strings"
)

// VirtiofsRequest is the configuration required for virtiofs share creation.
type VirtiofsRequest struct {
SharedDir string
SocketPath string
}

// ParseVirtiofsFlag parses the virtiofs flag into a slice of VirtiofsRequest.
func ParseVirtiofsFlag(disks []string) ([]VirtiofsRequest, error) {
result := []VirtiofsRequest{}

if len(disks) == 0 {
return nil, errors.New("at least one disk has to be specified")
}

for _, d := range disks {
parts := strings.SplitN(d, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid disk format: %q", d)
}

result = append(result, VirtiofsRequest{
SharedDir: parts[0],
SocketPath: parts[1],
})
}

return result, nil
}

// Virtiofs implements pflag.Value for accumulating multiple VirtiofsRequest entries.
type Virtiofs struct {
requests []VirtiofsRequest
}

// String returns a string representation suitable for flag printing.
func (f *Virtiofs) String() string {
if f == nil || len(f.requests) == 0 {
return ""
}

parts := make([]string, 0, len(f.requests))
for _, r := range f.requests {
parts = append(parts, fmt.Sprintf("%s:%s", r.SharedDir, r.SocketPath))
}

return strings.Join(parts, ",")
}

// Set parses and appends one or more disk specifications to the flag value.
// The input may contain a single spec ("sharedDir:socketPath") or a comma-separated list.
func (f *Virtiofs) Set(value string) error {
if strings.TrimSpace(value) == "" {
return errors.New("virtiofs value must not be empty")
}
// Support comma-separated values in a single Set call.
raw := strings.Split(value, ",")

reqs, err := ParseVirtiofsFlag(raw)
if err != nil {
return err
}

f.requests = append(f.requests, reqs...)

return nil
}

// Type returns the flag's value type name.
func (f *Virtiofs) Type() string { return "virtiofs" }

// Requests returns a defensive copy of the accumulated virtiofs share requests.
func (f *Virtiofs) Requests() []VirtiofsRequest {
out := make([]VirtiofsRequest, len(f.requests))
copy(out, f.requests)

return out
}
57 changes: 57 additions & 0 deletions cmd/talosctl/cmd/mgmt/cluster/create/flags/virtiofs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package flags_test

import (
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"

flags "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags"
)

func TestVirtiofsFlag_AccumulatesAndRequests(t *testing.T) {
t.Parallel()

var d flags.Virtiofs

fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
fs.Var(&d, "virtiofs", "")

args := []string{
"--virtiofs", "/mnt/shared/1:/tmp/mnt-shared-1.sock",
"--virtiofs", "/mnt/shared/2:/tmp/mnt-shared-2.sock,/mnt/shared/3:/tmp/mnt-shared-3.sock",
}

err := fs.Parse(args)
assert.NoError(t, err)

reqs := d.Requests()
assert.Len(t, reqs, 3)

assert.Equal(t, "/mnt/shared/1", reqs[0].SharedDir)
assert.Equal(t, "/tmp/mnt-shared-1.sock", reqs[0].SocketPath)

assert.Equal(t, "/mnt/shared/2", reqs[1].SharedDir)
assert.Equal(t, "/tmp/mnt-shared-2.sock", reqs[1].SocketPath)

assert.Equal(t, "/mnt/shared/3", reqs[2].SharedDir)
assert.Equal(t, "/tmp/mnt-shared-3.sock", reqs[2].SocketPath)

// Type should be stable
assert.Equal(t, "virtiofs", d.Type())

assert.Equal(t, "/mnt/shared/1:/tmp/mnt-shared-1.sock,/mnt/shared/2:/tmp/mnt-shared-2.sock,/mnt/shared/3:/tmp/mnt-shared-3.sock", d.String())
}

func TestVirtiofsFlag_SetInvalid(t *testing.T) {
t.Parallel()

var f flags.Virtiofs

err := f.Set("invalid-no-colon")
assert.Error(t, err)
}
52 changes: 52 additions & 0 deletions cmd/talosctl/cmd/mgmt/nfsd_launch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//go:build linux || darwin

package mgmt

import (
"net"
"strings"

"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"github.com/siderolabs/talos/pkg/provision/providers/vm"
)

var nfsdLaunchCmdFlags struct {
addr string
workdir string
}

// nfsdLaunchCmd represents the nfsd-launch command.
var nfsdLaunchCmd = &cobra.Command{
Use: "nfsd-launch",
Short: "Internal command used by VM provisioners",
Long: ``,
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
var ips []net.IP

for ip := range strings.SplitSeq(dnsdLaunchCmdFlags.addr, ",") {
ips = append(ips, net.ParseIP(ip))
}

var eg errgroup.Group

eg.Go(func() error {
return vm.NFSd(ips, dnsdLaunchCmdFlags.resolvConf)
})

return eg.Wait()
},
}

func init() {
nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.addr, "addr", "localhost:2049", `Address to bind nfsd to`)
nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.workdir, "workdir", "", `Working directory for nfsd`)
addCommand(nfsdLaunchCmd)
}
48 changes: 48 additions & 0 deletions cmd/talosctl/cmd/mgmt/virtiofsd_launch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//go:build linux || darwin

package mgmt

import (
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags"
"github.com/siderolabs/talos/pkg/provision/providers/vm"
)

var virtiofsdLaunchCmdFlags struct {
virtiofsdBin string
virtiofs flags.Virtiofs
}

// virtiofsdLaunchCmd represents the virtiofsd-launch command.
var virtiofsdLaunchCmd = &cobra.Command{
Use: "virtiofsd-launch",
Short: "Internal command used by VM provisioners",
Long: ``,
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
eg, ctx := errgroup.WithContext(cmd.Context())

for _, vfs := range virtiofsdLaunchCmdFlags.virtiofs.Requests() {
eg.Go(func() error {
return vm.Virtiofsd(ctx, virtiofsdLaunchCmdFlags.virtiofsdBin, vfs.SharedDir, vfs.SocketPath)
})
}

return eg.Wait()
},
}

func init() {
virtiofsdLaunchCmd.Flags().StringVar(&virtiofsdLaunchCmdFlags.virtiofsdBin, "bin",
"/usr/libexec/virtiofsd", `path to the virtiofsd binary`)
virtiofsdLaunchCmd.Flags().Var(&virtiofsdLaunchCmdFlags.virtiofs, "virtiofs",
`list of virtiofs shares to create in format "<share>:<socket>"`)
addCommand(virtiofsdLaunchCmd)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ require (
github.com/siderolabs/siderolink v0.3.15
github.com/siderolabs/talos/pkg/machinery v1.12.0-alpha.2
github.com/sirupsen/logrus v1.9.3
github.com/smallfz/libnfs-go v0.0.7
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,8 @@ github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72 h1:Boabco/vho
github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smallfz/libnfs-go v0.0.7 h1:7uS1ADMrJqihbQEtXfo8865+gEMl7bHKsDAY7ialKws=
github.com/smallfz/libnfs-go v0.0.7/go.mod h1:OtfrJ0akgDga0KIhtub2YJoDMnzHBntTAVwXdfNZZTU=
github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074 h1:05WpndnxEUV9TYnXWlR93QaeWHRk5+9cHeMmjGAK2CY=
github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389 h1:f/5NRv5IGZxbjBhc5MnlbNmyuXBPxvekhBAUzyKWyLY=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// Close the encrypted volumes.
func Close(ctx context.Context, logger *zap.Logger, volumeContext ManagerContext) error {
switch volumeContext.Cfg.TypedSpec().Type {
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
// volume types can be always closed
volumeContext.Status.Phase = block.VolumePhaseClosed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
volumeType := volumeContext.Cfg.TypedSpec().Type

switch volumeType {
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
// volume types above are always ready
volumeContext.Status.Phase = block.VolumePhaseReady

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var UserVolumeTransformers = []volumeConfigTransformer{
UserVolumeTransformer,
RawVolumeTransformer,
ExistingVolumeTransformer,
ExternalVolumeTransformer,
SwapVolumeTransformer,
}

Expand Down Expand Up @@ -123,7 +124,7 @@ func UserVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) {
WithConvertEncryptionConfiguration(userVolumeConfig.Encryption()).
WriterFunc()

case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
fallthrough

default:
Expand Down Expand Up @@ -210,6 +211,40 @@ func ExistingVolumeTransformer(c configconfig.Config) ([]VolumeResource, error)
return resources, nil
}

// ExternalVolumeTransformer is the transformer for external user volume configs.
func ExternalVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) {
if c == nil {
return nil, nil
}

resources := make([]VolumeResource, 0, len(c.ExternalVolumeConfigs()))

for _, externalVolumeConfig := range c.ExternalVolumeConfigs() {
volumeID := constants.ExternalVolumePrefix + externalVolumeConfig.Name()
resources = append(resources, VolumeResource{
VolumeID: volumeID,
Label: block.ExternalVolumeLabel,
TransformFunc: NewBuilder().
WithType(block.VolumeTypeExternal).
WithProvisioning(block.ProvisioningSpec{
Wave: block.WaveUserVolumes,
}).
WithMount(block.MountSpec{
TargetPath: externalVolumeConfig.Name(),
ParentID: constants.UserVolumeMountPoint,
SelinuxLabel: constants.EphemeralSelinuxLabel,
FileMode: 0o755,
UID: 0,
GID: 0,
}).
WriterFunc(),
MountTransformFunc: HandleExternalVolumeMountRequest(externalVolumeConfig),
})
}

return resources, nil
}

// SwapVolumeTransformer is the transformer for swap volume configs.
func SwapVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) {
if c == nil {
Expand Down Expand Up @@ -261,6 +296,15 @@ func HandleExistingVolumeMountRequest(existingVolumeConfig configconfig.Existing
}
}

// HandleExternalVolumeMountRequest returns a MountTransformFunc for external volumes.
func HandleExternalVolumeMountRequest(externalVolumeConfig configconfig.ExternalVolumeConfig) func(m *block.VolumeMountRequest) error {
return func(m *block.VolumeMountRequest) error {
m.TypedSpec().ReadOnly = externalVolumeConfig.Mount().ReadOnly()

return nil
}
}

// DefaultMountTransform is a no-op.
func DefaultMountTransform(_ *block.VolumeMountRequest) error {
return nil
Expand Down
Loading
Loading