diff --git a/cmd/finch/virtual_machine.go b/cmd/finch/virtual_machine.go index b4c28bbae..5bcfb19bd 100644 --- a/cmd/finch/virtual_machine.go +++ b/cmd/finch/virtual_machine.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "os" "strings" "github.com/runfinch/finch/pkg/disk" @@ -43,6 +44,7 @@ func newVirtualMachineCommand( newStartVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fs, fp.LimaSSHPrivateKeyPath(), diskManager), newStopVMCommand(limaCmdCreator, logger), newRemoveVMCommand(limaCmdCreator, logger), + newStatusVMCommand(limaCmdCreator, logger, os.Stdout), newInitVMCommand(limaCmdCreator, logger, optionalDepGroups, lca, nca, fp.BaseYamlFilePath(), fs, fp.LimaSSHPrivateKeyPath(), diskManager), ) diff --git a/cmd/finch/virtual_machine_status.go b/cmd/finch/virtual_machine_status.go new file mode 100644 index 000000000..93818b611 --- /dev/null +++ b/cmd/finch/virtual_machine_status.go @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "io" + + "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/lima" + + "github.com/spf13/cobra" +) + +func newStatusVMCommand(limaCmdCreator command.LimaCmdCreator, logger flog.Logger, stdout io.Writer) *cobra.Command { + statusVMCommand := &cobra.Command{ + Use: "status", + Short: "Status of the virtual machine", + RunE: newStatusVMAction(limaCmdCreator, logger, stdout).runAdapter, + } + + return statusVMCommand +} + +type statusVMAction struct { + creator command.LimaCmdCreator + logger flog.Logger + stdout io.Writer +} + +func newStatusVMAction(creator command.LimaCmdCreator, logger flog.Logger, stdout io.Writer) *statusVMAction { + return &statusVMAction{creator: creator, logger: logger, stdout: stdout} +} + +func (sva *statusVMAction) runAdapter(cmd *cobra.Command, args []string) error { + return sva.run() +} + +func (sva *statusVMAction) run() error { + status, err := lima.GetVMStatus(sva.creator, sva.logger, limaInstanceName) + if err != nil { + return err + } + switch status { + case lima.Running: + fmt.Fprintln(sva.stdout, "Running") + return nil + case lima.Nonexistent: + fmt.Fprintln(sva.stdout, "Nonexistent") + return nil + case lima.Stopped: + fmt.Fprintln(sva.stdout, "Stopped") + return nil + default: + return fmt.Errorf("instance state of %q is unknown", limaInstanceName) + } +} diff --git a/cmd/finch/virtual_machine_status_test.go b/cmd/finch/virtual_machine_status_test.go new file mode 100644 index 000000000..9c62afdbc --- /dev/null +++ b/cmd/finch/virtual_machine_status_test.go @@ -0,0 +1,190 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + + "github.com/runfinch/finch/pkg/mocks" +) + +func TestNewStatusVMCommand(t *testing.T) { + t.Parallel() + + cmd := newStatusVMCommand(nil, nil, nil) + assert.Equal(t, cmd.Name(), "status") +} + +func TestStatusVMAction_runAdapter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + command *cobra.Command + args []string + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "should get nonexistent vm status", + command: &cobra.Command{ + Use: "status", + }, + args: []string{}, + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + stdout := bytes.Buffer{} + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + tc.mockSvc(lcc, logger, lca, ctrl) + + assert.NoError(t, newStatusVMAction(lcc, logger, &stdout).runAdapter(tc.command, tc.args)) + }) + } +} + +func TestStatusVMAction_run(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + wantErr error + wantStatusOutput string + mockSvc func( + *mocks.LimaCmdCreator, + *mocks.Logger, + *mocks.LimaConfigApplier, + *gomock.Controller, + ) + }{ + { + name: "running VM", + wantErr: nil, + wantStatusOutput: "Running\n", + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + }, + }, + { + name: "stopped VM", + wantErr: nil, + wantStatusOutput: "Stopped\n", + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped") + }, + }, + { + name: "nonExistent VM", + wantErr: nil, + wantStatusOutput: "Nonexistent\n", + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte(""), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "") + }, + }, + { + name: "unknown VM status", + wantErr: errors.New("unrecognized system status"), + wantStatusOutput: "", + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Broken") + }, + }, + { + name: "status command returns an error", + wantErr: errors.New("get status error"), + wantStatusOutput: "", + mockSvc: func( + lcc *mocks.LimaCmdCreator, + logger *mocks.Logger, + lca *mocks.LimaConfigApplier, + ctrl *gomock.Controller, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Broken"), errors.New("get status error")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + logger := mocks.NewLogger(ctrl) + stdout := bytes.Buffer{} + lcc := mocks.NewLimaCmdCreator(ctrl) + lca := mocks.NewLimaConfigApplier(ctrl) + + tc.mockSvc(lcc, logger, lca, ctrl) + + err := newStatusVMAction(lcc, logger, &stdout).run() + assert.Equal(t, err, tc.wantErr) + assert.Equal(t, tc.wantStatusOutput, stdout.String()) + }) + } +} diff --git a/cmd/finch/virtual_machine_test.go b/cmd/finch/virtual_machine_test.go index 3571a7233..73b3b781c 100644 --- a/cmd/finch/virtual_machine_test.go +++ b/cmd/finch/virtual_machine_test.go @@ -21,7 +21,7 @@ func TestVirtualMachineCommand(t *testing.T) { assert.Equal(t, cmd.Use, virtualMachineRootCmd) // check the number of subcommand for vm - assert.Equal(t, len(cmd.Commands()), 4) + assert.Equal(t, len(cmd.Commands()), 5) } func TestPostVMStartInitAction_runAdapter(t *testing.T) { diff --git a/e2e/vm/lifecycle_test.go b/e2e/vm/lifecycle_test.go index 936cf0679..21d1aa86e 100644 --- a/e2e/vm/lifecycle_test.go +++ b/e2e/vm/lifecycle_test.go @@ -5,6 +5,7 @@ package vm import ( "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" "github.com/runfinch/common-tests/command" "github.com/runfinch/common-tests/option" ) @@ -18,6 +19,10 @@ var testVMLifecycle = func(o *option.Option) { command.New(o, virtualMachineRootCmd, "remove").WithoutSuccessfulExit().Run() }) + ginkgo.It("should get running status", func() { + gomega.Expect(command.StdoutStr(o, virtualMachineRootCmd, "status")).To(gomega.Equal("Running")) + }) + ginkgo.It("should be able to stop the virtual machine", func() { command.Run(o, "images") command.New(o, virtualMachineRootCmd, "stop").WithTimeoutInSeconds(60).Run() @@ -31,6 +36,10 @@ var testVMLifecycle = func(o *option.Option) { command.New(o, virtualMachineRootCmd, "init").WithoutSuccessfulExit().Run() }) + ginkgo.It("should get stopped status", func() { + gomega.Expect(command.StdoutStr(o, virtualMachineRootCmd, "status")).To(gomega.Equal("Stopped")) + }) + ginkgo.It("should be able to start the virtual machine", func() { command.New(o, virtualMachineRootCmd, "start").WithTimeoutInSeconds(120).Run() command.Run(o, "images") @@ -49,6 +58,10 @@ var testVMLifecycle = func(o *option.Option) { command.New(o, virtualMachineRootCmd, "stop").WithoutSuccessfulExit().Run() }) + ginkgo.It("should get nonexistent status", func() { + gomega.Expect(command.StdoutStr(o, virtualMachineRootCmd, "status")).To(gomega.Equal("Nonexistent")) + }) + ginkgo.It("should be able to init", func() { command.New(o, virtualMachineRootCmd, "init").WithTimeoutInSeconds(600).Run() command.Run(o, "images")