From 0bc42df6a6e070199b888d01637bbbd7f2f21204 Mon Sep 17 00:00:00 2001 From: Cartrius Phipps Date: Mon, 4 Aug 2025 14:42:55 -0700 Subject: [PATCH] feat: Add vm disk usage subcommand Signed-off-by: Cartrius Phipps --- cmd/finch/virtual_machine_darwin.go | 1 + cmd/finch/virtual_machine_disk_usage.go | 98 +++++++++++++ cmd/finch/virtual_machine_disk_usage_test.go | 140 +++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 cmd/finch/virtual_machine_disk_usage.go create mode 100644 cmd/finch/virtual_machine_disk_usage_test.go diff --git a/cmd/finch/virtual_machine_darwin.go b/cmd/finch/virtual_machine_darwin.go index 72bb8de62..492338759 100644 --- a/cmd/finch/virtual_machine_darwin.go +++ b/cmd/finch/virtual_machine_darwin.go @@ -28,6 +28,7 @@ func newDiskVMCommand(creator command.NerdctlCmdCreator, logger flog.Logger) *co diskCmd.AddCommand( newVMDiskResizeCommand(creator, logger), newVMDiskInfoCommand(creator, logger), + newVMDiskUsageCommand(creator, logger), ) return diskCmd diff --git a/cmd/finch/virtual_machine_disk_usage.go b/cmd/finch/virtual_machine_disk_usage.go new file mode 100644 index 000000000..2c15c58f6 --- /dev/null +++ b/cmd/finch/virtual_machine_disk_usage.go @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build darwin + +package main + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/flog" +) + +func newVMDiskUsageCommand(creator command.NerdctlCmdCreator, logger flog.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "usage", + Short: "Display disk usage information from within the virtual machine", + Long: `Display disk usage information from within the virtual machine. +This command shows the actual disk usage as seen from inside the VM, +including used space, available space, and usage percentage.`, + RunE: newDiskUsageAction(creator, logger).runAdapter, + } + return cmd +} + +type diskUsageAction struct { + creator command.NerdctlCmdCreator + logger flog.Logger +} + +func newDiskUsageAction(creator command.NerdctlCmdCreator, logger flog.Logger) *diskUsageAction { + return &diskUsageAction{ + creator: creator, + logger: logger, + } +} + +func (dua *diskUsageAction) runAdapter(_ *cobra.Command, _ []string) error { + return dua.run() +} + +func (dua *diskUsageAction) run() error { + // First check if the VM is running + statusCmd := dua.creator.CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName) + statusOutput, err := statusCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to check VM status: %w", err) + } + + status := strings.TrimSpace(string(statusOutput)) + if status != "Running" { + return fmt.Errorf("virtual machine is not running (status: %s)", status) + } + + shellCmd := dua.creator.CreateWithoutStdio("shell", limaInstanceName, "df", "-h", "/mnt/lima-finch") + output, err := shellCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to get disk usage information: %w\n%s", err, output) + } + + outputStr := strings.TrimSpace(string(output)) + if len(outputStr) == 0 { + return fmt.Errorf("no disk usage information available") + } + + // Parse and format the df output + lines := strings.Split(outputStr, "\n") + if len(lines) < 2 { + return fmt.Errorf("unexpected disk usage output format") + } + + // Parse the data line (skip header) + fields := strings.Fields(lines[1]) + if len(fields) < 6 { + return fmt.Errorf("insufficient disk usage data") + } + + // df output format: Filesystem Size Used Avail Use% Mounted + filesystem := fields[0] + size := fields[1] + used := fields[2] + available := fields[3] + percentage := fields[4] + mountpoint := fields[5] + + fmt.Printf("Filesystem: %s\n", filesystem) + fmt.Printf("Mountpoint: %s\n", mountpoint) + fmt.Printf("Total Size: %s\n", size) + fmt.Printf("Used: %s\n", used) + fmt.Printf("Available: %s\n", available) + fmt.Printf("Usage: %s\n", percentage) + + return nil +} diff --git a/cmd/finch/virtual_machine_disk_usage_test.go b/cmd/finch/virtual_machine_disk_usage_test.go new file mode 100644 index 000000000..4c4f527ae --- /dev/null +++ b/cmd/finch/virtual_machine_disk_usage_test.go @@ -0,0 +1,140 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build darwin + +package main + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/runfinch/finch/pkg/mocks" +) + +func TestDiskUsageAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(*mocks.NerdctlCmdCreator, *mocks.Command, *gomock.Controller) + wantErr error + }{ + { + name: "should return disk usage when VM is running", + mockSvc: func(creator *mocks.NerdctlCmdCreator, cmd *mocks.Command, ctrl *gomock.Controller) { + // Mock VM status check + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return([]byte("Running"), nil) + + // Mock disk usage command + creator.EXPECT().CreateWithoutStdio("shell", limaInstanceName, "df", "-h", "/mnt/lima-finch").Return(cmd) + cmd.EXPECT().CombinedOutput().Return([]byte(`Filesystem Size Used Avail Use% Mounted on +/dev/vda1 20G 5.0G 14G 27% /mnt/lima-finch`), nil) + }, + wantErr: nil, + }, + { + name: "should return error when VM is not running", + mockSvc: func(creator *mocks.NerdctlCmdCreator, _ *mocks.Command, ctrl *gomock.Controller) { + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return([]byte("Stopped"), nil) + }, + wantErr: errors.New("virtual machine is not running (status: Stopped)"), + }, + { + name: "should return error when status check fails", + mockSvc: func(creator *mocks.NerdctlCmdCreator, _ *mocks.Command, ctrl *gomock.Controller) { + // Mock VM status check failure + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return(nil, errors.New("lima command failed")) + }, + wantErr: errors.New("failed to check VM status: lima command failed"), + }, + { + name: "should return error when disk usage command fails", + mockSvc: func(creator *mocks.NerdctlCmdCreator, cmd *mocks.Command, ctrl *gomock.Controller) { + // Mock VM status check + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return([]byte("Running"), nil) + + // Mock disk usage command failure + creator.EXPECT().CreateWithoutStdio("shell", limaInstanceName, "df", "-h", "/mnt/lima-finch").Return(cmd) + cmd.EXPECT().CombinedOutput().Return([]byte("command failed"), errors.New("shell command failed")) + }, + wantErr: errors.New("failed to get disk usage information: shell command failed\ncommand failed"), + }, + { + name: "should return error when disk usage output is empty", + mockSvc: func(creator *mocks.NerdctlCmdCreator, cmd *mocks.Command, ctrl *gomock.Controller) { + // Mock VM status check + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return([]byte("Running"), nil) + + // Mock empty disk usage output + creator.EXPECT().CreateWithoutStdio("shell", limaInstanceName, "df", "-h", "/mnt/lima-finch").Return(cmd) + cmd.EXPECT().CombinedOutput().Return([]byte(""), nil) + }, + wantErr: errors.New("no disk usage information available"), + }, + { + name: "should return error when disk usage output format is unexpected", + mockSvc: func(creator *mocks.NerdctlCmdCreator, cmd *mocks.Command, ctrl *gomock.Controller) { + // Mock VM status check + statusCmd := mocks.NewCommand(ctrl) + creator.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(statusCmd) + statusCmd.EXPECT().CombinedOutput().Return([]byte("Running"), nil) + + // Mock malformed disk usage output + creator.EXPECT().CreateWithoutStdio("shell", limaInstanceName, "df", "-h", "/mnt/lima-finch").Return(cmd) + cmd.EXPECT().CombinedOutput().Return([]byte("invalid output"), nil) + }, + wantErr: errors.New("unexpected disk usage output format"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + creator := mocks.NewNerdctlCmdCreator(ctrl) + cmd := mocks.NewCommand(ctrl) + logger := mocks.NewLogger(ctrl) + + tc.mockSvc(creator, cmd, ctrl) + + action := newDiskUsageAction(creator, logger) + err := action.run() + + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNewVMDiskUsageCommand(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + creator := mocks.NewNerdctlCmdCreator(ctrl) + logger := mocks.NewLogger(ctrl) + + cmd := newVMDiskUsageCommand(creator, logger) + + assert.Equal(t, "usage", cmd.Use) + assert.Equal(t, "Display disk usage information from within the virtual machine", cmd.Short) + assert.NotEmpty(t, cmd.Long) + assert.NotNil(t, cmd.RunE) +}