diff --git a/cmd/finch/main.go b/cmd/finch/main.go index 57d6079a7..d7a5d36bb 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -101,7 +101,7 @@ var newApp = func(logger flog.Logger, fp path.Finch, fs afero.Fs, fc *config.Fin ) // append nerdctl commands - allCommands := initializeNerdctlCommands(lcc, logger, fs) + allCommands := initializeNerdctlCommands(lcc, ecc, logger, fs, fc) // append finch specific commands allCommands = append(allCommands, newVersionCommand(lcc, logger, stdOut), @@ -140,8 +140,14 @@ func virtualMachineCommands( ) } -func initializeNerdctlCommands(lcc command.LimaCmdCreator, logger flog.Logger, fs afero.Fs) []*cobra.Command { - nerdctlCommandCreator := newNerdctlCommandCreator(lcc, system.NewStdLib(), logger, fs) +func initializeNerdctlCommands( + lcc command.LimaCmdCreator, + ecc *command.ExecCmdCreator, + logger flog.Logger, + fs afero.Fs, + fc *config.Finch, +) []*cobra.Command { + nerdctlCommandCreator := newNerdctlCommandCreator(lcc, ecc, system.NewStdLib(), logger, fs, fc) var allNerdctlCommands []*cobra.Command for cmdName, cmdDescription := range nerdctlCmds { allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) diff --git a/cmd/finch/nerdctl.go b/cmd/finch/nerdctl.go index 776fa329e..bcf5ccde1 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -5,17 +5,21 @@ package main import ( "bufio" + "encoding/json" "fmt" "path/filepath" "strings" dockerops "github.com/docker/docker/opts" "github.com/lima-vm/lima/pkg/networks" + "golang.org/x/exp/slices" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" "github.com/runfinch/finch/pkg/flog" "github.com/runfinch/finch/pkg/lima" "github.com/runfinch/finch/pkg/system" @@ -33,19 +37,23 @@ type NerdctlCommandSystemDeps interface { } type nerdctlCommandCreator struct { - creator command.LimaCmdCreator + lcc command.LimaCmdCreator + ecc command.Creator systemDeps NerdctlCommandSystemDeps logger flog.Logger fs afero.Fs + fc *config.Finch } func newNerdctlCommandCreator( - creator command.LimaCmdCreator, + lcc command.LimaCmdCreator, + ecc command.Creator, systemDeps NerdctlCommandSystemDeps, logger flog.Logger, fs afero.Fs, + fc *config.Finch, ) *nerdctlCommandCreator { - return &nerdctlCommandCreator{creator: creator, systemDeps: systemDeps, logger: logger, fs: fs} + return &nerdctlCommandCreator{lcc: lcc, ecc: ecc, systemDeps: systemDeps, logger: logger, fs: fs, fc: fc} } func (ncc *nerdctlCommandCreator) create(cmdName string, cmdDesc string) *cobra.Command { @@ -56,21 +64,30 @@ func (ncc *nerdctlCommandCreator) create(cmdName string, cmdDesc string) *cobra. // the args passed to nerdctlCommand.run will be empty because // cobra will try to parse `-d alpine` as if alpine is the value of the `-d` flag. DisableFlagParsing: true, - RunE: newNerdctlCommand(ncc.creator, ncc.systemDeps, ncc.logger, ncc.fs).runAdapter, + RunE: newNerdctlCommand(ncc.lcc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runAdapter, } return command } type nerdctlCommand struct { - creator command.LimaCmdCreator + lcc command.LimaCmdCreator + ecc command.Creator systemDeps NerdctlCommandSystemDeps logger flog.Logger fs afero.Fs + fc *config.Finch } -func newNerdctlCommand(creator command.LimaCmdCreator, systemDeps NerdctlCommandSystemDeps, logger flog.Logger, fs afero.Fs) *nerdctlCommand { - return &nerdctlCommand{creator: creator, systemDeps: systemDeps, logger: logger, fs: fs} +func newNerdctlCommand( + lcc command.LimaCmdCreator, + ecc command.Creator, + systemDeps NerdctlCommandSystemDeps, + logger flog.Logger, + fs afero.Fs, + fc *config.Finch, +) *nerdctlCommand { + return &nerdctlCommand{lcc: lcc, ecc: ecc, systemDeps: systemDeps, logger: logger, fs: fs, fc: fc} } func (nc *nerdctlCommand) runAdapter(cmd *cobra.Command, args []string) error { @@ -78,7 +95,7 @@ func (nc *nerdctlCommand) runAdapter(cmd *cobra.Command, args []string) error { } func (nc *nerdctlCommand) run(cmdName string, args []string) error { - err := nc.assertVMIsRunning(nc.creator, nc.logger) + err := nc.assertVMIsRunning(nc.lcc, nc.logger) if err != nil { return err } @@ -154,9 +171,15 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { } } + var additionalEnv []string + switch cmdName { + case "build", "pull", "push": + ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) + } + // Add -E to sudo command in order to preserve existing environment variables, more info: // https://stackoverflow.com/questions/8633461/how-to-keep-environment-variables-when-using-sudo/8633575#8633575 - limaArgs := append([]string{"shell", limaInstanceName, "sudo", "-E"}, passedEnvArgs...) + limaArgs := append([]string{"shell", limaInstanceName, "sudo", "-E"}, append(additionalEnv, passedEnvArgs...)...) limaArgs = append(limaArgs, []string{nerdctlCmdName, cmdName}...) @@ -170,10 +193,10 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { limaArgs = append(limaArgs, finalArgs...) if nc.shouldReplaceForHelp(cmdName, args) { - return nc.creator.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, limaArgs...) + return nc.lcc.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, limaArgs...) } - return nc.creator.Create(limaArgs...).Run() + return nc.lcc.Create(limaArgs...).Run() } func (nc *nerdctlCommand) assertVMIsRunning(creator command.LimaCmdCreator, logger flog.Logger) error { @@ -318,6 +341,44 @@ func resolveIP(host string, logger flog.Logger) string { return host } +// ensureRemoteCredentials is called before any actions that may require remote resources, in order +// to ensure that fresh credentials are available inside the VM. +// For more details on how `aws configure export-credentials` works, checks the docs. +// +// [the docs]: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/export-credentials.html +func ensureRemoteCredentials( + fc *config.Finch, + ecc command.Creator, + outEnv *[]string, + logger flog.Logger, +) { + if slices.Contains(fc.CredsHelpers, "ecr-login") { + out, err := ecc.Create( + "aws", + "configure", + "export-credentials", + "--format", + "process", + ).CombinedOutput() + if err != nil { + logger.Debugln("failed to run `aws configure` command") + return + } + + var exportCredsOut aws.Credentials + err = json.Unmarshal(out, &exportCredsOut) + if err != nil { + logger.Debugln("`aws configure export-credentials` output is unexpected, is command available? " + + "This may result in a broken ecr-credential helper experience.") + return + } + + *outEnv = append(*outEnv, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", exportCredsOut.AccessKeyID)) + *outEnv = append(*outEnv, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", exportCredsOut.SecretAccessKey)) + *outEnv = append(*outEnv, fmt.Sprintf("AWS_SESSION_TOKEN=%s", exportCredsOut.SessionToken)) + } +} + var nerdctlCmds = map[string]string{ "build": "Build an image from Dockerfile", "builder": "Manage builds", diff --git a/cmd/finch/nerdctl_test.go b/cmd/finch/nerdctl_test.go index 222dcaf1d..1a9301506 100644 --- a/cmd/finch/nerdctl_test.go +++ b/cmd/finch/nerdctl_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/runfinch/finch/pkg/config" "github.com/runfinch/finch/pkg/flog" "github.com/runfinch/finch/pkg/command" @@ -28,7 +29,7 @@ var testStdoutRs = []command.Replacement{ func TestNerdctlCommandCreator_create(t *testing.T) { t.Parallel() - cmd := newNerdctlCommandCreator(nil, nil, nil, nil).create("build", "build description") + cmd := newNerdctlCommandCreator(nil, nil, nil, nil, nil, nil).create("build", "build description") assert.Equal(t, cmd.Name(), "build") assert.Equal(t, cmd.DisableFlagParsing, true) } @@ -71,11 +72,12 @@ func TestNerdctlCommand_runAdaptor(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ecc := mocks.NewCommandCreator(ctrl) ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) tc.mockSvc(lcc, logger, ctrl, ncsd) - assert.NoError(t, newNerdctlCommand(lcc, ncsd, logger, nil).runAdapter(tc.cmd, tc.args)) + assert.NoError(t, newNerdctlCommand(lcc, ecc, ncsd, logger, nil, &config.Finch{}).runAdapter(tc.cmd, tc.args)) }) } } @@ -86,18 +88,29 @@ func TestNerdctlCommand_run(t *testing.T) { testCases := []struct { name string cmdName string + fc *config.Finch args []string wantErr error - mockSvc func(*testing.T, *mocks.LimaCmdCreator, *mocks.NerdctlCommandSystemDeps, *mocks.Logger, *gomock.Controller, afero.Fs) + mockSvc func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) }{ { name: "happy path", cmdName: "build", + fc: &config.Finch{}, args: []string{"-t", "demo", "."}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -119,12 +132,14 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "stopped VM", cmdName: "build", + fc: &config.Finch{}, args: []string{"-t", "demo", "."}, wantErr: fmt.Errorf("instance %q is stopped, run `finch %s start` to start the instance", limaInstanceName, virtualMachineRootCmd), mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -139,6 +154,7 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "nonexistent VM", cmdName: "build", + fc: &config.Finch{}, args: []string{"-t", "demo", "."}, wantErr: fmt.Errorf( "instance %q does not exist, run `finch %s init` to create a new instance", @@ -146,6 +162,7 @@ func TestNerdctlCommand_run(t *testing.T) { mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -160,11 +177,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "unknown VM status", cmdName: "build", + fc: &config.Finch{}, args: []string{"-t", "demo", "."}, wantErr: errors.New("unrecognized system status"), mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -179,11 +198,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "status command returns an error", cmdName: "build", + fc: &config.Finch{}, args: []string{"-t", "demo", "."}, wantErr: errors.New("get status error"), mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -197,11 +218,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --debug flag", cmdName: "pull", + fc: &config.Finch{}, args: []string{"test:tag", "--debug"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -224,11 +247,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with environment flags parsing and env value doesn't exist", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "-e", "ARG1=val1", "--env=ARG2", "-eARG3", "alpine:latest", "env"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -254,11 +279,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with environment flags parsing and env value exists", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--env=ARG2", "-eARG3", "alpine:latest", "env"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -283,11 +310,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --env-file flag replacement", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--env-file=/env-file", "alpine:latest", "env"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -316,11 +345,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --env-file flag replacement and existing env value", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--env-file", "/env-file", "alpine:latest", "env"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -349,11 +380,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --env-file flag, but the specified file does not exist", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--env-file", "/env-file", "alpine:latest", "env"}, wantErr: &os.PathError{Op: "open", Path: "/env-file", Err: afero.ErrFileNotFound}, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -368,11 +401,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --add-host flag and special IP by space", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--add-host", "name:host-gateway", "alpine:latest"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -396,11 +431,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --add-host flag but without using special IP by space", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--add-host", "name:0.0.0.0", "alpine:latest"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -423,11 +460,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --add-host flag but without subsequent arg", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--add-host", "alpine:latest"}, wantErr: errors.New("run cmd error"), mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -450,11 +489,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --add-host flag and special IP by equal", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--add-host=name:host-gateway", "alpine:latest"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -478,11 +519,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --add-host flag but without using special IP by equal", cmdName: "run", + fc: &config.Finch{}, args: []string{"--rm", "--add-host=name:0.0.0.0", "alpine:latest"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -506,6 +549,7 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with multiple nested volumes", cmdName: "run", + fc: &config.Finch{}, args: []string{ "--rm", "-v", "/tmp:/tmp1/tmp2:rro", "--volume", "/tmp:/tmp1:rprivate,rro", "-v=/tmp:/tmp1/tmp2/tmp3/tmp4:rro", "--volume=/tmp:/tmp1/tmp3/tmp4:rshared", "-v", "volume", "alpine:latest", @@ -514,6 +558,7 @@ func TestNerdctlCommand_run(t *testing.T) { mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -537,6 +582,7 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with multiple nested volumes with full container run command", cmdName: "container", + fc: &config.Finch{}, args: []string{ "run", "--rm", "-v", "/tmp:/tmp1/tmp2:rro", "--volume", "/tmp:/tmp1:rprivate,rro", "-v=/tmp:/tmp1/tmp2/tmp3/tmp4:rro", "--volume=/tmp:/tmp1/tmp3/tmp4:rshared", "-v", "volume", "alpine:latest", @@ -545,6 +591,7 @@ func TestNerdctlCommand_run(t *testing.T) { mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -568,11 +615,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --help flag", cmdName: "pull", + fc: &config.Finch{}, args: []string{"test:tag", "--help"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -593,11 +642,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with --help flag but replacing returns error", cmdName: "pull", + fc: &config.Finch{}, args: []string{"test:tag", "--help"}, wantErr: fmt.Errorf("failed to replace"), mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -619,11 +670,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with COSIGN_PASSWORD env var and --sign=cosign", cmdName: "push", + fc: &config.Finch{}, args: []string{"--sign=cosign", "test:tag"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -646,11 +699,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with COSIGN_PASSWORD env var and --verify=cosign", cmdName: "pull", + fc: &config.Finch{}, args: []string{"--verify=cosign", "test:tag"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -673,11 +728,13 @@ func TestNerdctlCommand_run(t *testing.T) { { name: "with COSIGN_PASSWORD env var without cosign arg", cmdName: "pull", + fc: &config.Finch{}, args: []string{"test:tag"}, wantErr: nil, mockSvc: func( t *testing.T, lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller, @@ -697,6 +754,226 @@ func TestNerdctlCommand_run(t *testing.T) { c.EXPECT().Run() }, }, + { + name: "with ECR credential helper and environment set", + cmdName: "pull", + fc: &config.Finch{ + CredsHelpers: []string{"ecr-login"}, + }, + args: []string{"test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("TEST_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("TEST_SECRET_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("TEST_SESSION_TOKEN", true) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + + awsCmd := mocks.NewCommand(ctrl) + ecc.EXPECT().Create( + "aws", + "configure", + "export-credentials", + "--format", + "process", + ).Return(awsCmd) + awsCmd.EXPECT().CombinedOutput().Return([]byte(`{ + "AccessKeyID": "TEST_ACCESS_KEY_FROM_PROCESS", + "SecretAccessKey": "TEST_SECRET_ACCESS_KEY_FROM_PROCESS", + "SessionToken": "TEST_SESSION_TOKEN_FROM_PROCESS" +} +`), nil) + + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", + limaInstanceName, + "sudo", + "-E", + "AWS_ACCESS_KEY_ID=TEST_ACCESS_KEY_FROM_PROCESS", + "AWS_SECRET_ACCESS_KEY=TEST_SECRET_ACCESS_KEY_FROM_PROCESS", + "AWS_SESSION_TOKEN=TEST_SESSION_TOKEN_FROM_PROCESS", + "AWS_ACCESS_KEY_ID=TEST_ACCESS_KEY", + "AWS_SECRET_ACCESS_KEY=TEST_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN=TEST_SESSION_TOKEN", + nerdctlCmdName, + "pull", + "test:tag", + ).Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with ECR credential helper, no environment set", + cmdName: "pull", + fc: &config.Finch{ + CredsHelpers: []string{"ecr-login"}, + }, + args: []string{"test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("TEST_ACCESS_KEY", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("TEST_SECRET_ACCESS_KEY", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("TEST_SESSION_TOKEN", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + + awsCmd := mocks.NewCommand(ctrl) + ecc.EXPECT().Create( + "aws", + "configure", + "export-credentials", + "--format", + "process", + ).Return(awsCmd) + awsCmd.EXPECT().CombinedOutput().Return([]byte(`{ + "AccessKeyID": "TEST_ACCESS_KEY_FROM_PROCESS", + "SecretAccessKey": "TEST_SECRET_ACCESS_KEY_FROM_PROCESS", + "SessionToken": "TEST_SESSION_TOKEN_FROM_PROCESS" +} +`), nil) + + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", + limaInstanceName, + "sudo", + "-E", + "AWS_ACCESS_KEY_ID=TEST_ACCESS_KEY_FROM_PROCESS", + "AWS_SECRET_ACCESS_KEY=TEST_SECRET_ACCESS_KEY_FROM_PROCESS", + "AWS_SESSION_TOKEN=TEST_SESSION_TOKEN_FROM_PROCESS", + nerdctlCmdName, + "pull", + "test:tag", + ).Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with ECR credential helper, aws command fails but environment is used", + cmdName: "pull", + fc: &config.Finch{ + CredsHelpers: []string{"ecr-login"}, + }, + args: []string{"test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("TEST_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("TEST_SECRET_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("TEST_SESSION_TOKEN", true) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + + awsCmd := mocks.NewCommand(ctrl) + ecc.EXPECT().Create( + "aws", + "configure", + "export-credentials", + "--format", + "process", + ).Return(awsCmd) + awsCmd.EXPECT().CombinedOutput().Return(nil, fmt.Errorf("an error")) + logger.EXPECT().Debugln("failed to run `aws configure` command") + + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", + limaInstanceName, + "sudo", + "-E", + "AWS_ACCESS_KEY_ID=TEST_ACCESS_KEY", + "AWS_SECRET_ACCESS_KEY=TEST_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN=TEST_SESSION_TOKEN", + nerdctlCmdName, + "pull", + "test:tag", + ).Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with ECR credential helper, aws command fails but returns unexpected response", + cmdName: "pull", + fc: &config.Finch{ + CredsHelpers: []string{"ecr-login"}, + }, + args: []string{"test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ecc *mocks.CommandCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("TEST_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("TEST_SECRET_ACCESS_KEY", true) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("TEST_SESSION_TOKEN", true) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + + awsCmd := mocks.NewCommand(ctrl) + ecc.EXPECT().Create( + "aws", + "configure", + "export-credentials", + "--format", + "process", + ).Return(awsCmd) + awsCmd.EXPECT().CombinedOutput().Return([]byte("unexpected response"), nil) + logger.EXPECT().Debugln("`aws configure export-credentials` output is unexpected, is command available? " + + "This may result in a broken ecr-credential helper experience.") + + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", + limaInstanceName, + "sudo", + "-E", + "AWS_ACCESS_KEY_ID=TEST_ACCESS_KEY", + "AWS_SECRET_ACCESS_KEY=TEST_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN=TEST_SESSION_TOKEN", + nerdctlCmdName, + "pull", + "test:tag", + ).Return(c) + c.EXPECT().Run() + }, + }, } for _, tc := range testCases { @@ -706,11 +983,13 @@ func TestNerdctlCommand_run(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ecc := mocks.NewCommandCreator(ctrl) ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) fs := afero.NewMemMapFs() - tc.mockSvc(t, lcc, ncsd, logger, ctrl, fs) - assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, ncsd, logger, fs).run(tc.cmdName, tc.args)) + tc.mockSvc(t, lcc, ecc, ncsd, logger, ctrl, fs) + + assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, ecc, ncsd, logger, fs, tc.fc).run(tc.cmdName, tc.args)) }) } } @@ -771,9 +1050,10 @@ func TestNerdctlCommand_shouldReplaceForHelp(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ecc := mocks.NewCommandCreator(ctrl) ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) - assert.True(t, newNerdctlCommand(lcc, ncsd, logger, nil).shouldReplaceForHelp(tc.cmdName, tc.args)) + assert.True(t, newNerdctlCommand(lcc, ecc, ncsd, logger, nil, &config.Finch{}).shouldReplaceForHelp(tc.cmdName, tc.args)) }) } } diff --git a/go.mod b/go.mod index ff76b892b..7c4e2e7c8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/runfinch/finch go 1.20 require ( + github.com/aws/aws-sdk-go-v2 v1.21.2 github.com/docker/cli v24.0.7+incompatible github.com/docker/docker v24.0.7+incompatible github.com/golang/mock v1.6.0 @@ -27,6 +28,7 @@ require ( ) require ( + github.com/aws/smithy-go v1.15.0 // indirect github.com/containerd/containerd v1.7.3 // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/go.sum b/go.sum index cf983df7e..5c2eac2b4 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,10 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -248,6 +252,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= @@ -887,6 +893,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=