From 8cf44fc31d703b4b78c96af321cbfa2a0ed4a24f Mon Sep 17 00:00:00 2001 From: Shubharanshu Mahapatra Date: Fri, 29 Nov 2024 05:41:41 +0530 Subject: [PATCH] feat: refactor cli parsing Signed-off-by: Shubharanshu Mahapatra --- cmd/finch/devcontainer_patch.go | 18 +- cmd/finch/main.go | 28 +- cmd/finch/nerdctl.go | 392 ++++++++++------------ cmd/finch/nerdctl_darwin.go | 20 -- cmd/finch/nerdctl_native.go | 104 +----- cmd/finch/nerdctl_remote.go | 575 ++++++++------------------------ cmd/finch/nerdctl_windows.go | 295 ---------------- go.mod | 1 - go.sum | 3 - test.env | 19 ++ 10 files changed, 386 insertions(+), 1069 deletions(-) create mode 100644 test.env 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..8ceddf7da 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -42,13 +42,29 @@ 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 "volume": + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.createDockerCompatVolumeCmd()) + 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..be3bc0779 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,175 @@ 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() == "build" { + 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 { + 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) createDockerCompatVolumeCmd() *cobra.Command { + volumeCmd := &cobra.Command{ + Use: "volume", + Short: "Manage Volumes", + 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) + }, + } + return volumeCmd +} + +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 { + const modeDockerCompat = `--mode=dockercompat` + var inspectType string + inspectType = "" + 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" + continue + } + } + + switch inspectType { + case "image": + return ncc.createDockerCompatImageCmd().RunE(cmd, append([]string{"inspect", modeDockerCompat}, args...)) + case "volume": + return ncc.createDockerCompatVolumeCmd().RunE(cmd, append([]string{"inspect"}, args...)) + default: + return newNerdctlCommand(ncc.ncc, ncc.ecc, ncc.systemDeps, ncc.logger, ncc.fs, ncc.fc).runDockerCompatInspectAdapter(cmd, + append([]string{modeDockerCompat}, args...)) + } + }, + } + return inspectCmd +} + type nerdctlCommand struct { ncc command.NerdctlCmdCreator ecc command.Creator @@ -98,6 +263,14 @@ 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) 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 +372,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_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..2f24e0ea5 100644 --- a/cmd/finch/nerdctl_remote.go +++ b/cmd/finch/nerdctl_remote.go @@ -8,16 +8,12 @@ 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 +23,185 @@ 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() map[string]bool { + 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 := make(map[string]bool) + for _, env := range passedEnvs { + envSet[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 map[string]bool) ([]string, error) { + 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) + 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) + return nil, err } + args[i] = fmt.Sprintf("--add-host=%s", resolvedIP) } - } - - // 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] - } - } - - 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 { + return nil, err } + case strings.HasPrefix(arg, "--env"), strings.HasPrefix(arg, "-e"): + nc.handleEnvArg(arg, args, i, envSet) } } - 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 args, nil +} +func (nc *nerdctlCommand) getEnvArgs(envSet map[string]bool) ([]string, error) { var passedEnvArgs []string - for _, e := range passedEnvs { - v, b := nc.systemDeps.LookupEnv(e) - if !b { + for envVar := range envSet { + value, exists := nc.systemDeps.LookupEnv(envVar) + if !exists { continue } - if runtime.GOOS == "windows" && e == "COMPOSE_FILE" { - wslPath, err := convertToWSLPath(nc.systemDeps, v) + if runtime.GOOS == "windows" && envVar == "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", envVar, 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 map[string]bool) { + 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 envVar != "" && !envSet[envVar] { + envSet[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 map[string]bool) 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) + } + for _, envVar := range unassignedVars { + envSet[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,155 +226,16 @@ 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 map[string]bool) ([]string, error) { + file, err := nc.fs.Open(filepath.Clean(envFile)) if err != nil { - return false, []string{}, err + return nil, err } 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 { @@ -528,18 +245,12 @@ func handleEnvFile(fs afero.Fs, systemDeps NerdctlCommandSystemDeps, arg, arg2 s 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, "=") && !existingVars[line]: + unassignedVars = append(unassignedVars, line) } } if err := scanner.Err(); err != nil { - return skip, []string{}, err + return []string{}, err } - 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..6140f7b52 100644 --- a/go.mod +++ b/go.mod @@ -148,7 +148,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 diff --git a/go.sum b/go.sum index 556ee885d..c9752817c 100644 --- a/go.sum +++ b/go.sum @@ -306,7 +306,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -328,8 +327,6 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8= -github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/test.env b/test.env new file mode 100644 index 000000000..fbe47fb31 --- /dev/null +++ b/test.env @@ -0,0 +1,19 @@ +# Sample .env file for testing +APP_NAME=TestApp +APP_ENV=development +APP_PORT=8080 + +# Database configurations +DB_HOST=localhost +DB_PORT=5432 +DB_USER=testuser +DB_PASSWORD=secret + +# Empty or commented variables +# DB_NAME= +#API_KEY= + +# Special cases +SPECIAL_VAR=ValueWith=EqualsSign +UNASSIGNED_VAR +