Skip to content

Commit e010ef0

Browse files
committed
feat: external volumes
Add new volume type for managing external volume mounts - e.g. NFS or Virtiofs volumes Signed-off-by: Mateusz Urbanek <[email protected]>
1 parent 80ab7a0 commit e010ef0

File tree

37 files changed

+1811
-75
lines changed

37 files changed

+1811
-75
lines changed

api/resource/definitions/enums/enums.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,17 @@ enum BlockFilesystemType {
705705
FILESYSTEM_TYPE_EXT4 = 3;
706706
FILESYSTEM_TYPE_ISO9660 = 4;
707707
FILESYSTEM_TYPE_SWAP = 5;
708+
FILESYSTEM_TYPE_NFS = 6;
709+
FILESYSTEM_TYPE_VIRTIOFS = 7;
710+
}
711+
712+
// BlockNFSVersionType describes NFS version type.
713+
enum BlockNFSVersionType {
714+
NFS_VERSION_TYPE4_2 = 0;
715+
NFS_VERSION_TYPE4_1 = 1;
716+
NFS_VERSION_TYPE4 = 2;
717+
NFS_VERSION_TYPE3 = 3;
718+
NFS_VERSION_TYPE2 = 4;
708719
}
709720

710721
// BlockVolumePhase describes volume phase.
@@ -727,6 +738,7 @@ enum BlockVolumeType {
727738
VOLUME_TYPE_DIRECTORY = 3;
728739
VOLUME_TYPE_SYMLINK = 4;
729740
VOLUME_TYPE_OVERLAY = 5;
741+
VOLUME_TYPE_EXTERNAL = 6;
730742
}
731743

732744
// CriImageCacheStatus describes image cache status type.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package flags
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"strings"
11+
)
12+
13+
// VirtiofsRequest is the configuration required for virtiofs share creation.
14+
type VirtiofsRequest struct {
15+
SharedDir string
16+
SocketPath string
17+
}
18+
19+
// ParseVirtiofsFlag parses the virtiofs flag into a slice of VirtiofsRequest.
20+
func ParseVirtiofsFlag(disks []string) ([]VirtiofsRequest, error) {
21+
result := []VirtiofsRequest{}
22+
23+
if len(disks) == 0 {
24+
return nil, errors.New("at least one disk has to be specified")
25+
}
26+
27+
for _, d := range disks {
28+
parts := strings.SplitN(d, ":", 2)
29+
if len(parts) != 2 {
30+
return nil, fmt.Errorf("invalid disk format: %q", d)
31+
}
32+
33+
result = append(result, VirtiofsRequest{
34+
SharedDir: parts[0],
35+
SocketPath: parts[1],
36+
})
37+
}
38+
39+
return result, nil
40+
}
41+
42+
// Virtiofs implements pflag.Value for accumulating multiple VirtiofsRequest entries.
43+
type Virtiofs struct {
44+
requests []VirtiofsRequest
45+
}
46+
47+
// String returns a string representation suitable for flag printing.
48+
func (f *Virtiofs) String() string {
49+
if f == nil || len(f.requests) == 0 {
50+
return ""
51+
}
52+
53+
parts := make([]string, 0, len(f.requests))
54+
for _, r := range f.requests {
55+
parts = append(parts, fmt.Sprintf("%s:%s", r.SharedDir, r.SocketPath))
56+
}
57+
58+
return strings.Join(parts, ",")
59+
}
60+
61+
// Set parses and appends one or more disk specifications to the flag value.
62+
// The input may contain a single spec ("sharedDir:socketPath") or a comma-separated list.
63+
func (f *Virtiofs) Set(value string) error {
64+
if strings.TrimSpace(value) == "" {
65+
return errors.New("virtiofs value must not be empty")
66+
}
67+
// Support comma-separated values in a single Set call.
68+
raw := strings.Split(value, ",")
69+
70+
reqs, err := ParseVirtiofsFlag(raw)
71+
if err != nil {
72+
return err
73+
}
74+
75+
f.requests = append(f.requests, reqs...)
76+
77+
return nil
78+
}
79+
80+
// Type returns the flag's value type name.
81+
func (f *Virtiofs) Type() string { return "virtiofs" }
82+
83+
// Requests returns a defensive copy of the accumulated virtiofs share requests.
84+
func (f *Virtiofs) Requests() []VirtiofsRequest {
85+
out := make([]VirtiofsRequest, len(f.requests))
86+
copy(out, f.requests)
87+
88+
return out
89+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package flags_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/spf13/pflag"
11+
"github.com/stretchr/testify/assert"
12+
13+
flags "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags"
14+
)
15+
16+
func TestVirtiofsFlag_AccumulatesAndRequests(t *testing.T) {
17+
t.Parallel()
18+
19+
var d flags.Virtiofs
20+
21+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
22+
fs.Var(&d, "virtiofs", "")
23+
24+
args := []string{
25+
"--virtiofs", "/mnt/shared/1:/tmp/mnt-shared-1.sock",
26+
"--virtiofs", "/mnt/shared/2:/tmp/mnt-shared-2.sock,/mnt/shared/3:/tmp/mnt-shared-3.sock",
27+
}
28+
29+
err := fs.Parse(args)
30+
assert.NoError(t, err)
31+
32+
reqs := d.Requests()
33+
assert.Len(t, reqs, 3)
34+
35+
assert.Equal(t, "/mnt/shared/1", reqs[0].SharedDir)
36+
assert.Equal(t, "/tmp/mnt-shared-1.sock", reqs[0].SocketPath)
37+
38+
assert.Equal(t, "/mnt/shared/2", reqs[1].SharedDir)
39+
assert.Equal(t, "/tmp/mnt-shared-2.sock", reqs[1].SocketPath)
40+
41+
assert.Equal(t, "/mnt/shared/3", reqs[2].SharedDir)
42+
assert.Equal(t, "/tmp/mnt-shared-3.sock", reqs[2].SocketPath)
43+
44+
// Type should be stable
45+
assert.Equal(t, "virtiofs", d.Type())
46+
47+
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())
48+
}
49+
50+
func TestVirtiofsFlag_SetInvalid(t *testing.T) {
51+
t.Parallel()
52+
53+
var f flags.Virtiofs
54+
55+
err := f.Set("invalid-no-colon")
56+
assert.Error(t, err)
57+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
//go:build linux || darwin
6+
7+
package mgmt
8+
9+
import (
10+
"net"
11+
"strings"
12+
13+
"github.com/spf13/cobra"
14+
"golang.org/x/sync/errgroup"
15+
16+
"github.com/siderolabs/talos/pkg/provision/providers/vm"
17+
)
18+
19+
var nfsdLaunchCmdFlags struct {
20+
addr string
21+
workdir string
22+
}
23+
24+
// nfsdLaunchCmd represents the nfsd-launch command.
25+
var nfsdLaunchCmd = &cobra.Command{
26+
Use: "nfsd-launch",
27+
Short: "Internal command used by VM provisioners",
28+
Long: ``,
29+
Args: cobra.NoArgs,
30+
Hidden: true,
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
var ips []net.IP
33+
34+
for ip := range strings.SplitSeq(dnsdLaunchCmdFlags.addr, ",") {
35+
ips = append(ips, net.ParseIP(ip))
36+
}
37+
38+
var eg errgroup.Group
39+
40+
eg.Go(func() error {
41+
return vm.NFSd(ips, dnsdLaunchCmdFlags.resolvConf)
42+
})
43+
44+
return eg.Wait()
45+
},
46+
}
47+
48+
func init() {
49+
nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.addr, "addr", "localhost:2049", `Address to bind nfsd to`)
50+
nfsdLaunchCmd.Flags().StringVar(&nfsdLaunchCmdFlags.workdir, "workdir", "", `Working directory for nfsd`)
51+
addCommand(nfsdLaunchCmd)
52+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
//go:build linux || darwin
6+
7+
package mgmt
8+
9+
import (
10+
"github.com/spf13/cobra"
11+
"golang.org/x/sync/errgroup"
12+
13+
"github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/cluster/create/flags"
14+
"github.com/siderolabs/talos/pkg/provision/providers/vm"
15+
)
16+
17+
var virtiofsdLaunchCmdFlags struct {
18+
virtiofsdBin string
19+
virtiofs flags.Virtiofs
20+
}
21+
22+
// virtiofsdLaunchCmd represents the virtiofsd-launch command.
23+
var virtiofsdLaunchCmd = &cobra.Command{
24+
Use: "virtiofsd-launch",
25+
Short: "Internal command used by VM provisioners",
26+
Long: ``,
27+
Args: cobra.NoArgs,
28+
Hidden: true,
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
eg, ctx := errgroup.WithContext(cmd.Context())
31+
32+
for _, vfs := range virtiofsdLaunchCmdFlags.virtiofs.Requests() {
33+
eg.Go(func() error {
34+
return vm.Virtiofsd(ctx, virtiofsdLaunchCmdFlags.virtiofsdBin, vfs.SharedDir, vfs.SocketPath)
35+
})
36+
}
37+
38+
return eg.Wait()
39+
},
40+
}
41+
42+
func init() {
43+
virtiofsdLaunchCmd.Flags().StringVar(&virtiofsdLaunchCmdFlags.virtiofsdBin, "bin",
44+
"/usr/libexec/virtiofsd", `path to the virtiofsd binary`)
45+
virtiofsdLaunchCmd.Flags().Var(&virtiofsdLaunchCmdFlags.virtiofs, "virtiofs",
46+
`list of virtiofs shares to create in format "<share>:<socket>"`)
47+
addCommand(virtiofsdLaunchCmd)
48+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ require (
162162
github.com/siderolabs/siderolink v0.3.15
163163
github.com/siderolabs/talos/pkg/machinery v1.12.0-alpha.2
164164
github.com/sirupsen/logrus v1.9.3
165+
github.com/smallfz/libnfs-go v0.0.7
165166
github.com/spf13/cobra v1.10.1
166167
github.com/spf13/pflag v1.0.10
167168
github.com/stretchr/testify v1.11.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,8 @@ github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72 h1:Boabco/vho
671671
github.com/siderolabs/wgctrl-go v0.0.0-20251029173431-c4fd5f6a4e72/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
672672
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
673673
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
674+
github.com/smallfz/libnfs-go v0.0.7 h1:7uS1ADMrJqihbQEtXfo8865+gEMl7bHKsDAY7ialKws=
675+
github.com/smallfz/libnfs-go v0.0.7/go.mod h1:OtfrJ0akgDga0KIhtub2YJoDMnzHBntTAVwXdfNZZTU=
674676
github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074 h1:05WpndnxEUV9TYnXWlR93QaeWHRk5+9cHeMmjGAK2CY=
675677
github.com/smira/containerd/v2 v2.0.0-20251113120816-51ecd22de074/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
676678
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389 h1:f/5NRv5IGZxbjBhc5MnlbNmyuXBPxvekhBAUzyKWyLY=

internal/app/machined/pkg/controllers/block/internal/volumes/close.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
// Close the encrypted volumes.
1919
func Close(ctx context.Context, logger *zap.Logger, volumeContext ManagerContext) error {
2020
switch volumeContext.Cfg.TypedSpec().Type {
21-
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
21+
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
2222
// volume types can be always closed
2323
volumeContext.Status.Phase = block.VolumePhaseClosed
2424

internal/app/machined/pkg/controllers/block/internal/volumes/locate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func LocateAndProvision(ctx context.Context, logger *zap.Logger, volumeContext M
3131
volumeType := volumeContext.Cfg.TypedSpec().Type
3232

3333
switch volumeType {
34-
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
34+
case block.VolumeTypeTmpfs, block.VolumeTypeDirectory, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
3535
// volume types above are always ready
3636
volumeContext.Status.Phase = block.VolumePhaseReady
3737

internal/app/machined/pkg/controllers/block/internal/volumes/volumeconfig/user_volumes.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var UserVolumeTransformers = []volumeConfigTransformer{
2828
UserVolumeTransformer,
2929
RawVolumeTransformer,
3030
ExistingVolumeTransformer,
31+
ExternalVolumeTransformer,
3132
SwapVolumeTransformer,
3233
}
3334

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

126-
case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay:
127+
case block.VolumeTypeTmpfs, block.VolumeTypeSymlink, block.VolumeTypeOverlay, block.VolumeTypeExternal:
127128
fallthrough
128129

129130
default:
@@ -210,6 +211,40 @@ func ExistingVolumeTransformer(c configconfig.Config) ([]VolumeResource, error)
210211
return resources, nil
211212
}
212213

214+
// ExternalVolumeTransformer is the transformer for external user volume configs.
215+
func ExternalVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) {
216+
if c == nil {
217+
return nil, nil
218+
}
219+
220+
resources := make([]VolumeResource, 0, len(c.ExternalVolumeConfigs()))
221+
222+
for _, externalVolumeConfig := range c.ExternalVolumeConfigs() {
223+
volumeID := constants.ExternalVolumePrefix + externalVolumeConfig.Name()
224+
resources = append(resources, VolumeResource{
225+
VolumeID: volumeID,
226+
Label: block.ExternalVolumeLabel,
227+
TransformFunc: NewBuilder().
228+
WithType(block.VolumeTypeExternal).
229+
WithProvisioning(block.ProvisioningSpec{
230+
Wave: block.WaveUserVolumes,
231+
}).
232+
WithMount(block.MountSpec{
233+
TargetPath: externalVolumeConfig.Name(),
234+
ParentID: constants.UserVolumeMountPoint,
235+
SelinuxLabel: constants.EphemeralSelinuxLabel,
236+
FileMode: 0o755,
237+
UID: 0,
238+
GID: 0,
239+
}).
240+
WriterFunc(),
241+
MountTransformFunc: HandleExternalVolumeMountRequest(externalVolumeConfig),
242+
})
243+
}
244+
245+
return resources, nil
246+
}
247+
213248
// SwapVolumeTransformer is the transformer for swap volume configs.
214249
func SwapVolumeTransformer(c configconfig.Config) ([]VolumeResource, error) {
215250
if c == nil {
@@ -261,6 +296,15 @@ func HandleExistingVolumeMountRequest(existingVolumeConfig configconfig.Existing
261296
}
262297
}
263298

299+
// HandleExternalVolumeMountRequest returns a MountTransformFunc for external volumes.
300+
func HandleExternalVolumeMountRequest(externalVolumeConfig configconfig.ExternalVolumeConfig) func(m *block.VolumeMountRequest) error {
301+
return func(m *block.VolumeMountRequest) error {
302+
m.TypedSpec().ReadOnly = externalVolumeConfig.Mount().ReadOnly()
303+
304+
return nil
305+
}
306+
}
307+
264308
// DefaultMountTransform is a no-op.
265309
func DefaultMountTransform(_ *block.VolumeMountRequest) error {
266310
return nil

0 commit comments

Comments
 (0)