From 0c365e434611461dea9825e14c4fa11e7d2a56b7 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/nerdctl.go | 97 ------- cmd/finch/nerdctl_darwin.go | 14 - cmd/finch/nerdctl_native.go | 45 +-- cmd/finch/nerdctl_remote.go | 517 ++++++++--------------------------- cmd/finch/nerdctl_windows.go | 295 -------------------- go.mod | 1 - go.sum | 3 - 7 files changed, 115 insertions(+), 857 deletions(-) diff --git a/cmd/finch/nerdctl.go b/cmd/finch/nerdctl.go index 642ff30e2..bb8efaf23 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -204,12 +204,6 @@ 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, @@ -219,33 +213,6 @@ 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". @@ -344,67 +311,3 @@ func handleDockerCompatInspect(_ NerdctlCommandSystemDeps, fc *config.Finch, cmd 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..0b6648256 100644 --- a/cmd/finch/nerdctl_darwin.go +++ b/cmd/finch/nerdctl_darwin.go @@ -44,17 +44,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..4e63b2a69 100644 --- a/cmd/finch/nerdctl_native.go +++ b/cmd/finch/nerdctl_native.go @@ -17,11 +17,7 @@ import ( func (nc *nerdctlCommand) run(cmdName string, args []string) error { var ( - hasCmdHandler, hasArgHandler bool - cmdHandler commandHandler - aMap map[string]argHandler - err error - inspectType string + inspectType string ) // eat the debug arg, and set the log level to avoid nerdctl parsing this flag @@ -31,45 +27,6 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { 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:]...) diff --git a/cmd/finch/nerdctl_remote.go b/cmd/finch/nerdctl_remote.go index 6f679e152..785f0d92e 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" + "os" + "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" @@ -32,266 +28,117 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { return err } var ( - nerdctlArgs, envs, fileEnvs, cmdArgs, runArgs []string - skip, hasCmdHandler, hasArgHandler, lastOpt bool - cmdHandler commandHandler - aMap map[string]argHandler - firstOptPos int - inspectType string + nerdctlArgs, cmdArgs, runArgs []string + 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) - - aggArgHandlerMap := make(map[string]map[string]argHandler) - for k := range argHandlerMap { - aggArgHandlerMap[k] = make(map[string]argHandler) - maps.Copy(aggArgHandlerMap[k], argHandlerMap[k]) - } - for k := range osArgHandlerMap { - if _, ok := aggArgHandlerMap[k]; !ok { - aggArgHandlerMap[k] = make(map[string]argHandler) - } - maps.Copy(aggArgHandlerMap[k], osArgHandlerMap[k]) + 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", } - 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] - - 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 := make(map[string]bool) + for _, env := range passedEnvs { + envSet[env] = true } - // First check if the command has command handler - if hasCmdHandler { - err := cmdHandler(nc.systemDeps, nc.fc, &cmdName, &args, &inspectType) - if err != nil { - return err - } - } + for i := 0; i < len(args); i++ { + arg := args[i] - 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 + if arg == "--debug" { + nc.logger.SetLevel(flog.Debug) } - // lastOpt is used to track when no more flags need to be processed - lastOpt = false - - shortFlagBoolSet := cmdFlagSetMap[cmdName]["shortBoolFlags"] - longFlagBoolSet := cmdFlagSetMap[cmdName]["longBoolFlags"] - - shortFlagArgSet := cmdFlagSetMap[cmdName]["shortArgFlags"] - - 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] - } - } - - // 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 - } - // 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) + if strings.HasPrefix(arg, "--add-host") { + if arg == "--add-host" && i+1 < len(args) { + // Handle `--add-host` with a separate value + resolvedIP, err := resolveIP(args[i+1], nc.logger, nc.ecc) if err != nil { return err } + args[i+1] = resolvedIP nerdctlArgs = append(nerdctlArgs, arg) - case strings.HasPrefix(arg, "--add-host"): - // arg begins with --add-host + } else { + // Handle `--add-host=` 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 + arg = fmt.Sprintf("--add-host=%s", resolvedIP) 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) - } } + continue } - // 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() + // Handle environment variables + if strings.HasPrefix(arg, "--env") || strings.HasPrefix(arg, "-e") { + var envVar string + if strings.HasPrefix(arg, "--env=") { + // Handle `--env=KEY` + envVar = arg[6:] + } else if arg == "--env" && i+1 < len(args) { + // Handle `--env KEY` with a separate value + envVar = args[i+1] + } else if strings.HasPrefix(arg, "-e=") { + // Handle `-e=KEY` + envVar = arg[3:] + } else if arg == "-e" && i+1 < len(args) { + // Handle `-e KEY` with a separate value + envVar = args[i+1] + } - 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) - } + if envVar != "" && !envSet[envVar] { + passedEnvs = append(passedEnvs, envVar) + envSet[envVar] = true + } - 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: + if strings.HasPrefix(arg, "--env-file") { + // Handle --env-file= or --env-file + 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] + } - 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] + if envFile != "" { + unassignedVars, err := processEnvFile(envFile, envSet) + if err != nil { + fmt.Printf("Error reading env file: %v\n", err) + } + for _, envVar := range unassignedVars { + passedEnvs = append(passedEnvs, envVar) + envSet[envVar] = true } } + } - switch { - case arg == "--debug": - nc.logger.SetLevel(flog.Debug) - case arg == "--help": - nerdctlArgs = append(nerdctlArgs, arg) - default: - nerdctlArgs = append(nerdctlArgs, arg) - } + if runtime.GOOS == "windows" { + + re := regexp.MustCompile(`[ ,:=]([a-zA-Z]:[\\/][^ ,:]*)`) + + // Replace function to modify the matched path (customize as needed) + arg = re.ReplaceAllStringFunc(arg, func(match string) string { + separator := match[0] + match = match[1:] + wslPath, _ := convertToWSLPath(nc.systemDeps, match) + nc.logger.Infof("wsl path %s", match) + wslPath = string(separator) + wslPath + return wslPath + }) + + nerdctlArgs = append(nerdctlArgs, arg) + continue } - } - 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", + nerdctlArgs = append(nerdctlArgs, arg) } var passedEnvArgs []string - for _, e := range passedEnvs { + for e, _ := range envSet { v, b := nc.systemDeps.LookupEnv(e) if !b { continue @@ -307,18 +154,7 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { } 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) - } + 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 @@ -345,9 +181,23 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { return nil } + nc.logger.Infoln("runArgs %v", runArgs) + return nc.ncc.Create(runArgs...).Run() } +func (nc *nerdctlCommand) isWindowsPath(path string) bool { + // Check if the path starts with a drive letter (e.g., "C:\") + if len(path) > 1 && path[1] == ':' && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) { + return true + } + // Check if it contains backslashes, which are common in Windows paths + if strings.Contains(path, "\\") { + return true + } + return false +} + func (nc *nerdctlCommand) assertVMIsRunning(creator command.NerdctlCmdCreator, logger flog.Logger) error { // Extra call to check VM before running nerdctl commands. These are the reasons of not doing message replacing // 1. for the non-help commands, replacing stdout may cause "stdin is not a terminal" error for the commands that need input. @@ -370,176 +220,37 @@ 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 processEnvFile(envFile string, existingVars map[string]bool) ([]string, error) { + file, err := os.Open(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. + defer file.Close() + var unassignedVars []string scanner := bufio.NewScanner(file) - var envs []string for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) - if len(line) == 0 { + + // Skip comments and empty lines + if line == "" || strings.HasPrefix(line, "#") { continue } - switch { - case strings.HasPrefix(line, "#"): - // ignore comments - continue - case !strings.Contains(line, "="): - // only has the variable name; need to lookup value - if val, ok := systemDeps.LookupEnv(line); ok { - envs = append(envs, fmt.Sprintf("%s=%s", line, val)) - } - default: - // contains a name and value - envs = append(envs, line) + + // Split by '=' to determine if the variable is assigned a value + parts := strings.SplitN(line, "=", 2) + key := strings.TrimSpace(parts[0]) + + if key != "" && len(parts) == 1 && !existingVars[key] { + // Variable exists but no value is assigned + unassignedVars = append(unassignedVars, key) } } + if err := scanner.Err(); err != nil { - return skip, []string{}, err + return nil, 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=