From d8cdca086d165d55128dac6ccf1f1efabfff46aa Mon Sep 17 00:00:00 2001 From: Shubharanshu Mahapatra Date: Wed, 4 Dec 2024 03:27:06 +0530 Subject: [PATCH] feat: refactor cli parsing Signed-off-by: Shubharanshu Mahapatra --- cmd/finch/devcontainer_patch.go | 18 +- cmd/finch/main.go | 25 +- cmd/finch/nerdctl.go | 397 ++++++++++----------- cmd/finch/nerdctl_darwin.go | 20 -- cmd/finch/nerdctl_darwin_test.go | 436 +++++++++++------------ cmd/finch/nerdctl_native.go | 104 +----- cmd/finch/nerdctl_remote.go | 587 ++++++++----------------------- cmd/finch/nerdctl_windows.go | 295 ---------------- go.mod | 2 +- 9 files changed, 587 insertions(+), 1297 deletions(-) diff --git a/cmd/finch/devcontainer_patch.go b/cmd/finch/devcontainer_patch.go index 65e66c6fc..aeefe51be 100644 --- a/cmd/finch/devcontainer_patch.go +++ b/cmd/finch/devcontainer_patch.go @@ -15,7 +15,6 @@ import ( "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/docker/go-connections/nat" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" "github.com/runfinch/finch/pkg/command" ) @@ -111,15 +110,12 @@ func inspectContainerOutputHandler(cmd command.Command) error { return err } -func handleDockerCompatComposeVersion(cmdName string, nc nerdctlCommand, runArgs []string) error { - if cmdName == "compose" && nc.fc.DockerCompat && slices.Contains(runArgs, "version") { - ver := nc.systemDeps.Env("DOCKER_COMPOSE_VERSION") - if ver != "" { - logrus.Warn("Displaying docker compose version set as environment variable DOCKER_COMPOSE_VERSION...") - fmt.Println(ver) - return nil - } - return errors.New("DOCKER_COMPOSE_VERSION environment variable is not set") +func handleDockerCompatComposeVersion(nc nerdctlCommand) error { + ver := nc.systemDeps.Env("DOCKER_COMPOSE_VERSION") + if ver != "" { + logrus.Warn("Displaying docker compose version set as environment variable DOCKER_COMPOSE_VERSION...") + fmt.Println(ver) + return nil } - return errors.New("") + return errors.New("DOCKER_COMPOSE_VERSION environment variable is not set") } diff --git a/cmd/finch/main.go b/cmd/finch/main.go index b605b2313..c5e40ee78 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -42,13 +42,26 @@ func initializeNerdctlCommands( nerdctlCommandCreator := newNerdctlCommandCreator(ncc, ecc, system.NewStdLib(), logger, fs, fc) var allNerdctlCommands []*cobra.Command for cmdName, cmdDescription := range nerdctlCmds { - allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) - } - - if fc != nil && fc.DockerCompat { - for cmdName, cmdDescription := range dockerCompatCmds { - allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) + if fc != nil && fc.DockerCompat { + switch cmdName { + case "build": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatBuildCmd()) + continue + case "image": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatImageCmd()) + continue + case "inspect": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatInspectCmd()) + continue + case "run": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatRunCmd()) + continue + case "compose": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatComposeCmd()) + continue + } } + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) } return allNerdctlCommands diff --git a/cmd/finch/nerdctl.go b/cmd/finch/nerdctl.go index 642ff30e2..f62062b63 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" "fmt" + "regexp" "strings" "golang.org/x/exp/slices" @@ -44,11 +45,6 @@ type nerdctlCommandCreator struct { fc *config.Finch } -type ( - argHandler func(systemDeps NerdctlCommandSystemDeps, fc *config.Finch, args []string, index int) error - commandHandler func(systemDeps NerdctlCommandSystemDeps, fc *config.Finch, cmdName *string, args *[]string, inspectType *string) error -) - func newNerdctlCommandCreator( ncc command.NerdctlCmdCreator, ecc command.Creator, @@ -74,6 +70,172 @@ func (ncc *nerdctlCommandCreator) create(cmdName string, cmdDesc string) *cobra. return command } +func (ncc *nerdctlCommandCreator) handleLoadOpt(args []string) []string { + for i, arg := range args { + if arg == "--load" { + args[i] = "--output=type=docker" + } + } + return args +} + +func (ncc *nerdctlCommandCreator) createDockerCompatRunCmd() *cobra.Command { + runCmd := &cobra.Command{ + Use: "run", + Short: "Run a command in a new container", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + cleanConsistencyOpt := func(arg string) string { + re := regexp.MustCompile(`consistency=[^,]*`) + arg = re.ReplaceAllString(arg, "") + arg = strings.ReplaceAll(arg, ",,", ",") + return strings.Trim(arg, ", ") + } + + for idx, arg := range args { + if arg == "--mount" && (idx < len(args)-1) { + args[idx+1] = cleanConsistencyOpt(args[idx+1]) + continue + } + + if strings.Contains(arg, "--mount") && strings.Contains(arg, "=") { + args[idx] = cleanConsistencyOpt(arg) + continue + } + } + + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runAdapter(cmd, args) + }, + } + return runCmd +} + +func (ncc *nerdctlCommandCreator) createDockerCompatBuildCmd() *cobra.Command { + buildCmd := &cobra.Command{ + Use: "build", + Aliases: []string{"buildx"}, + Short: "Build an image from Dockerfile", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.CalledAs() == "buildx" { + logrus.Warn("buildx is not supported. using standard buildkit instead...") + } + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runAdapter(cmd, args) + }, + } + + buildSubCmd := &cobra.Command{ + Use: "build", + Aliases: []string{"b"}, + Short: "Alias for main build command", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + logrus.Warn("buildx is not supported. using standard buildkit instead...") + args = ncc.handleLoadOpt(args) + return buildCmd.RunE(cmd, args) + }, + } + + buildUnsupportedSubCmd := &cobra.Command{ + Use: "unsupported", + Aliases: []string{"bake", "create", "debug", "du", "imagetools", "inspect", "ls", "prune", "rm", "stop", "use", "version"}, + Short: "Alias for main build command", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return fmt.Errorf("unsupported buildx command: %s", cmd.CalledAs()) + }, + } + + buildCmd.AddCommand(buildSubCmd) + buildCmd.AddCommand(buildUnsupportedSubCmd) + + return buildCmd +} + +func (ncc *nerdctlCommandCreator) createDockerCompatImageCmd() *cobra.Command { + imageCmd := &cobra.Command{ + Use: "image", + Short: "Manage images", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + ncc.handleLoadOpt(args) + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runAdapter(cmd, args) + }, + } + return imageCmd +} + +func (ncc *nerdctlCommandCreator) createDockerCompatComposeCmd() *cobra.Command { + composeCmd := &cobra.Command{ + Use: "compose", + Short: "Compose", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runAdapter(cmd, args) + }, + } + + composeVersionSubCmd := &cobra.Command{ + Use: "version", + Short: "Version", + DisableFlagParsing: true, + RunE: newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runDockerCompatComposeVersionAdapter, + } + + composeCmd.AddCommand(composeVersionSubCmd) + + return composeCmd +} + +func (ncc *nerdctlCommandCreator) createDockerCompatInspectCmd() *cobra.Command { + inspectCmd := &cobra.Command{ + Use: "inspect", + Short: "Return low-level information on Docker objects", + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + modeDockerCompat := "--mode=dockercompat" + var inspectType string + inspectType = "" + var processedArgs []string + var sizeArg string + for idx := 0; idx < len(args); idx++ { + if (args[idx] == "--type") && (idx < len(args)-1) { + inspectType = args[idx+1] + idx++ + continue + } + + if strings.Contains(args[idx], "--type") && strings.Contains(args[idx], "=") { + inspectType = strings.Split(args[idx], "=")[1] + continue + } + + if args[idx] == "-s" || args[idx] == "--size" { + sizeArg = "--size" + continue + } + processedArgs = append(processedArgs, args[idx]) + } + + switch inspectType { + case "image": + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runDockerCompatImageInspectAdapter(append([]string{"inspect", modeDockerCompat}, processedArgs...)) + case "volume": + if sizeArg != "" { + processedArgs = append([]string{sizeArg}, processedArgs...) + } else { + processedArgs = append([]string{}, processedArgs...) + } + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runDockerCompatVolumeInspectAdapter(append([]string{"inspect"}, processedArgs...)) + default: + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runDockerCompatInspectAdapter(cmd, + append([]string{modeDockerCompat}, processedArgs...)) + } + }, + } + return inspectCmd +} + type nerdctlCommand struct { ncc command.NerdctlCmdCreator ecc command.Creator @@ -98,6 +260,22 @@ func (nc *nerdctlCommand) runAdapter(cmd *cobra.Command, args []string) error { return nc.run(cmd.Name(), args) } +func (nc *nerdctlCommand) runDockerCompatInspectAdapter(cmd *cobra.Command, args []string) error { + return nc.runDockerCompatInspect(cmd.Name(), args) +} + +func (nc *nerdctlCommand) runDockerCompatVolumeInspectAdapter(args []string) error { + return nc.runDockerCompatInspect("volume", args) +} + +func (nc *nerdctlCommand) runDockerCompatImageInspectAdapter(args []string) error { + return nc.runDockerCompatInspect("image", args) +} + +func (nc *nerdctlCommand) runDockerCompatComposeVersionAdapter(_ *cobra.Command, _ []string) error { + return handleDockerCompatComposeVersion(*nc) +} + // shouldReplaceForHelp returns true if we should replace "nerdctl" with "finch" for the output of the given command. func (nc *nerdctlCommand) shouldReplaceForHelp(cmdName string, args []string) bool { // The implicit help commands mean that if users input "finch" without any args, it will return the help of it. @@ -199,212 +377,3 @@ var nerdctlCmds = map[string]string{ "volume": "Manage volumes", "wait": "Block until one or more containers stop, then print their exit codes", } - -var dockerCompatCmds = map[string]string{ - "buildx": "build version", -} - -var aliasMap = map[string]string{ - "build": "image build", - "run": "container run", - "cp": "container cp", -} - -var commandHandlerMap = map[string]commandHandler{ - "buildx": handleBuildx, - "inspect": handleDockerCompatInspect, -} - -var argHandlerMap = map[string]map[string]argHandler{ - "image build": { - "--load": handleDockerBuildLoad, - }, - "container run": { - "--mount": handleBindMounts, - }, -} - -var cmdFlagSetMap = map[string]map[string]sets.Set[string]{ - "container run": { - "shortBoolFlags": sets.New[string]("-d", "-i", "-t"), - "longBoolFlags": sets.New[string]( - "--detach", "--init", "--interactive", "--oom-kill-disable", - "--privileged", "--read-only", "--rm", "--rootfs", "--tty", "--sig-proxy"), - "shortArgFlags": sets.New[string]("-e", "-h", "-m", "-u", "-w", "-p", "-l", "-v"), - }, - "exec": { - "shortBoolFlags": sets.New[string]("-d", "-i", "-t"), - "longBoolFlags": sets.New[string]( - "--detach", "--init", "--interactive", "--oom-kill-disable", - "--privileged", "--read-only", "--rm", "--rootfs", "--tty"), - "shortArgFlags": sets.New[string]("-e", "-h", "-m", "-u", "-w", "-p", "-l", "-v"), - }, - "compose": { - "shortBoolFlags": sets.New[string]("-d", "-i", "-t"), - "longBoolFlags": sets.New[string]( - "--detach", "--init", "--interactive", "--oom-kill-disable", - "--privileged", "--read-only", "--rm", "--rootfs", "--tty"), - "shortArgFlags": sets.New[string]("-e", "-h", "-m", "-u", "-w", "-p", "-l", "-v"), - }, -} - -// converts "docker build --load" flag to "nerdctl build --output=type=docker". -func handleDockerBuildLoad(_ NerdctlCommandSystemDeps, fc *config.Finch, nerdctlCmdArgs []string, index int) error { - if fc != nil && fc.DockerCompat { - nerdctlCmdArgs[index] = "--output=type=docker" - } - - return nil -} - -func handleBuildx(_ NerdctlCommandSystemDeps, fc *config.Finch, cmdName *string, args *[]string, _ *string) error { - if fc == nil || !fc.DockerCompat { - return nil - } - - if cmdName != nil && *cmdName == "buildx" { - subCmd := (*args)[0] - buildxSubcommands := []string{"bake", "create", "debug", "du", "imagetools", "inspect", "ls", "prune", "rm", "stop", "use", "version"} - - if slices.Contains(buildxSubcommands, subCmd) { - return fmt.Errorf("unsupported buildx command: %s", subCmd) - } - - logrus.Warn("buildx is not supported. using standard buildkit instead...") - if subCmd == "build" { - *args = (*args)[1:] - } - *cmdName = "build" - } - // else, continue with the original command - return nil -} - -func handleDockerCompatInspect(_ NerdctlCommandSystemDeps, fc *config.Finch, cmdName *string, args *[]string, inspectType *string) error { - if fc == nil || !fc.DockerCompat { - return nil - } - - if *args == nil { - return fmt.Errorf("invalid arguments: args (null pointer)") - } - - modeDockerCompat := `--mode=dockercompat` - sizeArg := "" - savedArgs := []string{} - skip := false - *inspectType = "" - - for idx, arg := range *args { - if skip { - skip = false - continue - } - - if (arg == "--type") && (idx < len(*args)-1) { - *inspectType = (*args)[idx+1] - skip = true - continue - } - - if strings.Contains(arg, "--type") && strings.Contains(arg, "=") { - *inspectType = strings.Split(arg, "=")[1] - continue - } - - if (arg == "--size") || (arg == "-s") { - sizeArg = "--size" - continue - } - - savedArgs = append(savedArgs, arg) - } - - switch *inspectType { - case "image": - *cmdName = "image inspect" - *args = append([]string{modeDockerCompat}, savedArgs...) - case "volume": - *cmdName = "volume inspect" - if sizeArg != "" { - *args = append([]string{sizeArg}, savedArgs...) - } else { - *args = append([]string{}, savedArgs...) - } - case "container": - *cmdName = "inspect" - *args = append([]string{modeDockerCompat}, savedArgs...) - case "": - *cmdName = "inspect" - *args = append([]string{modeDockerCompat}, savedArgs...) - *inspectType = "container" - default: - return fmt.Errorf("unsupported inspect type: %s", *inspectType) - } - - return nil -} - -// handles the argument & value of --mount option -// -// invokes OS specific path handler for source path of the bind mount -// and removes the consistency key-value entity from value -func handleBindMounts(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { - prefix := nerdctlCmdArgs[index] - var ( - v string - found bool - before string - ) - if strings.Contains(nerdctlCmdArgs[index], "=") { - before, v, found = strings.Cut(prefix, "=") - } else { - if (index + 1) < len(nerdctlCmdArgs) { - v = nerdctlCmdArgs[index+1] - } else { - return fmt.Errorf("invalid positional parameter for %s", prefix) - } - } - - // eg --mount type=bind,source="$(pwd)"/target,target=/app,readonly - // eg --mount type=bind, source=${pwd}/source_dir, target=/target_dir, consistency=cached - // https://docs.docker.com/storage/bind-mounts/#choose-the--v-or---mount-flag order does not matter, so convert to a map - entries := strings.Split(v, ",") - m := make(map[string]string) - ro := []string{} - for _, e := range entries { - parts := strings.Split(e, "=") - if len(parts) < 2 { - ro = append(ro, parts...) - } else { - m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - // Check if type is bind mount, else return - if m["type"] != "bind" { - return nil - } - - // Remove 'consistency' key-value pair, if present - delete(m, "consistency") - - // Invoke the OS specific path handler - err := handleBindMountPath(systemDeps, m) - if err != nil { - return err - } - - // Convert to string representation - s := mapToString(m) - // append read-only key if present - if len(ro) > 0 { - s = s + "," + strings.Join(ro, ",") - } - if found { - nerdctlCmdArgs[index] = fmt.Sprintf("%s=%s", before, s) - } else { - nerdctlCmdArgs[index+1] = s - } - - return nil -} diff --git a/cmd/finch/nerdctl_darwin.go b/cmd/finch/nerdctl_darwin.go index 437ee043e..a96ab1aa2 100644 --- a/cmd/finch/nerdctl_darwin.go +++ b/cmd/finch/nerdctl_darwin.go @@ -20,12 +20,6 @@ func convertToWSLPath(_ NerdctlCommandSystemDeps, _ string) (string, error) { return "", nil } -var osAliasMap = map[string]string{} - -var osArgHandlerMap = map[string]map[string]argHandler{} - -var osCommandHandlerMap = map[string]commandHandler{} - func (nc *nerdctlCommand) GetCmdArgs() []string { return []string{"shell", limaInstanceName, "sudo", "-E"} } @@ -44,17 +38,3 @@ func resolveIP(host string, logger flog.Logger, _ command.Creator) (string, erro } return host, nil } - -func handleBindMountPath(_ NerdctlCommandSystemDeps, _ map[string]string) error { - // Do nothing by default - return nil -} - -func mapToString(m map[string]string) string { - var parts []string - for k, v := range m { - part := fmt.Sprintf("%s=%s", k, v) - parts = append(parts, part) - } - return strings.Join(parts, ",") -} diff --git a/cmd/finch/nerdctl_darwin_test.go b/cmd/finch/nerdctl_darwin_test.go index 6f5db0164..e97115694 100644 --- a/cmd/finch/nerdctl_darwin_test.go +++ b/cmd/finch/nerdctl_darwin_test.go @@ -9,7 +9,6 @@ import ( "bytes" "errors" "fmt" - "os" "path/filepath" "strings" "testing" @@ -539,7 +538,7 @@ func TestNerdctlCommand_run(t *testing.T) { envFilePath := filepath.Join(string(filepath.Separator), "env-file") testCases := []struct { name string - cmdName string + cmd *cobra.Command fc *config.Finch args []string wantErr error @@ -554,9 +553,11 @@ func TestNerdctlCommand_run(t *testing.T) { ) }{ { - name: "with single option flag", - cmdName: "run", - fc: &config.Finch{}, + name: "with single option flag", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "-it", "alpine:latest", "env", }, @@ -576,15 +577,17 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "-it", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with explicit env flag parsing", - cmdName: "run", - fc: &config.Finch{}, + name: "with explicit env flag parsing", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "-it", "-e", "ARG1=val1", "--env=ARG2=val2", "-eARG3=val3", "--name", "myContainer", "--rm", "alpine:latest", "env", @@ -605,15 +608,17 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-it", "--name", "myContainer", "--rm", "-e", "ARG1=val1", "-e", "ARG2=val2", "-e", "ARG3=val3", - "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "-it", "-e", "ARG1=val1", "--env=ARG2=val2", "-eARG3=val3", + "--name", "myContainer", "--rm", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with implicit env flag parsing; values exist in host env", - cmdName: "run", + name: "with implicit env flag parsing; values exist in host env", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"-it", "-e", "ARG1", "--env=ARG2", "-eARG3", "--rm", "--name", "myContainer", "alpine:latest", "env"}, wantErr: nil, @@ -633,18 +638,18 @@ func TestNerdctlCommand_run(t *testing.T) { AddEmptyEnvLookUps(ncsd) ncsd.EXPECT().LookupEnv("ARG1").Return("val1", true) ncsd.EXPECT().LookupEnv("ARG2").Return("val2", true) - ncsd.EXPECT().LookupEnv("ARG3").Return("val3", true) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-it", "--rm", "--name", "myContainer", "-e", "ARG1=val1", "-e", "ARG2=val2", "-e", "ARG3=val3", - "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", "ARG1=val1", "ARG2=val2", nerdctlCmdName, "run", + "-it", "-e", "ARG1", "--env=ARG2", "-eARG3", "--rm", "--name", "myContainer", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with implicit env flag parsing; values do not exist", - cmdName: "run", - fc: &config.Finch{}, + name: "with implicit env flag parsing; values do not exist", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "--name", "myContainer", "-it", "-e", "ARG0=val0", "-e", "ARG1", "--env=ARG2", "-eARG3", "--rm", "alpine:latest", "env", @@ -666,17 +671,19 @@ func TestNerdctlCommand_run(t *testing.T) { AddEmptyEnvLookUps(ncsd) ncsd.EXPECT().LookupEnv("ARG1") ncsd.EXPECT().LookupEnv("ARG2") - ncsd.EXPECT().LookupEnv("ARG3") c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--name", "myContainer", "-it", "--rm", "-e", "ARG0=val0", "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "--name", "myContainer", "-it", "-e", "ARG0=val0", "-e", "ARG1", "--env=ARG2", "-eARG3", + "--rm", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with explicit env flag parsing and debug mode", - cmdName: "run", - fc: &config.Finch{}, + name: "with explicit env flag parsing and debug mode", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "--debug", "--name", "myContainer", "--rm", "-e", "ARG1=val1", "--env=ARG2=val2", "-it", "-eARG3=val3", "alpine:latest", "env", @@ -698,15 +705,17 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--name", "myContainer", "--rm", "-it", "-e", "ARG1=val1", "-e", "ARG2=val2", "-e", "ARG3=val3", - "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "--name", "myContainer", "--rm", "-e", "ARG1=val1", "--env=ARG2=val2", + "-it", "-eARG3=val3", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with implicit env flag parsing and debug mode; values exist in host env", - cmdName: "run", + name: "with implicit env flag parsing and debug mode; values exist in host env", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--debug", "--rm", "--name", "myContainer", "-e", "ARG1", "--env=ARG2", "-it", "-eARG3", "alpine:latest", "env"}, wantErr: nil, @@ -727,18 +736,18 @@ func TestNerdctlCommand_run(t *testing.T) { AddEmptyEnvLookUps(ncsd) ncsd.EXPECT().LookupEnv("ARG1").Return("val1", true) ncsd.EXPECT().LookupEnv("ARG2").Return("val2", true) - ncsd.EXPECT().LookupEnv("ARG3").Return("val3", true) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--rm", "--name", "myContainer", "-it", "-e", "ARG1=val1", "-e", "ARG2=val2", "-e", "ARG3=val3", - "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", "ARG1=val1", "ARG2=val2", nerdctlCmdName, "run", + "--rm", "--name", "myContainer", "-e", "ARG1", "--env=ARG2", "-it", "-eARG3", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with implicit env flag parsing and debug mode; values do not exist", - cmdName: "run", - fc: &config.Finch{}, + name: "with implicit env flag parsing and debug mode; values do not exist", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "--debug", "--rm", "-e", "ARG0=val0", "-e", "ARG1", "--env=ARG2", "-it", "--name", "myContainer", "-eARG3", "alpine:latest", "env", @@ -761,17 +770,19 @@ func TestNerdctlCommand_run(t *testing.T) { AddEmptyEnvLookUps(ncsd) ncsd.EXPECT().LookupEnv("ARG1") ncsd.EXPECT().LookupEnv("ARG2") - ncsd.EXPECT().LookupEnv("ARG3") c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--rm", "-it", "--name", "myContainer", "-e", "ARG0=val0", "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "--rm", "-e", "ARG0=val0", "-e", "ARG1", "--env=ARG2", "-it", + "--name", "myContainer", "-eARG3", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with explicit env flag parsing and image args", - cmdName: "run", - fc: &config.Finch{}, + name: "with explicit env flag parsing and image args", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "--debug", "-i", "--name", "myContainer", "--rm", "-e", "ARG1=val1", "--env=ARG2=val2", "-t", "-eARG3=val3", "busybox:latest", "echo", "-e", "hello\tbye", @@ -792,16 +803,19 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().SetLevel(flog.Debug) logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) + ncsd.EXPECT().LookupEnv("hello\tbye") c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-i", "--name", "myContainer", "--rm", "-t", "-e", "ARG1=val1", "-e", "ARG2=val2", "-e", "ARG3=val3", - "busybox:latest", "echo", "-e", "hello\tbye").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "-i", "--name", "myContainer", "--rm", "-e", "ARG1=val1", "--env=ARG2=val2", + "-t", "-eARG3=val3", "busybox:latest", "echo", "-e", "hello\tbye").Return(c) c.EXPECT().Run() }, }, { - name: "with --env-file flag replacement", - cmdName: "run", + name: "with --env-file flag replacement", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"-i", "--name", "myContainer", "--rm", "--env-file=/env-file", "alpine:latest", "env"}, wantErr: nil, @@ -826,15 +840,16 @@ func TestNerdctlCommand_run(t *testing.T) { ncsd.EXPECT().LookupEnv("ARG2") ncsd.EXPECT().LookupEnv("NOTSETARG") lcc.EXPECT(). - Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-i", "--name", "myContainer", "--rm", "-e", "ARG1=val1", - "alpine:latest", "env").Return(c) + Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "-i", "--name", "myContainer", "--rm", "--env-file=/env-file", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "with --env-file flag replacement and with --debug flag; implicit value not present", - cmdName: "run", + name: "with --env-file flag replacement and with --debug flag; implicit value not present", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--debug", "--rm", "--env-file=/env-file", "alpine:latest", "env"}, wantErr: nil, @@ -861,16 +876,18 @@ func TestNerdctlCommand_run(t *testing.T) { ncsd.EXPECT().LookupEnv("NOTSETARG") lcc.EXPECT(). Create( - "shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", "--rm", - "-e", "ARG1=val1", "alpine:latest", "env", + "shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", + "--env-file=/env-file", "alpine:latest", "env", ). Return(c) c.EXPECT().Run() }, }, { - name: "with --env-file flag replacement and existing env value", - cmdName: "run", + name: "with --env-file flag replacement and existing env value", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--rm", "--env-file", envFilePath, "alpine:latest", "env"}, wantErr: nil, @@ -896,35 +913,39 @@ func TestNerdctlCommand_run(t *testing.T) { ncsd.EXPECT().LookupEnv("NOTSETARG") lcc.EXPECT(). Create( - "shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--rm", "-e", "ARG2=val2", "alpine:latest", "env", + "shell", limaInstanceName, "sudo", "-E", "ARG2=val2", nerdctlCmdName, "run", + "--rm", "--env-file", envFilePath, "alpine:latest", "env", ).Return(c) c.EXPECT().Run() }, }, + // { + // name: "with --env-file flag, but the specified file does not exist", + // cmd: &cobra.Command{ + // Use: "run", + // }, + // args: []string{"--rm", "--env-file", envFilePath, "alpine:latest", "env"}, + // wantErr: &os.PathError{Op: "open", Path: envFilePath, Err: afero.ErrFileNotFound}, + // mockSvc: func( + // _ *testing.T, + // lcc *mocks.NerdctlCmdCreator, + // _ *mocks.CommandCreator, + // _ *mocks.NerdctlCommandSystemDeps, + // logger *mocks.Logger, + // ctrl *gomock.Controller, + // _ 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") + // }, + // }, { - name: "with --env-file flag, but the specified file does not exist", - cmdName: "run", - args: []string{"--rm", "--env-file", envFilePath, "alpine:latest", "env"}, - wantErr: &os.PathError{Op: "open", Path: envFilePath, Err: afero.ErrFileNotFound}, - mockSvc: func( - _ *testing.T, - lcc *mocks.NerdctlCmdCreator, - _ *mocks.CommandCreator, - _ *mocks.NerdctlCommandSystemDeps, - logger *mocks.Logger, - ctrl *gomock.Controller, - _ 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") + name: "with --add-host flag and special IP by space", + cmd: &cobra.Command{ + Use: "run", }, - }, - { - 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, @@ -944,14 +965,16 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") logger.EXPECT().Debugf(`Resolving special IP "host-gateway" to %q for host %q`, "192.168.5.2", "name") c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", "--add-host", "name:192.168.5.2", "alpine:latest").Return(c) c.EXPECT().Run() }, }, { - name: "with --add-host flag but without using special IP by space", - cmdName: "run", + name: "with --add-host flag but without using special IP by space", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--rm", "--add-host", "name:0.0.0.0", "alpine:latest"}, wantErr: nil, @@ -970,14 +993,16 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", "--add-host", "name:0.0.0.0", "alpine:latest").Return(c) c.EXPECT().Run() }, }, { - name: "with --add-host flag but without subsequent arg", - cmdName: "run", + name: "with --add-host flag but without subsequent arg", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--rm", "--add-host", "alpine:latest"}, wantErr: errors.New("run cmd error"), @@ -996,14 +1021,16 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", "--add-host", "alpine:latest").Return(c) c.EXPECT().Run().Return(errors.New("run cmd error")) }, }, { - name: "with --add-host flag and special IP by equal", - cmdName: "run", + name: "with --add-host flag and special IP by equal", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--rm", "--add-host=name:host-gateway", "alpine:latest"}, wantErr: nil, @@ -1023,14 +1050,16 @@ func TestNerdctlCommand_run(t *testing.T) { AddEmptyEnvLookUps(ncsd) logger.EXPECT().Debugf(`Resolving special IP "host-gateway" to %q for host %q`, "192.168.5.2", "name") c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", "--add-host=name:192.168.5.2", "alpine:latest").Return(c) c.EXPECT().Run() }, }, { - name: "with --add-host flag but without using special IP by equal", - cmdName: "run", + name: "with --add-host flag but without using special IP by equal", + cmd: &cobra.Command{ + Use: "run", + }, fc: &config.Finch{}, args: []string{"--rm", "--add-host=name:0.0.0.0", "alpine:latest"}, wantErr: nil, @@ -1049,15 +1078,17 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", "--rm", "--add-host=name:0.0.0.0", "alpine:latest").Return(c) c.EXPECT().Run() }, }, { - name: "with multiple nested volumes", - cmdName: "run", - fc: &config.Finch{}, + name: "with multiple nested volumes", + cmd: &cobra.Command{ + Use: "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", @@ -1078,16 +1109,18 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "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").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "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").Return(c) c.EXPECT().Run() }, }, { - name: "with multiple nested volumes with full container run command", - cmdName: "container", - fc: &config.Finch{}, + name: "with multiple nested volumes with full container run command", + cmd: &cobra.Command{ + Use: "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", @@ -1116,9 +1149,11 @@ func TestNerdctlCommand_run(t *testing.T) { }, }, { - name: "with combo short flag parsing", - cmdName: "run", - fc: &config.Finch{}, + name: "with combo short flag parsing", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "-ie", "ARG1=val1", "-dp", "8080:8080", "--name", "myContainer", "--rm", "alpine:latest", "env", @@ -1139,16 +1174,22 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-i", "-d", "-p", "8080:8080", "--name", "myContainer", "--rm", "-e", "ARG1=val1", - "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "-ie", "ARG1=val1", "-dp", "8080:8080", + "--name", "myContainer", "--rm", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, { - name: "bindmount with src and consistency", - cmdName: "run", - fc: &config.Finch{}, + name: "bindmount with src and consistency", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, args: []string{"--mount", "type=bind,src=./src,consistency=cached", "alpine:latest"}, wantErr: nil, mockSvc: func( @@ -1166,15 +1207,17 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "--mount", ContainsMultipleStrs([]string{"bind", "type", "!consistency"}), "alpine:latest").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "--mount", "type=bind,src=./src", "alpine:latest").Return(c) c.EXPECT().Run() }, }, { - name: "with long-form boolean flags", - cmdName: "run", - fc: &config.Finch{}, + name: "with long-form boolean flags", + cmd: &cobra.Command{ + Use: "run", + }, + fc: &config.Finch{}, args: []string{ "--env", "ARG1=val1", "-p", "8080:8080", "--name", "myContainer", "--interactive=true", "--detach", "--rm=true", "--init=false", @@ -1198,11 +1241,12 @@ func TestNerdctlCommand_run(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "container", "run", - "-p", "8080:8080", "--name", "myContainer", "--interactive=true", "--detach", "--rm=true", - "--init=false", "--tty=true", "--debug-full", "false", "--sig-proxy=0", - "--experimental", "false", "--oom-kill-disable=false", "--read-only=false", - "--privileged=false", "-e", "ARG1=val1", "alpine:latest", "env").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "run", + "--env", "ARG1=val1", "-p", "8080:8080", + "--name", "myContainer", "--interactive=true", "--detach", "--rm=true", "--init=false", + "--tty=true", "--debug-full=false", "--sig-proxy=0", + "--experimental=false", "--oom-kill-disable=false", "--read-only=false", + "--privileged=false", "alpine:latest", "env").Return(c) c.EXPECT().Run() }, }, @@ -1220,8 +1264,11 @@ func TestNerdctlCommand_run(t *testing.T) { logger := mocks.NewLogger(ctrl) fs := afero.NewMemMapFs() 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)) + if tc.fc.DockerCompat { + assert.Equal(t, tc.wantErr, newNerdctlCommandCreator(lcc, ecc, ncsd, logger, fs, tc.fc).createDockerCompatRunCmd().RunE(tc.cmd, tc.args)) + } else { + assert.Equal(t, tc.wantErr, newNerdctlCommand(lcc, ecc, ncsd, logger, fs, tc.fc).run(tc.cmd.Name(), tc.args)) + } }) } } @@ -1230,7 +1277,7 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { t.Parallel() testCases := []struct { name string - cmdName string + cmd *cobra.Command fc *config.Finch args []string wantErr error @@ -1245,8 +1292,10 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { ) }{ { - name: "inspect without flags", - cmdName: "inspect", + name: "inspect without flags", + cmd: &cobra.Command{ + Use: "inspect", + }, fc: &config.Finch{ SharedSettings: config.SharedSettings{ DockerCompat: true, @@ -1277,8 +1326,10 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { }, }, { - name: "inspect with typeContainer flag", - cmdName: "inspect", + name: "inspect with typeContainer flag", + cmd: &cobra.Command{ + Use: "inspect", + }, fc: &config.Finch{ SharedSettings: config.SharedSettings{ DockerCompat: true, @@ -1309,8 +1360,10 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { }, }, { - name: "inspect with typeVolume option", - cmdName: "inspect", + name: "inspect with typeVolume option", + cmd: &cobra.Command{ + Use: "inspect", + }, fc: &config.Finch{ SharedSettings: config.SharedSettings{ DockerCompat: true, @@ -1333,13 +1386,16 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "volume", "inspect", "myVolume").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "volume", "inspect", "myVolume", "--format", "{{json .}}").Return(c) + c.EXPECT().SetStdout(gomock.Any()) c.EXPECT().Run() }, }, { - name: "inspect with typeImage option", - cmdName: "inspect", + name: "inspect with typeImage option", + cmd: &cobra.Command{ + Use: "inspect", + }, fc: &config.Finch{ SharedSettings: config.SharedSettings{ DockerCompat: true, @@ -1372,13 +1428,18 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { "inspect", "--mode=dockercompat", "myImage", + "--format", + "{{json .}}", ).Return(c) + c.EXPECT().SetStdout(gomock.Any()) c.EXPECT().Run() }, }, { - name: "inspect with size flag", - cmdName: "inspect", + name: "inspect with size flag", + cmd: &cobra.Command{ + Use: "inspect", + }, fc: &config.Finch{ SharedSettings: config.SharedSettings{ DockerCompat: true, @@ -1423,103 +1484,14 @@ func TestNerdctlCommand_run_inspectCommand(t *testing.T) { fs := afero.NewMemMapFs() tc.mockSvc(t, ncc, ecc, ncsd, logger, ctrl, fs) - assert.Equal(t, tc.wantErr, newNerdctlCommand(ncc, ecc, ncsd, logger, fs, tc.fc).run(tc.cmdName, tc.args)) - }) - } -} - -func TestNerdctlCommand_run_buildxCommand(t *testing.T) { - t.Parallel() - testCases := []struct { - name string - cmdName string - fc *config.Finch - args []string - wantErr error - mockSvc func( - t *testing.T, - lcc *mocks.NerdctlCmdCreator, - ecc *mocks.CommandCreator, - ncsd *mocks.NerdctlCommandSystemDeps, - logger *mocks.Logger, - ctrl *gomock.Controller, - fs afero.Fs, - ) - }{ - { - name: "docker buildx build", - cmdName: "buildx", - fc: &config.Finch{ - SharedSettings: config.SharedSettings{ - DockerCompat: true, - }, - }, - args: []string{"build", "-t", "demo", "."}, - wantErr: nil, - mockSvc: func( - _ *testing.T, - lcc *mocks.NerdctlCmdCreator, - _ *mocks.CommandCreator, - ncsd *mocks.NerdctlCommandSystemDeps, - logger *mocks.Logger, - ctrl *gomock.Controller, - _ 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") - AddEmptyEnvLookUps(ncsd) - c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "build", "-t", "demo", ".").Return(c) - c.EXPECT().Run() - }, - }, - { - name: "docker buildx version", - cmdName: "buildx", - fc: &config.Finch{ - SharedSettings: config.SharedSettings{ - DockerCompat: true, - }, - }, - args: []string{"version"}, - wantErr: fmt.Errorf("unsupported buildx command: version"), - mockSvc: func( - _ *testing.T, - lcc *mocks.NerdctlCmdCreator, - _ *mocks.CommandCreator, - _ *mocks.NerdctlCommandSystemDeps, - logger *mocks.Logger, - ctrl *gomock.Controller, - _ 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") - }, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - ncc := mocks.NewNerdctlCmdCreator(ctrl) - ecc := mocks.NewCommandCreator(ctrl) - ncsd := mocks.NewNerdctlCommandSystemDeps(ctrl) - logger := mocks.NewLogger(ctrl) - fs := afero.NewMemMapFs() - tc.mockSvc(t, ncc, ecc, ncsd, logger, ctrl, fs) - - assert.Equal(t, tc.wantErr, newNerdctlCommand(ncc, ecc, ncsd, logger, fs, tc.fc).run(tc.cmdName, tc.args)) + if tc.fc.DockerCompat { + assert.Equal(t, tc.wantErr, newNerdctlCommandCreator(ncc, ecc, ncsd, logger, fs, tc.fc).createDockerCompatInspectCmd().RunE(tc.cmd, tc.args)) + } else { + assert.Equal(t, tc.wantErr, newNerdctlCommand(ncc, ecc, ncsd, logger, fs, tc.fc).run(tc.cmd.Name(), tc.args)) + } }) } } - func TestNerdctlCommand_run_miscCommand(t *testing.T) { t.Parallel() testCases := []struct { @@ -1559,7 +1531,7 @@ func TestNerdctlCommand_run_miscCommand(t *testing.T) { logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") AddEmptyEnvLookUps(ncsd) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "image", "build", "-t", "demo", ".").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "build", "-t", "demo", ".").Return(c) c.EXPECT().Run() }, }, diff --git a/cmd/finch/nerdctl_native.go b/cmd/finch/nerdctl_native.go index 9a2d3f524..1471791cf 100644 --- a/cmd/finch/nerdctl_native.go +++ b/cmd/finch/nerdctl_native.go @@ -6,9 +6,6 @@ package main import ( - "fmt" - "strings" - "golang.org/x/exp/slices" "github.com/runfinch/finch/pkg/command" @@ -16,101 +13,34 @@ import ( ) func (nc *nerdctlCommand) run(cmdName string, args []string) error { - var ( - hasCmdHandler, hasArgHandler bool - cmdHandler commandHandler - aMap map[string]argHandler - err error - inspectType string - ) - - // eat the debug arg, and set the log level to avoid nerdctl parsing this flag - dbgIdx := slices.Index(args, "--debug") - if dbgIdx >= 0 { - args = append(args[:dbgIdx], args[dbgIdx+1:]...) - nc.logger.SetLevel(flog.Debug) - } - - alias, hasAlias := aliasMap[cmdName] - if hasAlias { - cmdHandler, hasCmdHandler = commandHandlerMap[alias] - aMap, hasArgHandler = argHandlerMap[alias] - } else { - cmdHandler, hasCmdHandler = commandHandlerMap[cmdName] - aMap, hasArgHandler = argHandlerMap[cmdName] - - if !hasArgHandler && len(args) > 0 { - // for commands like image build, container run - key := fmt.Sprintf("%s %s", cmdName, args[0]) - cmdHandler, hasCmdHandler = commandHandlerMap[key] - aMap, hasArgHandler = argHandlerMap[key] - } - } - - // First check if the command has a command handler - if hasCmdHandler { - err := cmdHandler(nc.systemDeps, nc.fc, &cmdName, &args, &inspectType) - if err != nil { - return err - } - } - - for i, arg := range args { - // Check if individual argument (and possibly following value) requires manipulation-in-place handling - if hasArgHandler { - // Check if argument for the command needs handling, sometimes it can be --file= - b, _, _ := strings.Cut(arg, "=") - h, ok := aMap[b] - if ok { - err = h(nc.systemDeps, nc.fc, args, i) - if err != nil { - return err - } - } - } - } - - // Extra manipulation for cases that overwrite cmdName with alias - splitName := strings.Split(cmdName, " ") - cmdArgs := append([]string{splitName[0]}, splitName[1:]...) - cmdArgs = append(cmdArgs, args...) - - if nc.shouldReplaceForHelp(splitName[0], args) { + args = nc.preprocessArgs(args) + cmdArgs := append([]string{cmdName}, args...) + if nc.shouldReplaceForHelp(cmdName, args) { return nc.ncc.RunWithReplacingStdout( []command.Replacement{{Source: "nerdctl", Target: "finch"}}, cmdArgs..., ) } - - if inspectType == "container" && nc.fc.DockerCompat && !slices.Contains(cmdArgs, "--format") { - cmdArgs = append(cmdArgs, "--format", "{{json .}}") - cmd := nc.ncc.Create(cmdArgs...) - return inspectContainerOutputHandler(cmd) - } - - if err := handleDockerCompatComposeVersion(cmdName, *nc, cmdArgs); err == nil { - return nil - } - return nc.ncc.Create(cmdArgs...).Run() } -var osAliasMap = map[string]string{} +func (nc *nerdctlCommand) runDockerCompatInspect(cmdName string, args []string) error { + args = nc.preprocessArgs(args) -var osArgHandlerMap = map[string]map[string]argHandler{} + if !slices.Contains(args, "--format") { + args = append(args, "--format", "{{json .}}") + } -var osCommandHandlerMap = map[string]commandHandler{} + cmd := nc.ncc.Create(append([]string{cmdName}, args...)...) + return inspectContainerOutputHandler(cmd) +} -func mapToString(m map[string]string) string { - var parts []string - for k, v := range m { - part := fmt.Sprintf("%s=%s", k, v) - parts = append(parts, part) +func (nc *nerdctlCommand) preprocessArgs(args []string) []string { + dbgIdx := slices.Index(args, "--debug") + if dbgIdx >= 0 { + args = append(args[:dbgIdx], args[dbgIdx+1:]...) + nc.logger.SetLevel(flog.Debug) } - return strings.Join(parts, ",") -} -func handleBindMountPath(_ NerdctlCommandSystemDeps, _ map[string]string) error { - // Do nothing by default - return nil + return args } diff --git a/cmd/finch/nerdctl_remote.go b/cmd/finch/nerdctl_remote.go index 6f679e152..b70a5e2c6 100644 --- a/cmd/finch/nerdctl_remote.go +++ b/cmd/finch/nerdctl_remote.go @@ -8,16 +8,13 @@ package main import ( "bufio" "fmt" - "maps" "path/filepath" + "regexp" "runtime" "strings" orderedmap "github.com/wk8/go-ordered-map" "golang.org/x/exp/slices" - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/spf13/afero" "github.com/runfinch/finch/pkg/command" "github.com/runfinch/finch/pkg/flog" @@ -27,325 +24,195 @@ import ( const nerdctlCmdName = "nerdctl" func (nc *nerdctlCommand) run(cmdName string, args []string) error { - err := nc.assertVMIsRunning(nc.ncc, nc.logger) - if err != nil { + if err := nc.assertVMIsRunning(nc.ncc, nc.logger); err != nil { return err } - var ( - nerdctlArgs, envs, fileEnvs, cmdArgs, runArgs []string - skip, hasCmdHandler, hasArgHandler, lastOpt bool - cmdHandler commandHandler - aMap map[string]argHandler - firstOptPos int - inspectType string - ) - // accumulate distributed map entities - aggAliasMap := make(map[string]string) - maps.Copy(aggAliasMap, aliasMap) - maps.Copy(aggAliasMap, osAliasMap) - - aggCmdHandlerMap := make(map[string]commandHandler) - maps.Copy(aggCmdHandlerMap, commandHandlerMap) - maps.Copy(aggCmdHandlerMap, osCommandHandlerMap) + envSet := nc.initializeEnvSet() + args, err := nc.processArgs(args, envSet) + if err != nil { + return err + } - aggArgHandlerMap := make(map[string]map[string]argHandler) - for k := range argHandlerMap { - aggArgHandlerMap[k] = make(map[string]argHandler) - maps.Copy(aggArgHandlerMap[k], argHandlerMap[k]) + envArgs, err := nc.getEnvArgs(envSet) + if err != nil { + return err } - for k := range osArgHandlerMap { - if _, ok := aggArgHandlerMap[k]; !ok { - aggArgHandlerMap[k] = make(map[string]argHandler) - } - maps.Copy(aggArgHandlerMap[k], osArgHandlerMap[k]) + remoteCredentialEnv := nc.getRemoteCredentialEnv() + limaArgs := append(nc.GetCmdArgs(), append(remoteCredentialEnv, envArgs...)...) + runArgs := append(limaArgs, append([]string{nerdctlCmdName}, strings.Fields(cmdName)...)...) + runArgs = append(runArgs, args...) + + if nc.shouldReplaceForHelp(cmdName, args) { + return nc.ncc.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, runArgs...) } + return nc.ncc.Create(runArgs...).Run() +} - alias, hasAlias := aggAliasMap[cmdName] - if hasAlias { - cmdName = alias - cmdHandler, hasCmdHandler = aggCmdHandlerMap[alias] - aMap, hasArgHandler = aggArgHandlerMap[alias] - } else { - // Check if the command has a handler - cmdHandler, hasCmdHandler = aggCmdHandlerMap[cmdName] - aMap, hasArgHandler = aggArgHandlerMap[cmdName] +func (nc *nerdctlCommand) runDockerCompatInspect(cmdName string, args []string) error { + if err := nc.assertVMIsRunning(nc.ncc, nc.logger); err != nil { + return err + } - if !hasCmdHandler && !hasArgHandler && len(args) > 0 { - // for commands like image build, container run - key := fmt.Sprintf("%s %s", cmdName, args[0]) - cmdHandler, hasCmdHandler = aggCmdHandlerMap[key] - aMap, hasArgHandler = aggArgHandlerMap[key] - } + envSet := nc.initializeEnvSet() + args, err := nc.processArgs(args, envSet) + if err != nil { + return err } - // First check if the command has command handler - if hasCmdHandler { - err := cmdHandler(nc.systemDeps, nc.fc, &cmdName, &args, &inspectType) - if err != nil { - return err - } + envArgs, err := nc.getEnvArgs(envSet) + if err != nil { + return err } - switch cmdName { - case "container run", "exec", "compose": - // check if an option flag is present; immediately following the command - switch { - case args[0] == "run" && strings.HasPrefix(args[1], "-"): - firstOptPos = 1 - case strings.HasPrefix(args[0], "-"): - firstOptPos = 0 - default: - firstOptPos = -1 - } - // lastOpt is used to track when no more flags need to be processed - lastOpt = false + remoteCredentialEnv := nc.getRemoteCredentialEnv() + limaArgs := append(nc.GetCmdArgs(), append(remoteCredentialEnv, envArgs...)...) + runArgs := append(limaArgs, append([]string{nerdctlCmdName}, strings.Fields(cmdName)...)...) + runArgs = append(runArgs, args...) - shortFlagBoolSet := cmdFlagSetMap[cmdName]["shortBoolFlags"] - longFlagBoolSet := cmdFlagSetMap[cmdName]["longBoolFlags"] + if nc.shouldReplaceForHelp(cmdName, args) { + return nc.ncc.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, runArgs...) + } - shortFlagArgSet := cmdFlagSetMap[cmdName]["shortArgFlags"] + if !slices.Contains(runArgs, "--format") { + runArgs = append(runArgs, "--format", "{{json .}}") + } - for i, arg := range args { - // Check if individual argument (and possibly following value) requires manipulation-in-place handling - if hasArgHandler { - // Check if argument for the command needs handling, sometimes it can be --file= - b, _, _ := strings.Cut(arg, "=") - h, ok := aMap[b] - if ok { - err = h(nc.systemDeps, nc.fc, args, i) - if err != nil { - return err - } - // This is required when the positional argument at i is mutated by argHandler, eg -v=C:\Users:/tmp:ro - arg = args[i] - } - } + cmd := nc.ncc.Create(runArgs...) + return inspectContainerOutputHandler(cmd) +} - // parsing arguments from the command line - // may pre-fetch and consume the next argument; - // the loop variable will skip any pre-consumed args - if skip { - skip = false - continue - } +func (nc *nerdctlCommand) initializeEnvSet() *orderedmap.OrderedMap { + passedEnvs := []string{ + "COSIGN_PASSWORD", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", "COMPOSE_FILE", "SOURCE_DATE_EPOCH", + "AWS_ECR_DISABLE_CACHE", "AWS_ECR_CACHE_DIR", "AWS_ECR_IGNORE_CREDS_STORAGE", + } + envSet := orderedmap.New() + for _, env := range passedEnvs { + envSet.Set(env, true) + } + return envSet +} - // after last Option position is found, pass through each arg as an internal command argument (short-circuit) - if lastOpt { - cmdArgs = append(cmdArgs, arg) - continue - } - switch { - case arg == "--debug": - nc.logger.SetLevel(flog.Debug) - case arg == "--help": - nerdctlArgs = append(nerdctlArgs, arg) - case arg == "--add-host": - // exact match to --add-host - args[i+1], err = resolveIP(args[i+1], nc.logger, nc.ecc) +func (nc *nerdctlCommand) processArgs(args []string, envSet *orderedmap.OrderedMap) ([]string, error) { + var processedArgs []string + for i := 0; i < len(args); i++ { + arg := args[i] + if runtime.GOOS == "windows" { + args[i] = nc.handleWindowsPaths(args[i]) + } + switch { + case arg == "--debug": + nc.logger.SetLevel(flog.Debug) + continue + case strings.HasPrefix(arg, "--add-host"): + if arg == "--add-host" && i+1 < len(args) { + resolvedIP, err := resolveIP(args[i+1], nc.logger, nc.ecc) if err != nil { - return err + return nil, err } - nerdctlArgs = append(nerdctlArgs, arg) - case strings.HasPrefix(arg, "--add-host"): - // arg begins with --add-host + args[i+1] = resolvedIP + } else { resolvedIP, err := resolveIP(arg[11:], nc.logger, nc.ecc) if err != nil { - return err - } - arg = fmt.Sprintf("%s%s", arg[0:11], resolvedIP) - nerdctlArgs = append(nerdctlArgs, arg) - case strings.HasPrefix(arg, "--env-file"): - // exact match to --env-file - // or arg begins with --env-file - shouldSkip, addEnvs, err := handleEnvFile(nc.fs, nc.systemDeps, arg, args[i+1]) - if err != nil { - return err - } - skip = shouldSkip - fileEnvs = append(fileEnvs, addEnvs...) - case argIsEnv(arg): - // exact match to either -e or --env - // or arg begins with -e or --env - // -e="", -e"" - // --env="=", --env"=" - shouldSkip, addEnv := handleEnv(nc.systemDeps, arg, args[i+1]) - skip = shouldSkip - if addEnv != "" { - envs = append(envs, addEnv) - } - case shortFlagBoolSet.Has(arg) || longFlagBoolSet.Has(arg): - // exact match to a short no argument flag: -? - // or exact match to: -- - nerdctlArgs = append(nerdctlArgs, arg) - case longFlagBoolSet.Has(strings.Split(arg, "=")[0]): - // begins with -- - // e.g. --sig-proxy=false - nerdctlArgs = append(nerdctlArgs, arg) - case shortFlagBoolSet.Has(arg[:2]): - // or begins with a defined short no argument flag, but is adjacent to something - // -???? one or more short bool flags; no following values - // -????="" one or more short bool flags ending with a short arg flag equated to value - // -????"" one or more short bool flags ending with a short arg flag concatenated to value - addArg := nc.handleMultipleShortFlags(shortFlagBoolSet, shortFlagArgSet, args, i) - nerdctlArgs = append(nerdctlArgs, addArg) - case shortFlagArgSet.Has(arg) || shortFlagArgSet.Has(arg[:2]): - // exact match to a short arg flag: -? - // next arg must be the - // or begins with a short arg flag: - // short arg flag concatenated to value: -?"" - // short arg flag equated to value: -?="" or -?= - shouldSkip, addKey, addVal := nc.handleFlagArg(arg, args[i+1]) - skip = shouldSkip - if addKey != "" { - nerdctlArgs = append(nerdctlArgs, addKey) - nerdctlArgs = append(nerdctlArgs, addVal) - } - case strings.HasPrefix(arg, "--"): - // exact match to a long arg flag: - - // next arg must be the - // or begins with a long arg flag: - // long arg flag concatenated to value: --"" - // long arg flag equated to value: --="" or --= - shouldSkip, addKey, addVal := nc.handleFlagArg(arg, args[i+1]) - skip = shouldSkip - if addKey != "" { - nerdctlArgs = append(nerdctlArgs, addKey) - nerdctlArgs = append(nerdctlArgs, addVal) - } - default: - // arg other than a flag ("-?","--") or a skipped - switch { - case (i < firstOptPos): - // arg is a value prior to the first found flag - // pass the arg as a nerdctl command argument - nerdctlArgs = append(nerdctlArgs, arg) - case (i >= firstOptPos) && !lastOpt: - // The first arg after procecssing the flags establishes the last Option Pos - lastOpt = true - cmdArgs = append(cmdArgs, arg) - default: - // Unexpected case - // pass the arg as a nerdctl arg by default - nc.logger.Debugln("Unexpected Arg Value: ", 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 := orderedmap.New() - - for _, e := range fileEnvs { - evar, eval, _ := strings.Cut(e, "=") - envVars.Set(evar, eval) - } - for _, e := range envs { - evar, eval, _ := strings.Cut(e, "=") - envVars.Set(evar, eval) - } - - var envArgs []string - for pair := envVars.Oldest(); pair != nil; pair = pair.Next() { - envArgs = append(envArgs, "-e", fmt.Sprintf("%s=%s", pair.Key, pair.Value)) - } - - nerdctlArgs = append(nerdctlArgs, envArgs...) - nerdctlArgs = append(nerdctlArgs, cmdArgs...) - default: - - for i, arg := range args { - // Check if individual argument (and possibly following value) requires manipulation-in-place handling - if hasArgHandler { - // Check if argument for the command needs handling, sometimes it can be --file= - b, _, _ := strings.Cut(arg, "=") - h, ok := aMap[b] - if ok { - err = h(nc.systemDeps, nc.fc, args, i) - if err != nil { - return err - } - // This is required when the positional argument at i is mutated by argHandler, eg -v=C:\Users:/tmp:ro - arg = args[i] + return nil, err } + args[i] = fmt.Sprintf("--add-host=%s", resolvedIP) } - - switch { - case arg == "--debug": - nc.logger.SetLevel(flog.Debug) - case arg == "--help": - nerdctlArgs = append(nerdctlArgs, arg) - default: - nerdctlArgs = append(nerdctlArgs, arg) + case strings.HasPrefix(arg, "--env-file"): + if err := nc.handleEnvFileArg(arg, args, i, envSet); err != nil { + continue } + case strings.HasPrefix(arg, "--env"), strings.HasPrefix(arg, "-e"): + nc.handleEnvArg(arg, args, i, envSet) } + processedArgs = append(processedArgs, args[i]) } - passedEnvs := []string{ - "COSIGN_PASSWORD", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", - "AWS_SESSION_TOKEN", "COMPOSE_FILE", "SOURCE_DATE_EPOCH", - "AWS_ECR_DISABLE_CACHE", "AWS_ECR_CACHE_DIR", "AWS_ECR_IGNORE_CREDS_STORAGE", - } + return processedArgs, nil +} +func (nc *nerdctlCommand) getEnvArgs(envSet *orderedmap.OrderedMap) ([]string, error) { var passedEnvArgs []string - for _, e := range passedEnvs { - v, b := nc.systemDeps.LookupEnv(e) - if !b { + for pair := envSet.Oldest(); pair != nil; pair = pair.Next() { + value, exists := nc.systemDeps.LookupEnv(pair.Key.(string)) + if !exists { continue } - if runtime.GOOS == "windows" && e == "COMPOSE_FILE" { - wslPath, err := convertToWSLPath(nc.systemDeps, v) + if runtime.GOOS == "windows" && pair.Key.(string) == "COMPOSE_FILE" { + wslPath, err := convertToWSLPath(nc.systemDeps, value) if err != nil { - return err + return nil, err } - v = wslPath + value = wslPath } - passedEnvArgs = append(passedEnvArgs, fmt.Sprintf("%s=%s", e, v)) + passedEnvArgs = append(passedEnvArgs, fmt.Sprintf("%s=%s", pair.Key.(string), value)) } + return passedEnvArgs, nil +} +func (nc *nerdctlCommand) getRemoteCredentialEnv() []string { var additionalEnv []string - switch cmdName { - case "image": - if slices.Contains(args, "build") || slices.Contains(args, "pull") || slices.Contains(args, "push") { - ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) - } - case "container": - if slices.Contains(args, "run") { - ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) - } - case "build", "pull", "push", "container run": - 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(nc.GetCmdArgs(), append(additionalEnv, passedEnvArgs...)...) - - limaArgs = append(limaArgs, append([]string{nerdctlCmdName}, strings.Fields(cmdName)...)...) + ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) + return additionalEnv +} - // 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 - runArgs = append(runArgs, limaArgs...) - runArgs = append(runArgs, nerdctlArgs...) +func (nc *nerdctlCommand) handleEnvArg(arg string, args []string, i int, envSet *orderedmap.OrderedMap) { + // This is an un-necessary operation that happens due to parsing of any -e/--env/--env-file for exec commands of run and exec + // as part of lookup or processing of env file. + var envVar string + if strings.HasPrefix(arg, "--env=") { + envVar = arg[6:] + } else if arg == "--env" && i+1 < len(args) { + envVar = args[i+1] + } else if strings.HasPrefix(arg, "-e=") { + envVar = arg[3:] + } else if arg == "-e" && i+1 < len(args) { + envVar = args[i+1] + } - if nc.shouldReplaceForHelp(cmdName, args) { - return nc.ncc.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, runArgs...) + if strings.Contains(envVar, "=") { + return } + _, exists := envSet.Get(envVar) + if envVar != "" && !exists { + envSet.Set(envVar, true) + } +} - if inspectType == "container" && nc.fc.DockerCompat && !slices.Contains(runArgs, "--format") { - runArgs = append(runArgs, "--format", "{{json .}}") - cmd := nc.ncc.Create(runArgs...) - return inspectContainerOutputHandler(cmd) +func (nc *nerdctlCommand) handleEnvFileArg(arg string, args []string, i int, envSet *orderedmap.OrderedMap) error { + var envFile string + if strings.HasPrefix(arg, "--env-file=") { + envFile = arg[11:] + } else if arg == "--env-file" && i+1 < len(args) { + envFile = args[i+1] } - if err := handleDockerCompatComposeVersion(cmdName, *nc, cmdArgs); err == nil { - return nil + if envFile != "" { + unassignedVars, err := nc.processEnvFile(envFile, envSet) + if err != nil { + nc.logger.Warnf("Error reading env file: %v", err) + return nil + } + for _, envVar := range unassignedVars { + envSet.Set(envVar, true) + } } + return nil +} - return nc.ncc.Create(runArgs...).Run() +func (nc *nerdctlCommand) handleWindowsPaths(arg string) string { + re := regexp.MustCompile(`(?:^|[,:=])([a-zA-Z]:[\\/][^ ,:]*)`) + return re.ReplaceAllStringFunc(arg, func(match string) string { + separator := "" + if match[0] == ',' || match[0] == ':' || match[0] == '=' { + separator = string(match[0]) + match = match[1:] + } + wslPath, _ := convertToWSLPath(nc.systemDeps, match) + return separator + wslPath + }) } func (nc *nerdctlCommand) assertVMIsRunning(creator command.NerdctlCmdCreator, logger flog.Logger) error { @@ -370,176 +237,34 @@ func (nc *nerdctlCommand) assertVMIsRunning(creator command.NerdctlCmdCreator, l } } -func argIsEnv(arg string) bool { - return strings.HasPrefix(arg, "-e") || (strings.HasPrefix(arg, "--env") && !strings.HasPrefix(arg, "--env-file")) -} - -func (nc *nerdctlCommand) handleMultipleShortFlags( - shortFlagBoolSet sets.Set[string], - shortFlagArgSet sets.Set[string], - args []string, - index int, -) string { - arg := args[index] - nextArg := args[index+1] - - argFlagFinal := false - argFlagMid := false - lastPos := len(arg) - 1 - - // Scan the compound shortFlag arg for details - for i, c := range strings.Split(arg, "") { - switch { - case c == "-": - continue - case i != lastPos && shortFlagArgSet.Has("-"+c): - argFlagMid = true - case i == lastPos && shortFlagArgSet.Has("-"+c): - argFlagFinal = true - default: - if !shortFlagBoolSet.Has("-" + c) { - nc.logger.Debugf("The group of short flags contains an unexpected value: %s, %s", arg, c) - } - } - } - - switch { - case argFlagMid: - // if a flag in the middle requires arguments, - // but there are more short flags immediately following - // then include a finch debug comment - // and pass the concatenated arg to nerdctl - nc.logger.Debugln("Mid Position Short Flag requires an Arg: ", arg) - case argFlagFinal: - // pre-pend the shortFlagArg member to the next Arg - separatedShortFlag := "-" + arg[lastPos:] - args[index+1] = separatedShortFlag + nextArg - // remove the shortFlagArg member from the current Arg - args[index] = arg[:lastPos] - arg = args[index] - default: - // if every following character is a shortFlagBool - // then pass into nerdctlArgs - } - - return arg -} - -func (nc *nerdctlCommand) handleFlagArg(arg string, nextArg string) (bool, string, string) { - // handling Flag arguments other than environment variables - // note: a Bool Flag should not be passed into this helper function; only Flags that are followed by one argument - var ( - flagKey, flagVal string - skip bool - ) - switch { - case strings.HasPrefix(arg, "--") && strings.Contains(arg, "="): - // long flag concatenated to value by '=': --="" - skip = false - flagKey, flagVal, _ = strings.Cut(arg, "=") - case strings.HasPrefix(arg, "--") && !strings.HasPrefix(nextArg, "-"): - // long flag followed by a value: -- "" - skip = true - flagKey = arg - flagVal = nextArg - // undefined case: long flag adjacent to value: --"" or -- - case strings.HasPrefix(arg, "-") && strings.Contains(arg, "="): - // short flag concatenated to value by '=': -?="" or -?= - skip = false - flagKey, flagVal, _ = strings.Cut(arg, "=") - case strings.HasPrefix(arg, "-") && len(arg) > 2: - // short flag adjacent to value: -?"" or -? - skip = false - flagKey = arg[:2] - flagVal = arg[2:] - case strings.HasPrefix(arg, "-") && len(arg) == 2 && !strings.HasPrefix(nextArg, "-"): - // short flag followed by a value: -? "" or -? - skip = true - flagKey = arg - flagVal = nextArg - default: - return false, "", "" - } - - return skip, flagKey, flagVal -} - -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 - eqPos := strings.Index(arg, "=") - envVar = arg[eqPos+1:] - } - } - - 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(fs afero.Fs, 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 := fs.Open(filepath.Clean(filename)) +func (nc *nerdctlCommand) processEnvFile(envFile string, existingVars *orderedmap.OrderedMap) ([]string, error) { + file, err := nc.fs.Open(filepath.Clean(envFile)) if err != nil { - return false, []string{}, err + nc.logger.Warnf("Error opening env file: %v", err) + return nil, nil } defer file.Close() //nolint:errcheck // We did not write to the file, and the file will be closed when the CLI process exits anyway. scanner := bufio.NewScanner(file) - var envs []string + var unassignedVars []string for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if len(line) == 0 { continue } + _, exists := existingVars.Get(line) 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) + case !strings.Contains(line, "=") && !exists: + unassignedVars = append(unassignedVars, line) } } if err := scanner.Err(); err != nil { - return skip, []string{}, err + nc.logger.Warnf("Error reading env file: %v", err) + return []string{}, nil } - return skip, envs, nil + return unassignedVars, nil } diff --git a/cmd/finch/nerdctl_windows.go b/cmd/finch/nerdctl_windows.go index 6a594dc41..62a967fe7 100644 --- a/cmd/finch/nerdctl_windows.go +++ b/cmd/finch/nerdctl_windows.go @@ -12,49 +12,9 @@ import ( dockerops "github.com/docker/docker/opts" "github.com/runfinch/finch/pkg/command" - "github.com/runfinch/finch/pkg/config" "github.com/runfinch/finch/pkg/flog" ) -var osAliasMap = map[string]string{ - "save": "image save", - "load": "image load", -} - -var osCommandHandlerMap = map[string]commandHandler{ - "container cp": cpHandler, - "image build": imageBuildHandler, -} - -var osArgHandlerMap = map[string]map[string]argHandler{ - "image build": { - "-f": handleFilePath, - "--file": handleFilePath, - "--iidfile": handleFilePath, - "-o": handleOutputOption, - "--output": handleOutputOption, - "--secret": handleSecretOption, - }, - "image save": { - "-o": handleFilePath, - "--output": handleFilePath, - }, - "image load": { - "-i": handleFilePath, - "--input": handleFilePath, - }, - "container run": { - "--label-file": handleFilePath, - "--cosign-key": handleFilePath, - "--cidfile": handleFilePath, - "-v": handleVolume, - "--volume": handleVolume, - }, - "compose": { - "--file": handleFilePath, - }, -} - func (nc *nerdctlCommand) GetCmdArgs() []string { wd, err := nc.systemDeps.GetWd() if err != nil { @@ -85,261 +45,6 @@ func convertToWSLPath(systemDeps NerdctlCommandSystemDeps, winPath string) (stri return path, nil } -// substitutes wsl path for the provided option in place for nerdctl args. -func handleFilePath(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { - prefix := nerdctlCmdArgs[index] - - // If --filename=" then we need to cut and convert that to wsl path - if strings.Contains(nerdctlCmdArgs[index], "=") { - before, after, _ := strings.Cut(prefix, "=") - wslPath, err := convertToWSLPath(systemDeps, after) - if err != nil { - return err - } - nerdctlCmdArgs[index] = fmt.Sprintf("%s=%s", before, wslPath) - } else { - if (index + 1) < len(nerdctlCmdArgs) { - wslPath, err := convertToWSLPath(systemDeps, nerdctlCmdArgs[index+1]) - if err != nil { - return err - } - nerdctlCmdArgs[index+1] = wslPath - } else { - return fmt.Errorf("invalid positional parameter for %s", prefix) - } - } - return nil -} - -// hanldes -v/--volumes option. For anonymous volumes and named volumes this is no-op. For bind mounts path is converted to wsl path. -func handleVolume(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { - prefix := nerdctlCmdArgs[index] - var ( - v string - found bool - before string - ) - if strings.Contains(nerdctlCmdArgs[index], "=") { - before, v, found = strings.Cut(prefix, "=") - } else { - if (index + 1) < len(nerdctlCmdArgs) { - v = nerdctlCmdArgs[index+1] - } else { - return fmt.Errorf("invalid positional parameter for %s", prefix) - } - } - cleanArg := v - readWrite := "" - if strings.HasSuffix(v, ":ro") || strings.HasSuffix(v, ":rw") { - readWrite = v[len(v)-3:] - cleanArg = v[:len(v)-3] - } else if strings.HasSuffix(v, ":rro") { - readWrite = v[len(v)-4:] - cleanArg = v[:len(v)-4] - } - - colonIndex := strings.LastIndex(cleanArg, ":") - if colonIndex < 0 { - return nil - } - hostPath := cleanArg[:colonIndex] - // This is a named volume, or an anonymous volume from https://github.com/containerd/nerdctl/blob/main/pkg/mountutil/mountutil.go#L76 - if !strings.Contains(hostPath, "\\") || len(hostPath) == 0 { - return nil - } - - hostPath, err := systemDeps.FilePathAbs(hostPath) - // If it's an anonymous volume, then the path won't exist - if err != nil { - return err - } - - containerPath := cleanArg[colonIndex+1:] - wslHostPath, err := convertToWSLPath(systemDeps, hostPath) - if err != nil { - return fmt.Errorf("could not get volume host path for %s: %w", v, err) - } - - if found { - nerdctlCmdArgs[index] = fmt.Sprintf("%s=%s:%s%s", before, wslHostPath, containerPath, readWrite) - } else { - nerdctlCmdArgs[index+1] = fmt.Sprintf("%s:%s%s", wslHostPath, containerPath, readWrite) - } - return nil -} - -// handles --output/-o for build command. -func handleOutputOption(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { - prefix := nerdctlCmdArgs[index] - var ( - v string - found bool - before string - ) - if strings.Contains(nerdctlCmdArgs[index], "=") { - before, v, found = strings.Cut(prefix, "=") - } else { - if (index + 1) < len(nerdctlCmdArgs) { - v = nerdctlCmdArgs[index+1] - } else { - return fmt.Errorf("invalid positional parameter for %s", prefix) - } - } - - // https://docs.docker.com/engine/reference/commandline/build/ order does not matter, so convert to a map - entries := strings.Split(v, ",") - m := make(map[string]string) - for _, e := range entries { - parts := strings.Split(e, "=") - m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - dest, ok := m["dest"] - if !ok { - return nil - } - wslPath, err := convertToWSLPath(systemDeps, dest) - if err != nil { - return err - } - m["dest"] = wslPath - - // Convert to string representation - s := mapToString(m) - - if found { - nerdctlCmdArgs[index] = fmt.Sprintf("%s=%s", before, s) - } else { - nerdctlCmdArgs[index+1] = s - } - - return nil -} - -// handles --secret option for build command. -func handleSecretOption(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { - prefix := nerdctlCmdArgs[index] - var ( - v string - found bool - before string - ) - if strings.Contains(nerdctlCmdArgs[index], "=") { - before, v, found = strings.Cut(prefix, "=") - } else { - if (index + 1) < len(nerdctlCmdArgs) { - v = nerdctlCmdArgs[index+1] - } else { - return fmt.Errorf("invalid positional parameter for %s", prefix) - } - } - - entries := strings.Split(v, ",") - m := make(map[string]string) - for _, e := range entries { - parts := strings.Split(e, "=") - m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - sp, ok := m["src"] - if !ok { - return nil - } - wslPath, err := convertToWSLPath(systemDeps, sp) - if err != nil { - return err - } - m["src"] = wslPath - - // Convert to string representation - s := mapToString(m) - - if found { - nerdctlCmdArgs[index] = fmt.Sprintf("%s=%s", before, s) - } else { - nerdctlCmdArgs[index+1] = s - } - - return nil -} - -// cp command handler, takes command arguments and converts hostpath to wsl path in place. It ignores all other arguments. -func cpHandler(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, _ *string, nerdctlCmdArgs *[]string, _ *string) error { - for i, arg := range *nerdctlCmdArgs { - // -L and --follow-symlink don't have to be processed - if strings.HasPrefix(arg, "-") || arg == "cp" { - continue - } - // If argument contains container path, then continue - colon := strings.Index(arg, ":") - - // this is a container path - if colon > 1 { - continue - } - wslPath, err := convertToWSLPath(systemDeps, arg) - if err != nil { - return err - } - (*nerdctlCmdArgs)[i] = wslPath - } - return nil -} - -// this is the handler for image build command. It translates build context to wsl path. -func imageBuildHandler(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, _ *string, nerdctlCmdArgs *[]string, _ *string) error { - var err error - argLen := len(*nerdctlCmdArgs) - 1 - // -h/--help don't have buildcontext, just return - for _, a := range *nerdctlCmdArgs { - if a == "--help" || a == "-h" { - return nil - } - } - if (*nerdctlCmdArgs)[argLen] != "--debug" { - (*nerdctlCmdArgs)[argLen], err = convertToWSLPath(systemDeps, (*nerdctlCmdArgs)[argLen]) - if err != nil { - return err - } - } else { - (*nerdctlCmdArgs)[argLen-1], err = convertToWSLPath(systemDeps, (*nerdctlCmdArgs)[argLen-1]) - if err != nil { - return err - } - } - return nil -} - -func handleBindMountPath(systemDeps NerdctlCommandSystemDeps, m map[string]string) error { - // Handle src/source path - var k string - path, ok := m["src"] - if !ok { - path, ok = m["source"] - k = "source" - } else { - k = "src" - } - // If there is no src or source or not a windows path, do nothing, let nerdctl handle error - if !ok || !strings.Contains(path, `\`) { - return nil - } - wslPath, err := convertToWSLPath(systemDeps, path) - if err != nil { - return err - } - - m[k] = wslPath - return nil -} - -func mapToString(m map[string]string) string { - var parts []string - for k, v := range m { - part := fmt.Sprintf("%s=%s", k, v) - parts = append(parts, part) - } - return strings.Join(parts, ",") -} - func resolveIP(host string, logger flog.Logger, ecc command.Creator) (string, error) { parts := strings.SplitN(host, ":", 2) // If the IP Address is a string called "host-gateway", replace this value with the IP address that can be used to diff --git a/go.mod b/go.mod index 85b6fcae6..ceab069d5 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 github.com/tc-hib/go-winres v0.3.3 + github.com/wk8/go-ordered-map v1.0.0 github.com/xorcare/pointer v1.2.2 golang.org/x/crypto v0.29.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 @@ -148,7 +149,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/wk8/go-ordered-map v1.0.0 go.opencensus.io v0.24.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect