From da824bad01c7d368b0924b4506845b53e17632a6 Mon Sep 17 00:00:00 2001 From: Sam Berning Date: Tue, 27 Dec 2022 15:11:24 -0800 Subject: [PATCH] feat: saves containerd user data to a persistent disk this allows users to retain downloaded images, containers, etc. across new installations of finch Signed-off-by: Sam Berning --- cmd/finch/main.go | 2 + cmd/finch/virtual_machine.go | 4 +- cmd/finch/virtual_machine_init.go | 19 +- cmd/finch/virtual_machine_init_test.go | 25 ++- cmd/finch/virtual_machine_test.go | 2 +- finch.yaml | 21 +- go.mod | 16 ++ pkg/disk/disk.go | 130 +++++++++++ pkg/disk/disk_test.go | 123 +++++++++++ pkg/disk/limactl_disk.go | 13 ++ pkg/mocks/pkg_disk_disk.go | 289 +++++++++++++++++++++++++ pkg/path/finch.go | 6 + pkg/path/finch_test.go | 7 + 13 files changed, 638 insertions(+), 19 deletions(-) create mode 100644 pkg/disk/disk.go create mode 100644 pkg/disk/disk_test.go create mode 100644 pkg/disk/limactl_disk.go create mode 100644 pkg/mocks/pkg_disk_disk.go diff --git a/cmd/finch/main.go b/cmd/finch/main.go index f7c7f3ad8..0b835ae89 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -6,6 +6,7 @@ package main import ( "fmt" + "github.com/runfinch/finch/pkg/disk" "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/config" @@ -108,6 +109,7 @@ func virtualMachineCommands( config.NewNerdctlApplier(fssh.NewDialer(), fs, fp.LimaSSHPrivateKeyPath(), system.NewStdLib()), fp, fs, + disk.NewUserDataDiskManager(lcc, &afero.OsFs{}, fp, system.NewStdLib().Env("HOME")), ) } diff --git a/cmd/finch/virtual_machine.go b/cmd/finch/virtual_machine.go index fb8d4ebd3..1f6e15c90 100644 --- a/cmd/finch/virtual_machine.go +++ b/cmd/finch/virtual_machine.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "github.com/runfinch/finch/pkg/disk" "strings" "github.com/runfinch/finch/pkg/command" @@ -30,6 +31,7 @@ func newVirtualMachineCommand( nca config.NerdctlConfigApplier, fp path.Finch, fs afero.Fs, + diskManager disk.UserDataDiskManager, ) *cobra.Command { virtualMachineCommand := &cobra.Command{ Use: virtualMachineRootCmd, @@ -40,7 +42,7 @@ func newVirtualMachineCommand( newStartVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fs, fp.LimaSSHPrivateKeyPath()), newStopVMCommand(limaCmdCreator, logger), newRemoveVMCommand(limaCmdCreator, logger), - newInitVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fp.BaseYamlFilePath(), fs, fp.LimaSSHPrivateKeyPath()), + newInitVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fp.BaseYamlFilePath(), fs, fp.LimaSSHPrivateKeyPath(), diskManager), ) return virtualMachineCommand diff --git a/cmd/finch/virtual_machine_init.go b/cmd/finch/virtual_machine_init.go index 941bbc85d..581caa13f 100644 --- a/cmd/finch/virtual_machine_init.go +++ b/cmd/finch/virtual_machine_init.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "github.com/runfinch/finch/pkg/disk" "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/config" @@ -25,11 +26,12 @@ func newInitVMCommand( baseYamlFilePath string, fs afero.Fs, privateKeyPath string, + diskManager disk.UserDataDiskManager, ) *cobra.Command { initVMCommand := &cobra.Command{ Use: "init", Short: "Initialize the virtual machine", - RunE: newInitVMAction(lcc, logger, optionalDepGroups, lca, baseYamlFilePath).runAdapter, + RunE: newInitVMAction(lcc, logger, optionalDepGroups, lca, baseYamlFilePath, diskManager).runAdapter, PostRunE: newPostVMStartInitAction(logger, lcc, fs, privateKeyPath, nca).runAdapter, } @@ -42,6 +44,7 @@ type initVMAction struct { logger flog.Logger optionalDepGroups []*dependency.Group limaConfigApplier config.LimaConfigApplier + diskManager disk.UserDataDiskManager } func newInitVMAction( @@ -50,9 +53,10 @@ func newInitVMAction( optionalDepGroups []*dependency.Group, lca config.LimaConfigApplier, baseYamlFilePath string, + diskManager disk.UserDataDiskManager, ) *initVMAction { return &initVMAction{ - creator: creator, logger: logger, optionalDepGroups: optionalDepGroups, limaConfigApplier: lca, baseYamlFilePath: baseYamlFilePath, + creator: creator, logger: logger, optionalDepGroups: optionalDepGroups, limaConfigApplier: lca, baseYamlFilePath: baseYamlFilePath, diskManager: diskManager, } } @@ -61,7 +65,7 @@ func (iva *initVMAction) runAdapter(cmd *cobra.Command, args []string) error { } func (iva *initVMAction) run() error { - err := iva.assertVMIsNonexistent(iva.creator, iva.logger) + err := iva.assertVMIsNonexistent() if err != nil { return err } @@ -76,6 +80,11 @@ func (iva *initVMAction) run() error { return err } + err = iva.diskManager.InitializeUserDataDisk(); + if err != nil { + return err + } + instanceName := fmt.Sprintf("--name=%v", limaInstanceName) limaCmd := iva.creator.CreateWithoutStdio("start", instanceName, iva.baseYamlFilePath, "--tty=false") iva.logger.Info("Initializing and starting Finch virtual machine...") @@ -88,8 +97,8 @@ func (iva *initVMAction) run() error { return nil } -func (iva *initVMAction) assertVMIsNonexistent(creator command.LimaCmdCreator, logger flog.Logger) error { - status, err := lima.GetVMStatus(creator, logger, limaInstanceName) +func (iva *initVMAction) assertVMIsNonexistent() error { + status, err := lima.GetVMStatus(iva.creator, iva.logger, limaInstanceName) if err != nil { return err } diff --git a/cmd/finch/virtual_machine_init_test.go b/cmd/finch/virtual_machine_init_test.go index 2e3b55641..2ba35cb6e 100644 --- a/cmd/finch/virtual_machine_init_test.go +++ b/cmd/finch/virtual_machine_init_test.go @@ -21,7 +21,7 @@ const mockBaseYamlFilePath = "/os/os.yaml" func TestNewInitVMCommand(t *testing.T) { t.Parallel() - cmd := newInitVMCommand(nil, nil, nil, nil, nil, "", nil, "") + cmd := newInitVMCommand(nil, nil, nil, nil, nil, "", nil, "", nil) assert.Equal(t, cmd.Name(), "init") } @@ -37,6 +37,7 @@ func TestInitVMAction_runAdapter(t *testing.T) { *mocks.LimaCmdCreator, *mocks.Logger, *mocks.LimaConfigApplier, + *mocks.MockUserDataDiskManager, *gomock.Controller, ) }{ @@ -61,6 +62,7 @@ func TestInitVMAction_runAdapter(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -70,6 +72,7 @@ func TestInitVMAction_runAdapter(t *testing.T) { command := mocks.NewCommand(ctrl) lca.EXPECT().Apply().Return(nil) + dm.EXPECT().InitializeUserDataDisk().Return(nil) lcc.EXPECT().CreateWithoutStdio("start", fmt.Sprintf("--name=%s", limaInstanceName), mockBaseYamlFilePath, "--tty=false").Return(command) command.EXPECT().CombinedOutput() @@ -89,11 +92,12 @@ func TestInitVMAction_runAdapter(t *testing.T) { logger := mocks.NewLogger(ctrl) lcc := mocks.NewLimaCmdCreator(ctrl) lca := mocks.NewLimaConfigApplier(ctrl) + dm := mocks.NewMockUserDataDiskManager(ctrl) groups := tc.groups(ctrl) - tc.mockSvc(lcc, logger, lca, ctrl) + tc.mockSvc(lcc, logger, lca, dm, ctrl) - assert.NoError(t, newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath).runAdapter(tc.command, tc.args)) + assert.NoError(t, newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath, dm).runAdapter(tc.command, tc.args)) }) } } @@ -109,6 +113,7 @@ func TestInitVMAction_run(t *testing.T) { *mocks.LimaCmdCreator, *mocks.Logger, *mocks.LimaConfigApplier, + *mocks.MockUserDataDiskManager, *gomock.Controller, ) }{ @@ -122,6 +127,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -130,6 +136,7 @@ func TestInitVMAction_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "") lca.EXPECT().Apply().Return(nil) + dm.EXPECT().InitializeUserDataDisk().Return(nil) command := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("start", fmt.Sprintf("--name=%s", limaInstanceName), @@ -150,6 +157,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -170,6 +178,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -188,6 +197,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -206,6 +216,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -234,6 +245,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -257,6 +269,7 @@ func TestInitVMAction_run(t *testing.T) { lcc *mocks.LimaCmdCreator, logger *mocks.Logger, lca *mocks.LimaConfigApplier, + dm *mocks.MockUserDataDiskManager, ctrl *gomock.Controller, ) { getVMStatusC := mocks.NewCommand(ctrl) @@ -265,6 +278,7 @@ func TestInitVMAction_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "") lca.EXPECT().Apply().Return(nil) + dm.EXPECT().InitializeUserDataDisk().Return(nil) logs := []byte("stdout + stderr") command := mocks.NewCommand(ctrl) @@ -287,11 +301,12 @@ func TestInitVMAction_run(t *testing.T) { logger := mocks.NewLogger(ctrl) lcc := mocks.NewLimaCmdCreator(ctrl) lca := mocks.NewLimaConfigApplier(ctrl) + dm := mocks.NewMockUserDataDiskManager(ctrl) groups := tc.groups(ctrl) - tc.mockSvc(lcc, logger, lca, ctrl) + tc.mockSvc(lcc, logger, lca, dm, ctrl) - err := newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath).run() + err := newInitVMAction(lcc, logger, groups, lca, mockBaseYamlFilePath, dm).run() assert.Equal(t, err, tc.wantErr) }) } diff --git a/cmd/finch/virtual_machine_test.go b/cmd/finch/virtual_machine_test.go index 0a8d9f38d..3571a7233 100644 --- a/cmd/finch/virtual_machine_test.go +++ b/cmd/finch/virtual_machine_test.go @@ -17,7 +17,7 @@ import ( func TestVirtualMachineCommand(t *testing.T) { t.Parallel() - cmd := newVirtualMachineCommand(nil, nil, nil, nil, nil, "", nil) + cmd := newVirtualMachineCommand(nil, nil, nil, nil, nil, "", nil, nil) assert.Equal(t, cmd.Use, virtualMachineRootCmd) // check the number of subcommand for vm diff --git a/finch.yaml b/finch.yaml index 0d6c68c3c..d36457b48 100644 --- a/finch.yaml +++ b/finch.yaml @@ -81,6 +81,14 @@ mounts: # 🟢 Builtin default: "reverse-sshfs" mountType: reverse-sshfs +# Lima disks to attach to the instance. The disks will be accessible from inside the +# instance, labeled by name. (e.g. if the disk is named "data", it will be labeled +# "lima-data" inside the instance). The disk will be mounted inside the instance at +# `/mnt/lima-${VOLUME}`. +# 🟢 Builtin default: null +additionalDisks: +- "finch" + ssh: # A localhost port of the host. Forwarded to port 22 of the guest. # 🟢 Builtin default: 0 (automatically assigned to a free port) @@ -136,13 +144,12 @@ provision: systemctl reset-failed NetworkManager-wait-online.service systemctl mask NetworkManager-wait-online.service # # `user` is executed without the root privilege -# - mode: user -# script: | -# #!/bin/bash -# set -eux -o pipefail -# cat < ~/.vimrc -# set number -# EOF +- mode: user + script: | + #!/bin/bash + sudo chown $USER /mnt/lima-finch + sudo mount --bind /mnt/lima-finch ~/.local/share/containerd + systemctl --user restart containerd.service # Probe scripts to check readiness. # 🟢 Builtin default: null diff --git a/go.mod b/go.mod index e9c778171..3c53490b5 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,22 @@ require ( inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect ) +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/containers/gvisor-tap-vsock v0.4.1-0.20220920072955-5b1aff8ba743 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + gvisor.dev/gvisor v0.0.0-20220908032458-edc830a43ba6 // indirect + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.5.0 diff --git a/pkg/disk/disk.go b/pkg/disk/disk.go new file mode 100644 index 000000000..7bfd2761b --- /dev/null +++ b/pkg/disk/disk.go @@ -0,0 +1,130 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package disk + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/path" + "github.com/spf13/afero" + "io/fs" +) + +const ( + diskName = "finch" +) + +type UserDataDiskManager interface { + InitializeUserDataDisk() error +} + +type DiskFS interface { + afero.Fs + afero.Linker + afero.LinkReader +} + +type userDataDiskManager struct { + lcc command.LimaCmdCreator + fs DiskFS + finch path.Finch + homeDir string +} + +func NewUserDataDiskManager( + lcc command.LimaCmdCreator, + fs DiskFS, + finch path.Finch, + homeDir string, +) UserDataDiskManager { + return &userDataDiskManager{ + lcc: lcc, + fs: fs, + finch: finch, + homeDir: homeDir, + } +} + +func (m *userDataDiskManager) InitializeUserDataDisk() error { + if m.limaDiskExists() { + limaPath := fmt.Sprintf("%s/_disks/%s/datadisk", m.finch.LimaHomePath(), diskName) + loc, err := m.fs.ReadlinkIfPossible(limaPath) + if err != nil { + return err + } + // if the file is not m symlink, loc will be an empty string + // both os.Readlink() and UserDataDiskPath return absolute paths, so they will be equal if equivalent + if loc != m.finch.UserDataDiskPath(m.homeDir) { + err := m.attachPersistentDiskToLimaDisk() + if err != nil { + return err + } + } + return nil + } + if err := m.createLimaDisk(); err != nil { + return err + } + err := m.attachPersistentDiskToLimaDisk() + if err != nil { + return err + } + return nil +} + +func (m *userDataDiskManager) persistentDiskExists() bool { + _, err := m.fs.Stat(m.finch.UserDataDiskPath(m.homeDir)) + return err == nil +} + +func (m *userDataDiskManager) limaDiskExists() bool { + cmd := m.lcc.CreateWithoutStdio("disk", "ls", diskName, "--json") + out, err := cmd.Output() + if err != nil { + return false + } + diskListOutput := &limactlDiskListOutput{} + err = json.Unmarshal(out, diskListOutput) + if err != nil { + return false + } + return diskListOutput.Name == diskName +} + +func (m *userDataDiskManager) createLimaDisk() error { + cmd := m.lcc.CreateWithoutStdio("disk", "create", diskName, "--size=50G") + return cmd.Run() +} + +func (m *userDataDiskManager) attachPersistentDiskToLimaDisk() error { + limaPath := fmt.Sprintf("%s/_disks/%s/datadisk", m.finch.LimaHomePath(), diskName) + if !m.persistentDiskExists() { + err := m.fs.Rename(limaPath, m.finch.UserDataDiskPath(m.homeDir)) + if err != nil { + return err + } + } + + // if m datadisk already exists in the lima path, SymlinkIfPossible will no-op. + // to ensure that it symlinks properly, we have to delete the disk in the lima path + _, err := m.fs.Stat(limaPath) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + } else { + err = m.fs.Remove(limaPath) + if err != nil { + return err + } + } + + err = m.fs.SymlinkIfPossible(m.finch.UserDataDiskPath(m.homeDir), limaPath) + if err != nil { + return err + } + return nil +} diff --git a/pkg/disk/disk_test.go b/pkg/disk/disk_test.go new file mode 100644 index 000000000..1c9f425aa --- /dev/null +++ b/pkg/disk/disk_test.go @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package disk + +import ( + "fmt" + "github.com/golang/mock/gomock" + "github.com/runfinch/finch/pkg/mocks" + "github.com/runfinch/finch/pkg/path" + "github.com/stretchr/testify/assert" + "io/fs" + "testing" +) + +func TestDisk_NewUserDataDiskManager(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + dfs := mocks.NewMockDiskFS(ctrl) + finch := path.Finch("mock_finch") + homeDir := "mock_home" + + NewUserDataDiskManager(lcc, dfs, finch, homeDir) +} + +func TestUserDataDiskManager_InitializeUserDataDisk(t *testing.T) { + t.Parallel() + + finch := path.Finch("mock_finch") + homeDir := "mock_home" + + limaPath := fmt.Sprintf("%s/_disks/%s/datadisk", finch.LimaHomePath(), diskName) + mockListArgs := []string{"disk", "ls", diskName, "--json"} + mockCreateArgs := []string{"disk", "create", diskName, "--size=50G"} + listSuccessOutput := []byte("{\"name\":\"finch\",\"size\":5,\"dir\":\"mock_dir\",\"instance\":\"\",\"instanceDir\":\"\",\"mountPoint\":\"/mnt/lima-finch\"}") + + testCases := []struct { + name string + wantErr error + mockSvc func(*mocks.LimaCmdCreator, *mocks.MockDiskFS, *mocks.Command) + }{ + { + name: "create and save disk", + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, dfs *mocks.MockDiskFS, cmd *mocks.Command) { + lcc.EXPECT().CreateWithoutStdio(mockListArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte(""), nil) + + lcc.EXPECT().CreateWithoutStdio(mockCreateArgs).Return(cmd) + cmd.EXPECT().Run().Return(nil) + + dfs.EXPECT().Stat(finch.UserDataDiskPath(homeDir)).Return(nil, fs.ErrNotExist) + dfs.EXPECT().Rename(limaPath, finch.UserDataDiskPath(homeDir)).Return(nil) + + dfs.EXPECT().Stat(limaPath).Return(nil, fs.ErrNotExist) + dfs.EXPECT().SymlinkIfPossible(finch.UserDataDiskPath(homeDir), limaPath).Return(nil) + }, + }, + { + name: "disk already exists", + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, dfs *mocks.MockDiskFS, cmd *mocks.Command) { + lcc.EXPECT().CreateWithoutStdio(mockListArgs).Return(cmd) + cmd.EXPECT().Output().Return(listSuccessOutput, nil) + + dfs.EXPECT().ReadlinkIfPossible(limaPath).Return(finch.UserDataDiskPath(homeDir), nil) + }, + }, + { + name: "disk exists but has not been saved", + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, dfs *mocks.MockDiskFS, cmd *mocks.Command) { + lcc.EXPECT().CreateWithoutStdio(mockListArgs).Return(cmd) + cmd.EXPECT().Output().Return(listSuccessOutput, nil) + + // not a link + dfs.EXPECT().ReadlinkIfPossible(limaPath).Return("", nil) + + dfs.EXPECT().Stat(finch.UserDataDiskPath(homeDir)).Return(nil, fs.ErrNotExist) + dfs.EXPECT().Rename(limaPath, finch.UserDataDiskPath(homeDir)).Return(nil) + + dfs.EXPECT().Stat(limaPath).Return(nil, fs.ErrNotExist) + dfs.EXPECT().SymlinkIfPossible(finch.UserDataDiskPath(homeDir), limaPath).Return(nil) + }, + }, + { + name: "disk does not exist but a persistent disk does", + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, dfs *mocks.MockDiskFS, cmd *mocks.Command) { + lcc.EXPECT().CreateWithoutStdio(mockListArgs).Return(cmd) + cmd.EXPECT().Output().Return([]byte(""), nil) + + lcc.EXPECT().CreateWithoutStdio(mockCreateArgs).Return(cmd) + cmd.EXPECT().Run().Return(nil) + + dfs.EXPECT().Stat(finch.UserDataDiskPath(homeDir)).Return(nil, nil) + + dfs.EXPECT().Stat(limaPath).Return(nil, nil) + dfs.EXPECT().Remove(limaPath).Return(nil) + + dfs.EXPECT().SymlinkIfPossible(finch.UserDataDiskPath(homeDir), limaPath).Return(nil) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + dfs := mocks.NewMockDiskFS(ctrl) + cmd := mocks.NewCommand(ctrl) + tc.mockSvc(lcc, dfs, cmd) + dm := NewUserDataDiskManager(lcc, dfs, finch, homeDir) + err := dm.InitializeUserDataDisk() + assert.Equal(t, tc.wantErr, err) + }) + } +} diff --git a/pkg/disk/limactl_disk.go b/pkg/disk/limactl_disk.go new file mode 100644 index 000000000..af6d35607 --- /dev/null +++ b/pkg/disk/limactl_disk.go @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package disk + +type limactlDiskListOutput struct { + Name string `json:"name"` + Size int64 `json:"size"` + Dir string `json:"dir"` + Instance string `json:"instance"` + InstanceDir string `json:"instanceDir"` + MountPoint string `json:"mountPoint"` +} diff --git a/pkg/mocks/pkg_disk_disk.go b/pkg/mocks/pkg_disk_disk.go new file mode 100644 index 000000000..436c5291e --- /dev/null +++ b/pkg/mocks/pkg_disk_disk.go @@ -0,0 +1,289 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: pkg/disk/disk.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + os "os" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + afero "github.com/spf13/afero" +) + +// MockUserDataDiskManager is a mock of UserDataDiskManager interface. +type MockUserDataDiskManager struct { + ctrl *gomock.Controller + recorder *MockUserDataDiskManagerMockRecorder +} + +// MockUserDataDiskManagerMockRecorder is the mock recorder for MockUserDataDiskManager. +type MockUserDataDiskManagerMockRecorder struct { + mock *MockUserDataDiskManager +} + +// NewMockUserDataDiskManager creates a new mock instance. +func NewMockUserDataDiskManager(ctrl *gomock.Controller) *MockUserDataDiskManager { + mock := &MockUserDataDiskManager{ctrl: ctrl} + mock.recorder = &MockUserDataDiskManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserDataDiskManager) EXPECT() *MockUserDataDiskManagerMockRecorder { + return m.recorder +} + +// InitializeUserDataDisk mocks base method. +func (m *MockUserDataDiskManager) InitializeUserDataDisk() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InitializeUserDataDisk") + ret0, _ := ret[0].(error) + return ret0 +} + +// InitializeUserDataDisk indicates an expected call of InitializeUserDataDisk. +func (mr *MockUserDataDiskManagerMockRecorder) InitializeUserDataDisk() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitializeUserDataDisk", reflect.TypeOf((*MockUserDataDiskManager)(nil).InitializeUserDataDisk)) +} + +// MockDiskFS is a mock of DiskFS interface. +type MockDiskFS struct { + ctrl *gomock.Controller + recorder *MockDiskFSMockRecorder +} + +// MockDiskFSMockRecorder is the mock recorder for MockDiskFS. +type MockDiskFSMockRecorder struct { + mock *MockDiskFS +} + +// NewMockDiskFS creates a new mock instance. +func NewMockDiskFS(ctrl *gomock.Controller) *MockDiskFS { + mock := &MockDiskFS{ctrl: ctrl} + mock.recorder = &MockDiskFSMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDiskFS) EXPECT() *MockDiskFSMockRecorder { + return m.recorder +} + +// Chmod mocks base method. +func (m *MockDiskFS) Chmod(name string, mode os.FileMode) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Chmod", name, mode) + ret0, _ := ret[0].(error) + return ret0 +} + +// Chmod indicates an expected call of Chmod. +func (mr *MockDiskFSMockRecorder) Chmod(name, mode interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chmod", reflect.TypeOf((*MockDiskFS)(nil).Chmod), name, mode) +} + +// Chown mocks base method. +func (m *MockDiskFS) Chown(name string, uid, gid int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Chown", name, uid, gid) + ret0, _ := ret[0].(error) + return ret0 +} + +// Chown indicates an expected call of Chown. +func (mr *MockDiskFSMockRecorder) Chown(name, uid, gid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chown", reflect.TypeOf((*MockDiskFS)(nil).Chown), name, uid, gid) +} + +// Chtimes mocks base method. +func (m *MockDiskFS) Chtimes(name string, atime, mtime time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Chtimes", name, atime, mtime) + ret0, _ := ret[0].(error) + return ret0 +} + +// Chtimes indicates an expected call of Chtimes. +func (mr *MockDiskFSMockRecorder) Chtimes(name, atime, mtime interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chtimes", reflect.TypeOf((*MockDiskFS)(nil).Chtimes), name, atime, mtime) +} + +// Create mocks base method. +func (m *MockDiskFS) Create(name string) (afero.File, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", name) + ret0, _ := ret[0].(afero.File) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockDiskFSMockRecorder) Create(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDiskFS)(nil).Create), name) +} + +// Mkdir mocks base method. +func (m *MockDiskFS) Mkdir(name string, perm os.FileMode) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mkdir", name, perm) + ret0, _ := ret[0].(error) + return ret0 +} + +// Mkdir indicates an expected call of Mkdir. +func (mr *MockDiskFSMockRecorder) Mkdir(name, perm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdir", reflect.TypeOf((*MockDiskFS)(nil).Mkdir), name, perm) +} + +// MkdirAll mocks base method. +func (m *MockDiskFS) MkdirAll(path string, perm os.FileMode) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MkdirAll", path, perm) + ret0, _ := ret[0].(error) + return ret0 +} + +// MkdirAll indicates an expected call of MkdirAll. +func (mr *MockDiskFSMockRecorder) MkdirAll(path, perm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MockDiskFS)(nil).MkdirAll), path, perm) +} + +// Name mocks base method. +func (m *MockDiskFS) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockDiskFSMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockDiskFS)(nil).Name)) +} + +// Open mocks base method. +func (m *MockDiskFS) Open(name string) (afero.File, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Open", name) + ret0, _ := ret[0].(afero.File) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Open indicates an expected call of Open. +func (mr *MockDiskFSMockRecorder) Open(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockDiskFS)(nil).Open), name) +} + +// OpenFile mocks base method. +func (m *MockDiskFS) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpenFile", name, flag, perm) + ret0, _ := ret[0].(afero.File) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// OpenFile indicates an expected call of OpenFile. +func (mr *MockDiskFSMockRecorder) OpenFile(name, flag, perm interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenFile", reflect.TypeOf((*MockDiskFS)(nil).OpenFile), name, flag, perm) +} + +// ReadlinkIfPossible mocks base method. +func (m *MockDiskFS) ReadlinkIfPossible(name string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadlinkIfPossible", name) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadlinkIfPossible indicates an expected call of ReadlinkIfPossible. +func (mr *MockDiskFSMockRecorder) ReadlinkIfPossible(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadlinkIfPossible", reflect.TypeOf((*MockDiskFS)(nil).ReadlinkIfPossible), name) +} + +// Remove mocks base method. +func (m *MockDiskFS) Remove(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Remove", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// Remove indicates an expected call of Remove. +func (mr *MockDiskFSMockRecorder) Remove(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockDiskFS)(nil).Remove), name) +} + +// RemoveAll mocks base method. +func (m *MockDiskFS) RemoveAll(path string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveAll", path) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveAll indicates an expected call of RemoveAll. +func (mr *MockDiskFSMockRecorder) RemoveAll(path interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockDiskFS)(nil).RemoveAll), path) +} + +// Rename mocks base method. +func (m *MockDiskFS) Rename(oldname, newname string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rename", oldname, newname) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rename indicates an expected call of Rename. +func (mr *MockDiskFSMockRecorder) Rename(oldname, newname interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockDiskFS)(nil).Rename), oldname, newname) +} + +// Stat mocks base method. +func (m *MockDiskFS) Stat(name string) (os.FileInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stat", name) + ret0, _ := ret[0].(os.FileInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Stat indicates an expected call of Stat. +func (mr *MockDiskFSMockRecorder) Stat(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockDiskFS)(nil).Stat), name) +} + +// SymlinkIfPossible mocks base method. +func (m *MockDiskFS) SymlinkIfPossible(oldname, newname string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SymlinkIfPossible", oldname, newname) + ret0, _ := ret[0].(error) + return ret0 +} + +// SymlinkIfPossible indicates an expected call of SymlinkIfPossible. +func (mr *MockDiskFSMockRecorder) SymlinkIfPossible(oldname, newname interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SymlinkIfPossible", reflect.TypeOf((*MockDiskFS)(nil).SymlinkIfPossible), oldname, newname) +} diff --git a/pkg/path/finch.go b/pkg/path/finch.go index e46d5a2fe..e3f71cd0b 100644 --- a/pkg/path/finch.go +++ b/pkg/path/finch.go @@ -18,6 +18,12 @@ func (Finch) ConfigFilePath(homeDir string) string { return fmt.Sprintf("%s/.finch/finch.yaml", homeDir) } +// UserDataDiskPath returns the path to the permanent storage location of the Finch +// user data disk +func (Finch) UserDataDiskPath(homeDir string) string { + return fmt.Sprintf("%s/.finch/datadisk", homeDir) +} + // LimaHomePath returns the path that should be set to LIMA_HOME for Finch. func (w Finch) LimaHomePath() string { return fmt.Sprintf("%s/lima/data", w) diff --git a/pkg/path/finch_test.go b/pkg/path/finch_test.go index f7e514d77..e6be52671 100644 --- a/pkg/path/finch_test.go +++ b/pkg/path/finch_test.go @@ -23,6 +23,13 @@ func TestFinch_ConfigFilePath(t *testing.T) { assert.Equal(t, res, "homeDir/.finch/finch.yaml") } +func TestFinch_UserDataDiskPath(t *testing.T) { + t.Parallel() + + res := mockFinch.UserDataDiskPath("homeDir") + assert.Equal(t, res, "homeDir/.finch/datadisk") +} + func TestFinch_LimaHomePath(t *testing.T) { t.Parallel()