From 7b25b406f90256ed767d4bc85f4ac9636ee24fd4 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 1 Feb 2023 14:46:18 -0500 Subject: [PATCH 1/2] chore: update common-tests to v0.4.0 Signed-off-by: Phil Estes --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 55ab86987..9371f4af7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/onsi/gomega v1.26.0 github.com/pelletier/go-toml v1.9.5 github.com/pkg/sftp v1.13.5 - github.com/runfinch/common-tests v0.3.1 + github.com/runfinch/common-tests v0.4.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/afero v1.9.3 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index 8e52ded51..164360a8c 100644 --- a/go.sum +++ b/go.sum @@ -502,8 +502,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/runfinch/common-tests v0.3.1 h1:TG2DQZ6HhaCmy/2wCm36kRWrm/p5DSAjH//gcKdfrcA= -github.com/runfinch/common-tests v0.3.1/go.mod h1:UpmjZJ5nid549Xct3uQfFwkygnA1/XUJydizbZdeKHM= +github.com/runfinch/common-tests v0.4.0 h1:MOtkY/Wcg3hEILDWcaA4cK/NXmk9vUS4y0yOdlIm2Cw= +github.com/runfinch/common-tests v0.4.0/go.mod h1:8Kjz5W85nbSzFCI0NEnwoPMDvTeCm/JutQAEVFMJP/I= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= From 0eb86067913af81afccd6d8bc485f4077a0d2ecd Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Fri, 13 Jan 2023 15:45:20 -0500 Subject: [PATCH 2/2] fix: properly handle local environment value pass-through The -e,--env flag as well as values within an --env-file can contain only a variable name with no "={VALUE}" portion. These entries provide a shorthand to say "pass the existing environment value of this variable into the container". Because of the VM boundary we need to extrapolate these values on the Finch side and pass them as discrete values into the Lima VM on the nerdctl command line. Also the file referenced by --env-file may not be accessible inside the VM, so we translate each entry in the file into -e entries on the command line rather than fail on the VM side, unable to locate the file being referenced. Signed-off-by: Phil Estes --- cmd/finch/main.go | 2 +- cmd/finch/nerdctl.go | 182 ++++++++++++++++++++++++--- cmd/finch/nerdctl_test.go | 131 ++++++++++++++++--- pkg/mocks/nerdctl_cmd_system_deps.go | 52 ++++++++ pkg/system/stdlib.go | 4 + pkg/system/system.go | 5 + 6 files changed, 343 insertions(+), 33 deletions(-) create mode 100644 pkg/mocks/nerdctl_cmd_system_deps.go diff --git a/cmd/finch/main.go b/cmd/finch/main.go index 6b301b8fc..b4460ec4b 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -125,7 +125,7 @@ func virtualMachineCommands( } func initializeNerdctlCommands(lcc command.LimaCmdCreator, logger flog.Logger) []*cobra.Command { - nerdctlCommandCreator := newNerdctlCommandCreator(lcc, logger) + nerdctlCommandCreator := newNerdctlCommandCreator(lcc, system.NewStdLib(), logger) 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 86f437bbf..8eb50af9c 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -4,27 +4,41 @@ package main import ( + "bufio" "fmt" + "os" + "path/filepath" + "strings" "github.com/spf13/cobra" + "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/flog" + "github.com/runfinch/finch/pkg/lima" + "github.com/runfinch/finch/pkg/system" "k8s.io/apimachinery/pkg/util/sets" - - "github.com/runfinch/finch/pkg/command" - "github.com/runfinch/finch/pkg/lima" ) const nerdctlCmdName = "nerdctl" +// NerdctlCommandSystemDeps contains the system dependencies for newNerdctlCommand. +// +//go:generate mockgen -copyright_file=../../copyright_header -destination=../../pkg/mocks/nerdctl_cmd_system_deps.go -package=mocks -mock_names NerdctlCommandSystemDeps=NerdctlCommandSystemDeps -source=nerdctl.go NerdctlCommandSystemDeps +type NerdctlCommandSystemDeps interface { + system.EnvChecker +} + type nerdctlCommandCreator struct { - creator command.LimaCmdCreator - logger flog.Logger + creator command.LimaCmdCreator + systemDeps NerdctlCommandSystemDeps + logger flog.Logger } -func newNerdctlCommandCreator(creator command.LimaCmdCreator, logger flog.Logger) *nerdctlCommandCreator { - return &nerdctlCommandCreator{creator: creator, logger: logger} +func newNerdctlCommandCreator(creator command.LimaCmdCreator, systemDeps NerdctlCommandSystemDeps, + logger flog.Logger, +) *nerdctlCommandCreator { + return &nerdctlCommandCreator{creator: creator, systemDeps: systemDeps, logger: logger} } func (ncc *nerdctlCommandCreator) create(cmdName string, cmdDesc string) *cobra.Command { @@ -35,19 +49,20 @@ 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.logger).runAdapter, + RunE: newNerdctlCommand(ncc.creator, ncc.systemDeps, ncc.logger).runAdapter, } return command } type nerdctlCommand struct { - creator command.LimaCmdCreator - logger flog.Logger + creator command.LimaCmdCreator + systemDeps NerdctlCommandSystemDeps + logger flog.Logger } -func newNerdctlCommand(creator command.LimaCmdCreator, logger flog.Logger) *nerdctlCommand { - return &nerdctlCommand{creator: creator, logger: logger} +func newNerdctlCommand(creator command.LimaCmdCreator, systemDeps NerdctlCommandSystemDeps, logger flog.Logger) *nerdctlCommand { + return &nerdctlCommand{creator: creator, systemDeps: systemDeps, logger: logger} } func (nc *nerdctlCommand) runAdapter(cmd *cobra.Command, args []string) error { @@ -59,18 +74,63 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { if err != nil { return err } + var ( + nerdctlArgs, envs, fileEnvs []string + skip bool + ) - var nerdctlArgs []string - for _, arg := range args { - if arg == "--debug" { + for i, arg := range args { + // parsing environment values from the command line may pre-fetch and + // consume the next argument; this loop variable will skip these pre-consumed + // entries from the command line + if skip { + skip = false + continue + } + switch { + case arg == "--debug": // explicitly setting log level to avoid `--debug` flag being interpreted as nerdctl command nc.logger.SetLevel(flog.Debug) - continue + case argIsEnv(arg): + shouldSkip, addEnv := handleEnv(nc.systemDeps, arg, args[i+1]) + skip = shouldSkip + if addEnv != "" { + envs = append(envs, addEnv) + } + + case strings.HasPrefix(arg, "--env-file"): + shouldSkip, addEnvs, err := handleEnvFile(nc.systemDeps, arg, args[i+1]) + if err != nil { + return err + } + skip = shouldSkip + fileEnvs = append(fileEnvs, addEnvs...) + default: + nerdctlArgs = append(nerdctlArgs, arg) } - nerdctlArgs = append(nerdctlArgs, arg) } + // to handle environment variables properly, we add all entries found via + // env-file includes to the map first and then all command line environment + // flags, making sure that command line overrides environment file options, + // and that later command line flags override earlier ones + envVars := make(map[string]string) - limaArgs := append([]string{"shell", limaInstanceName, nerdctlCmdName, cmdName}, nerdctlArgs...) + for _, e := range fileEnvs { + evar, eval, _ := strings.Cut(e, "=") + envVars[evar] = eval + } + for _, e := range envs { + evar, eval, _ := strings.Cut(e, "=") + envVars[evar] = eval + } + + var finalArgs []string + for key, val := range envVars { + finalArgs = append(finalArgs, "-e", fmt.Sprintf("%s=%s", key, val)) + } + finalArgs = append(finalArgs, nerdctlArgs...) + + limaArgs := append([]string{"shell", limaInstanceName, nerdctlCmdName, cmdName}, finalArgs...) if nc.shouldReplaceForHelp(cmdName, args) { return nc.creator.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, limaArgs...) @@ -122,6 +182,92 @@ func (nc *nerdctlCommand) shouldReplaceForHelp(cmdName string, args []string) bo return false } +func argIsEnv(arg string) bool { + if strings.HasPrefix(arg, "-e") || (strings.HasPrefix(arg, "--env") && !strings.HasPrefix(arg, "--env-file")) { + return true + } + return false +} + +func handleEnv(systemDeps NerdctlCommandSystemDeps, arg, arg2 string) (bool, string) { + var ( + envVar string + skip bool + ) + switch arg { + case "-e", "--env": + skip = true + envVar = arg2 + default: + // flag and value are in the same string + if strings.HasPrefix(arg, "-e") { + envVar = arg[2:] + } else { + // only other case is "--env="; skip that prefix + envVar = arg[6:] + } + } + + if strings.Contains(envVar, "=") { + return skip, envVar + } + // if no value was provided we need to check the OS environment + // for a value and only set if it exists in the current env + if val, ok := systemDeps.LookupEnv(envVar); ok { + return skip, fmt.Sprintf("%s=%s", envVar, val) + } + // no value found; do not set the variable in the env + return skip, "" +} + +func handleEnvFile(systemDeps NerdctlCommandSystemDeps, arg, arg2 string) (bool, []string, error) { + var ( + filename string + skip bool + ) + + switch arg { + case "--env-file": + skip = true + filename = arg2 + default: + filename = arg[11:] + } + + file, err := os.Open(filepath.Clean(filename)) + if err != nil { + return false, []string{}, err + } + defer file.Close() //nolint:errcheck,gosec // close of a file in O_RDONLY mode has no gosec issue + + scanner := bufio.NewScanner(file) + + var envs []string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if len(line) == 0 { + continue + } + switch { + case strings.HasPrefix(line, "#"): + // ignore comments + continue + case !strings.Contains(line, "="): + // only has the variable name; need to lookup value + if val, ok := systemDeps.LookupEnv(line); ok { + envs = append(envs, fmt.Sprintf("%s=%s", line, val)) + } + default: + // contains a name and value + envs = append(envs, line) + } + } + if err := scanner.Err(); err != nil { + return skip, []string{}, err + } + return skip, envs, nil +} + 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 57e4b78de..57ef0556f 100644 --- a/cmd/finch/nerdctl_test.go +++ b/cmd/finch/nerdctl_test.go @@ -6,6 +6,7 @@ package main import ( "errors" "fmt" + "os" "testing" "github.com/golang/mock/gomock" @@ -25,7 +26,7 @@ var testStdoutRs = []command.Replacement{ func TestNerdctlCommandCreator_create(t *testing.T) { t.Parallel() - cmd := newNerdctlCommandCreator(nil, nil).create("build", "build description") + cmd := newNerdctlCommandCreator(nil, nil, nil).create("build", "build description") assert.Equal(t, cmd.Name(), "build") assert.Equal(t, cmd.DisableFlagParsing, true) } @@ -64,10 +65,11 @@ func TestNerdctlCommand_runAdaptor(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) tc.mockSvc(lcc, logger, ctrl) - assert.NoError(t, newNerdctlCommand(lcc, logger).runAdapter(tc.cmd, tc.args)) + assert.NoError(t, newNerdctlCommand(lcc, ncsd, logger).runAdapter(tc.cmd, tc.args)) }) } } @@ -80,14 +82,14 @@ func TestNerdctlCommand_run(t *testing.T) { cmdName string args []string wantErr error - mockSvc func(*mocks.LimaCmdCreator, *mocks.Logger, *gomock.Controller) + mockSvc func(*mocks.LimaCmdCreator, *mocks.NerdctlCommandSystemDeps, *mocks.Logger, *gomock.Controller) }{ { name: "happy path", cmdName: "build", args: []string{"-t", "demo", "."}, wantErr: nil, - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) @@ -103,7 +105,7 @@ func TestNerdctlCommand_run(t *testing.T) { args: []string{"-t", "demo", "."}, wantErr: fmt.Errorf("instance %q is stopped, run `finch %s start` to start the instance", limaInstanceName, virtualMachineRootCmd), - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil) @@ -117,7 +119,7 @@ func TestNerdctlCommand_run(t *testing.T) { wantErr: fmt.Errorf( "instance %q does not exist, run `finch %s init` to create a new instance", limaInstanceName, virtualMachineRootCmd), - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte(""), nil) @@ -129,7 +131,7 @@ func TestNerdctlCommand_run(t *testing.T) { cmdName: "build", args: []string{"-t", "demo", "."}, wantErr: errors.New("unrecognized system status"), - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Broken"), nil) @@ -141,7 +143,7 @@ func TestNerdctlCommand_run(t *testing.T) { cmdName: "build", args: []string{"-t", "demo", "."}, wantErr: errors.New("get status error"), - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, 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")) @@ -152,7 +154,7 @@ func TestNerdctlCommand_run(t *testing.T) { cmdName: "pull", args: []string{"test:tag", "--debug"}, wantErr: nil, - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) @@ -163,12 +165,48 @@ func TestNerdctlCommand_run(t *testing.T) { c.EXPECT().Run() }, }, + { + name: "with environment flags parsing and env value doesn't exist", + cmdName: "run", + args: []string{"--rm", "-e", "ARG1=val1", "--env=ARG2", "-eARG3", "alpine:latest", "env"}, + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, 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") + c := mocks.NewCommand(ctrl) + ncsd.EXPECT().LookupEnv("ARG2") + ncsd.EXPECT().LookupEnv("ARG3") + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "run", + "-e", "ARG1=val1", "--rm", "alpine:latest", "env").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with environment flags parsing and env value exists", + cmdName: "run", + args: []string{"--rm", "--env=ARG2", "-eARG3", "alpine:latest", "env"}, + wantErr: nil, + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, 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") + c := mocks.NewCommand(ctrl) + ncsd.EXPECT().LookupEnv("ARG2") + ncsd.EXPECT().LookupEnv("ARG3").Return("val3", true) + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "run", + "-e", "ARG3=val3", "--rm", "alpine:latest", "env").Return(c) + c.EXPECT().Run() + }, + }, { name: "with --help flag", cmdName: "pull", args: []string{"test:tag", "--help"}, wantErr: nil, - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) @@ -182,7 +220,7 @@ func TestNerdctlCommand_run(t *testing.T) { cmdName: "pull", args: []string{"test:tag", "--help"}, wantErr: fmt.Errorf("failed to replace"), - mockSvc: func(lcc *mocks.LimaCmdCreator, logger *mocks.Logger, ctrl *gomock.Controller) { + mockSvc: func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, ctrl *gomock.Controller) { getVMStatusC := mocks.NewCommand(ctrl) lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) @@ -201,11 +239,75 @@ func TestNerdctlCommand_run(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) - tc.mockSvc(lcc, logger, ctrl) - assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, logger).run(tc.cmdName, tc.args)) + tc.mockSvc(lcc, ncsd, logger, ctrl) + assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, ncsd, logger).run(tc.cmdName, tc.args)) }) } + t.Run("with --env-file flag replacement", func(t *testing.T) { + t.Parallel() + f, err := os.CreateTemp("", "envfiletest") + assert.NoError(t, err) + defer func() { + f.Close() //nolint:errcheck,gosec //temp file during unit test + os.Remove(f.Name()) //nolint:errcheck,gosec //temp file during unit test + }() + envFileStr := "# a comment\nARG1=val1\n ARG2\n\n # a 2nd comment\nNOTSETARG\n " + _, err = f.Write([]byte(envFileStr)) + assert.NoError(t, err) + cmdName := "run" + args := []string{"--rm", fmt.Sprintf("--env-file=%s", f.Name()), "alpine:latest", "env"} + mockSvc := func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, 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") + c := mocks.NewCommand(ctrl) + ncsd.EXPECT().LookupEnv("ARG2") + ncsd.EXPECT().LookupEnv("NOTSETARG") + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "run", "-e", "ARG1=val1", "--rm", "alpine:latest", "env").Return(c) + c.EXPECT().Run() + } + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) + logger := mocks.NewLogger(ctrl) + mockSvc(lcc, ncsd, logger, ctrl) + assert.Equal(t, nil, newNerdctlCommand(lcc, ncsd, logger).run(cmdName, args)) + }) + + t.Run("with --env-file flag replacement and existing env value", func(t *testing.T) { + t.Parallel() + f, err := os.CreateTemp("", "envfiletest") + assert.NoError(t, err) + defer func() { + f.Close() //nolint:errcheck,gosec //temp file during unit test + os.Remove(f.Name()) //nolint:errcheck,gosec //temp file during unit test + }() + envFileStr := "# a comment\n ARG2\n\n # a 2nd comment\nNOTSETARG\n " + _, err = f.Write([]byte(envFileStr)) + assert.NoError(t, err) + cmdName := "run" + args := []string{"--rm", "--env-file", f.Name(), "alpine:latest", "env"} + mockSvc := func(lcc *mocks.LimaCmdCreator, ncsd *mocks.NerdctlCommandSystemDeps, logger *mocks.Logger, 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") + c := mocks.NewCommand(ctrl) + ncsd.EXPECT().LookupEnv("ARG2").Return("val2", true) + ncsd.EXPECT().LookupEnv("NOTSETARG") + lcc.EXPECT().Create("shell", limaInstanceName, nerdctlCmdName, "run", "-e", "ARG2=val2", "--rm", "alpine:latest", "env").Return(c) + c.EXPECT().Run() + } + ctrl := gomock.NewController(t) + lcc := mocks.NewLimaCmdCreator(ctrl) + ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) + logger := mocks.NewLogger(ctrl) + mockSvc(lcc, ncsd, logger, ctrl) + assert.Equal(t, nil, newNerdctlCommand(lcc, ncsd, logger).run(cmdName, args)) + }) } func TestNerdctlCommand_shouldReplaceForHelp(t *testing.T) { @@ -264,8 +366,9 @@ func TestNerdctlCommand_shouldReplaceForHelp(t *testing.T) { ctrl := gomock.NewController(t) lcc := mocks.NewLimaCmdCreator(ctrl) + ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) logger := mocks.NewLogger(ctrl) - assert.True(t, newNerdctlCommand(lcc, logger).shouldReplaceForHelp(tc.cmdName, tc.args)) + assert.True(t, newNerdctlCommand(lcc, ncsd, logger).shouldReplaceForHelp(tc.cmdName, tc.args)) }) } } diff --git a/pkg/mocks/nerdctl_cmd_system_deps.go b/pkg/mocks/nerdctl_cmd_system_deps.go new file mode 100644 index 000000000..68a6942f4 --- /dev/null +++ b/pkg/mocks/nerdctl_cmd_system_deps.go @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: nerdctl.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// NerdctlCommandSystemDeps is a mock of NerdctlCommandSystemDeps interface. +type NerdctlCommandSystemDeps struct { + ctrl *gomock.Controller + recorder *NerdctlCommandSystemDepsMockRecorder +} + +// NerdctlCommandSystemDepsMockRecorder is the mock recorder for NerdctlCommandSystemDeps. +type NerdctlCommandSystemDepsMockRecorder struct { + mock *NerdctlCommandSystemDeps +} + +// NewNerdctlCommandSystemDeps creates a new mock instance. +func NewNerdctlCommandSystemDeps(ctrl *gomock.Controller) *NerdctlCommandSystemDeps { + mock := &NerdctlCommandSystemDeps{ctrl: ctrl} + mock.recorder = &NerdctlCommandSystemDepsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *NerdctlCommandSystemDeps) EXPECT() *NerdctlCommandSystemDepsMockRecorder { + return m.recorder +} + +// LookupEnv mocks base method. +func (m *NerdctlCommandSystemDeps) LookupEnv(key string) (string, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupEnv", key) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// LookupEnv indicates an expected call of LookupEnv. +func (mr *NerdctlCommandSystemDepsMockRecorder) LookupEnv(key interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupEnv", reflect.TypeOf((*NerdctlCommandSystemDeps)(nil).LookupEnv), key) +} diff --git a/pkg/system/stdlib.go b/pkg/system/stdlib.go index 8dde8d2f4..0f9f1d1c7 100644 --- a/pkg/system/stdlib.go +++ b/pkg/system/stdlib.go @@ -38,6 +38,10 @@ func (s *StdLib) Env(key string) string { return os.Getenv(key) } +func (s *StdLib) LookupEnv(key string) (string, bool) { + return os.LookupEnv(key) +} + func (s *StdLib) Stdin() *os.File { return os.Stdin } diff --git a/pkg/system/system.go b/pkg/system/system.go index 985a9e060..92c6323e9 100644 --- a/pkg/system/system.go +++ b/pkg/system/system.go @@ -47,6 +47,11 @@ type EnvGetter interface { Env(key string) string } +// EnvChecker mocks out os.LookupEnv. +type EnvChecker interface { + LookupEnv(key string) (string, bool) +} + // StdinGetter mocks out os.Stdin. type StdinGetter interface { Stdin() *os.File