From c004516c48dcbe85cd16b0c4b0060105e5118fa8 Mon Sep 17 00:00:00 2001 From: Sam Chew Date: Wed, 2 Oct 2024 02:19:06 -0700 Subject: [PATCH] feat: Add Dockercompat Mode & DevContainer Support (#1100) * Port changes from Shubham fork Signed-off-by: Sam Chew * Add DevContainers functionality to Finch Signed-off-by: Sam Chew --------- Signed-off-by: Sam Chew --- cmd/finch/main.go | 7 + cmd/finch/nerdctl.go | 126 ++++++++++++- cmd/finch/nerdctl_darwin.go | 83 ++++++++- cmd/finch/nerdctl_darwin_test.go | 311 ++++++++++++++++++++++++++++++- cmd/finch/nerdctl_native.go | 62 ++++++ cmd/finch/nerdctl_remote.go | 86 ++++++--- cmd/finch/nerdctl_windows.go | 123 ++++++------ pkg/config/config.go | 1 + 8 files changed, 706 insertions(+), 93 deletions(-) diff --git a/cmd/finch/main.go b/cmd/finch/main.go index 9a43fcd60..0e77be70c 100644 --- a/cmd/finch/main.go +++ b/cmd/finch/main.go @@ -44,5 +44,12 @@ func initializeNerdctlCommands( for cmdName, cmdDescription := range nerdctlCmds { allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) } + + if fc.DockerCompat { + for cmdName, cmdDescription := range dockerCompatCmds { + allNerdctlCommands = append(allNerdctlCommands, nerdctlCommandCreator.create(cmdName, cmdDescription)) + } + } + return allNerdctlCommands } diff --git a/cmd/finch/nerdctl.go b/cmd/finch/nerdctl.go index c372c9e27..cde5cf652 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -6,10 +6,12 @@ package main import ( "encoding/json" "fmt" + "strings" "golang.org/x/exp/slices" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/sirupsen/logrus" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -41,6 +43,11 @@ 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) error +) + func newNerdctlCommandCreator( ncc command.NerdctlCmdCreator, ecc command.Creator, @@ -192,12 +199,33 @@ var nerdctlCmds = map[string]string{ "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, + }, +} + 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"), + "--privileged", "--read-only", "--rm", "--rootfs", "--tty", "--sig-proxy"), "shortArgFlags": sets.New[string]("-e", "-h", "-m", "-u", "-w", "-p", "-l", "-v"), }, "exec": { @@ -215,3 +243,99 @@ var cmdFlagSetMap = map[string]map[string]sets.Set[string]{ "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.DockerCompat { + nerdctlCmdArgs[index] = "--output=type=docker" + } + + return nil +} + +func handleBuildx(_ NerdctlCommandSystemDeps, fc *config.Finch, cmdName *string, args *[]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) error { + if !fc.DockerCompat { + return nil + } + + if *args == nil { + return fmt.Errorf("invalid arguments: args (null pointer)") + } + + modeDockerCompat := `--mode=dockercompat` + inspectType := "" + sizeArg := "" + savedArgs := []string{} + skip := false + + 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...) + default: + return fmt.Errorf("unsupported inspect type: %s", inspectType) + } + + return nil +} diff --git a/cmd/finch/nerdctl_darwin.go b/cmd/finch/nerdctl_darwin.go index 527bd9eb9..4ed4164ed 100644 --- a/cmd/finch/nerdctl_darwin.go +++ b/cmd/finch/nerdctl_darwin.go @@ -13,6 +13,7 @@ import ( "github.com/lima-vm/lima/pkg/networks" "github.com/runfinch/finch/pkg/command" + "github.com/runfinch/finch/pkg/config" "github.com/runfinch/finch/pkg/flog" ) @@ -20,13 +21,15 @@ func convertToWSLPath(_ NerdctlCommandSystemDeps, _ string) (string, error) { return "", nil } -var aliasMap = map[string]string{ - "run": "container run", -} +var osAliasMap = map[string]string{} -var argHandlerMap = map[string]map[string]argHandler{} +var osArgHandlerMap = map[string]map[string]argHandler{ + "container run": { + "--mount": handleBindMounts, + }, +} -var commandHandlerMap = map[string]commandHandler{} +var osCommandHandlerMap = map[string]commandHandler{} func (nc *nerdctlCommand) GetCmdArgs() []string { return []string{"shell", limaInstanceName, "sudo", "-E"} @@ -46,3 +49,73 @@ func resolveIP(host string, logger flog.Logger, _ command.Creator) (string, erro } return host, nil } + +// removes the consistency key-value entity from --mount. +func handleBindMounts(_ 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) + } + } + + // This is where the 'consistency=cached' strings should be removed.... + // "consistency will be one of the keys in the following map" + + // eg --mount type=bind,source="$(pwd)"/target,target=/app,readonly + // eg --mount type=bind, + // source=/Users/stchew/projs/arbtest_devcontainers_extensions, + // target=/workspaces/arbtest_devcontainers_extensions, + // 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 + delete(m, "consistency") + + // 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 +} + +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 dfa0e6f5c..fc99f001b 100644 --- a/cmd/finch/nerdctl_darwin_test.go +++ b/cmd/finch/nerdctl_darwin_test.go @@ -1228,6 +1228,315 @@ func TestNerdctlCommand_run(t *testing.T) { } } +func TestNerdctlCommand_run_inspectCommand(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: "inspect without flags", + cmdName: "inspect", + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, + args: []string{"da24"}, + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "inspect", "--mode=dockercompat", "da24").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "inspect with typeContainer flag", + cmdName: "inspect", + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, + args: []string{"--type=container", "44de"}, + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "inspect", "--mode=dockercompat", "44de").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "inspect with typeVolume option", + cmdName: "inspect", + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, + args: []string{"--type=volume", "myVolume"}, + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "volume", "inspect", "myVolume").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "inspect with typeImage option", + cmdName: "inspect", + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, + args: []string{"--type=image", "myImage"}, + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create( + "shell", + limaInstanceName, + "sudo", + "-E", + nerdctlCmdName, + "image", + "inspect", + "--mode=dockercompat", + "myImage", + ).Return(c) + c.EXPECT().Run() + }, + }, + { + name: "inspect with size flag", + cmdName: "inspect", + fc: &config.Finch{ + SharedSettings: config.SharedSettings{ + DockerCompat: true, + }, + }, + args: []string{"--size", "44de"}, + 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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "inspect", "--mode=dockercompat", "44de").Return(c) + c.EXPECT().Run() + }, + }, + } + + 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)) + }) + } +} + +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") + ncsd.EXPECT().LookupEnv("AWS_ACCESS_KEY_ID").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SECRET_ACCESS_KEY").Return("", false) + ncsd.EXPECT().LookupEnv("AWS_SESSION_TOKEN").Return("", false) + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) + ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) + 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)) + }) + } +} + func TestNerdctlCommand_run_miscCommand(t *testing.T) { t.Parallel() testCases := []struct { @@ -1271,7 +1580,7 @@ func TestNerdctlCommand_run_miscCommand(t *testing.T) { ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("", false) ncsd.EXPECT().LookupEnv("COMPOSE_FILE").Return("", false) c := mocks.NewCommand(ctrl) - lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "build", "-t", "demo", ".").Return(c) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "-E", nerdctlCmdName, "image", "build", "-t", "demo", ".").Return(c) c.EXPECT().Run() }, }, diff --git a/cmd/finch/nerdctl_native.go b/cmd/finch/nerdctl_native.go index 062115f22..784043741 100644 --- a/cmd/finch/nerdctl_native.go +++ b/cmd/finch/nerdctl_native.go @@ -6,6 +6,9 @@ package main import ( + "fmt" + "strings" + "golang.org/x/exp/slices" "github.com/runfinch/finch/pkg/command" @@ -13,6 +16,14 @@ import ( ) func (nc *nerdctlCommand) run(cmdName string, args []string) error { + + var ( + hasCmdHandler, hasArgHandler bool + cmdHandler commandHandler + aMap map[string]argHandler + err error + ) + // eat the debug arg, and set the log level to avoid nerdctl parsing this flag dbgIdx := slices.Index(args, "--debug") if dbgIdx >= 0 { @@ -20,7 +31,52 @@ 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) + 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 + } + } + } + } + + // TODO: Extra manipulation if overwriting cmdName with alias + //splitName := strings.Split(cmdName, " ") + //cmdArgs := append([]string{splitName[0]}, splitName[1:]...) + //cmdArgs = append(cmdArgs, args...) + cmdArgs := append([]string{cmdName}, args...) + if nc.shouldReplaceForHelp(cmdName, args) { return nc.ncc.RunWithReplacingStdout( []command.Replacement{{Source: "nerdctl", Target: "finch"}}, @@ -30,3 +86,9 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { return nc.ncc.Create(cmdArgs...).Run() } + +var osAliasMap = map[string]string{} + +var osArgHandlerMap = map[string]map[string]argHandler{} + +var osCommandHandlerMap = map[string]commandHandler{} diff --git a/cmd/finch/nerdctl_remote.go b/cmd/finch/nerdctl_remote.go index f1659df50..333535b0a 100644 --- a/cmd/finch/nerdctl_remote.go +++ b/cmd/finch/nerdctl_remote.go @@ -8,6 +8,7 @@ package main import ( "bufio" "fmt" + "maps" "path/filepath" "runtime" "strings" @@ -25,11 +26,6 @@ import ( const nerdctlCmdName = "nerdctl" -type ( - argHandler func(systemDeps NerdctlCommandSystemDeps, args []string, index int) error - commandHandler func(systemDeps NerdctlCommandSystemDeps, args []string) error -) - func (nc *nerdctlCommand) run(cmdName string, args []string) error { err := nc.assertVMIsRunning(nc.ncc, nc.logger) if err != nil { @@ -37,33 +33,54 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { } var ( nerdctlArgs, envs, fileEnvs, cmdArgs, runArgs []string - skip, hasCmdHander, hasArgHandler, lastOpt bool + skip, hasCmdHandler, hasArgHandler, lastOpt bool cmdHandler commandHandler aMap map[string]argHandler firstOptPos int ) - alias, hasAlias := aliasMap[cmdName] + // 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]) + } + + alias, hasAlias := aggAliasMap[cmdName] if hasAlias { cmdName = alias - cmdHandler, hasCmdHander = commandHandlerMap[alias] - aMap, hasArgHandler = argHandlerMap[alias] + cmdHandler, hasCmdHandler = aggCmdHandlerMap[alias] + aMap, hasArgHandler = aggArgHandlerMap[alias] } else { // Check if the command has a handler - cmdHandler, hasCmdHander = commandHandlerMap[cmdName] - aMap, hasArgHandler = argHandlerMap[cmdName] + cmdHandler, hasCmdHandler = aggCmdHandlerMap[cmdName] + aMap, hasArgHandler = aggArgHandlerMap[cmdName] - if !hasCmdHander && !hasArgHandler && len(args) > 0 { + if !hasCmdHandler && !hasArgHandler && len(args) > 0 { // for commands like image build, container run key := fmt.Sprintf("%s %s", cmdName, args[0]) - cmdHandler, hasCmdHander = commandHandlerMap[key] - aMap, hasArgHandler = argHandlerMap[key] + cmdHandler, hasCmdHandler = aggCmdHandlerMap[key] + aMap, hasArgHandler = aggArgHandlerMap[key] } } // First check if the command has command handler - if hasCmdHander { - err := cmdHandler(nc.systemDeps, args) + if hasCmdHandler { + err := cmdHandler(nc.systemDeps, nc.fc, &cmdName, &args) if err != nil { return err } @@ -89,13 +106,13 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { shortFlagArgSet := cmdFlagSetMap[cmdName]["shortArgFlags"] for i, arg := range args { - // Check if command requires arg handling + // 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, args, i) + err = h(nc.systemDeps, nc.fc, args, i) if err != nil { return err } @@ -103,6 +120,7 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { 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 @@ -116,7 +134,6 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { cmdArgs = append(cmdArgs, arg) continue } - switch { case arg == "--debug": nc.logger.SetLevel(flog.Debug) @@ -157,17 +174,26 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { envs = append(envs, addEnv) } case shortFlagBoolSet.Has(arg) || longFlagBoolSet.Has(arg): - // exact match to a short flag: -? + // 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]): - // begins with a defined short flag, but is adjacent to one or more short flags: -???? + // 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: -h,-m,-u,-w,-p,-l,-v - // or begins with: -h,-m,-u,-w,-p,-l,-v - // concatenated short flags and values: -p"8080:8080" + // 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 != "" { @@ -175,7 +201,11 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { 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 != "" { @@ -227,13 +257,13 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { default: for i, arg := range args { - // Check if command requires arg handling + // 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, args, i) + err = h(nc.systemDeps, nc.fc, args, i) if err != nil { return err } @@ -284,7 +314,7 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { if slices.Contains(args, "run") { ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) } - case "build", "pull", "push", "run": + case "build", "pull", "push", "container run": ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger) } diff --git a/cmd/finch/nerdctl_windows.go b/cmd/finch/nerdctl_windows.go index 26bcf26d0..1d55085b1 100644 --- a/cmd/finch/nerdctl_windows.go +++ b/cmd/finch/nerdctl_windows.go @@ -12,9 +12,50 @@ 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, + "--mount": handleBindMounts, + }, + "compose": { + "--file": handleFilePath, + }, +} + func (nc *nerdctlCommand) GetCmdArgs() []string { wd, err := nc.systemDeps.GetWd() if err != nil { @@ -46,7 +87,7 @@ func convertToWSLPath(systemDeps NerdctlCommandSystemDeps, winPath string) (stri } // substitutes wsl path for the provided option in place for nerdctl args. -func handleFilePath(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string, index int) error { +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 @@ -72,7 +113,7 @@ func handleFilePath(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string } // 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, nerdctlCmdArgs []string, index int) error { +func handleVolume(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { prefix := nerdctlCmdArgs[index] var ( v string @@ -129,7 +170,9 @@ func handleVolume(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string, } // translates source path of the bind mount to wslpath for --mount option. -func handleBindMounts(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string, index int) error { +// +// and removes the consistency key-value entity from --mount +func handleBindMounts(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { prefix := nerdctlCmdArgs[index] var ( v string @@ -146,13 +189,15 @@ func handleBindMounts(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []stri } } + // e.g. --mount type=bind,source="$(pwd)"/target,target=/app,readonly + // e.g. --mount type=bind,source=/Users/stchew/projs/arbtest_devcontainers_extensions, + // target=/workspaces/arbtest_devcontainers_extensions,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, "=") - // eg --mount type=bind,source="$(pwd)"/target,target=/app,readonly if len(parts) < 2 { ro = append(ro, parts...) } else { @@ -163,6 +208,11 @@ func handleBindMounts(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []stri if m["type"] != "bind" { return nil } + + // Remove 'consistency' key-value pair + delete(m, "consistency") + + // Handle src/source path var k string path, ok := m["src"] if !ok { @@ -197,7 +247,7 @@ func handleBindMounts(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []stri } // handles --output/-o for build command. -func handleOutputOption(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string, index int) error { +func handleOutputOption(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { prefix := nerdctlCmdArgs[index] var ( v string @@ -244,7 +294,7 @@ func handleOutputOption(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []st } // handles --secret option for build command. -func handleSecretOption(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string, index int) error { +func handleSecretOption(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, nerdctlCmdArgs []string, index int) error { prefix := nerdctlCmdArgs[index] var ( v string @@ -290,8 +340,8 @@ func handleSecretOption(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []st } // cp command handler, takes command arguments and converts hostpath to wsl path in place. It ignores all other arguments. -func cpHandler(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string) error { - for i, arg := range nerdctlCmdArgs { +func cpHandler(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, _ *string, nerdctlCmdArgs *[]string) error { + for i, arg := range *nerdctlCmdArgs { // -L and --follow-symlink don't have to be processed if strings.HasPrefix(arg, "-") || arg == "cp" { continue @@ -307,28 +357,28 @@ func cpHandler(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string) err if err != nil { return err } - nerdctlCmdArgs[i] = wslPath + (*nerdctlCmdArgs)[i] = wslPath } return nil } // this is the handler for image build command. It translates build context to wsl path. -func imageBuildHandler(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []string) error { +func imageBuildHandler(systemDeps NerdctlCommandSystemDeps, _ *config.Finch, _ *string, nerdctlCmdArgs *[]string) error { var err error - argLen := len(nerdctlCmdArgs) - 1 + argLen := len(*nerdctlCmdArgs) - 1 // -h/--help don't have buildcontext, just return - for _, a := range nerdctlCmdArgs { + for _, a := range *nerdctlCmdArgs { if a == "--help" || a == "-h" { return nil } } - if nerdctlCmdArgs[argLen] != "--debug" { - nerdctlCmdArgs[argLen], err = convertToWSLPath(systemDeps, nerdctlCmdArgs[argLen]) + 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]) + (*nerdctlCmdArgs)[argLen-1], err = convertToWSLPath(systemDeps, (*nerdctlCmdArgs)[argLen-1]) if err != nil { return err } @@ -336,49 +386,6 @@ func imageBuildHandler(systemDeps NerdctlCommandSystemDeps, nerdctlCmdArgs []str return nil } -var aliasMap = map[string]string{ - "build": "image build", - "save": "image save", - "load": "image load", - "cp": "container cp", - "run": "container run", -} - -var argHandlerMap = 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, - "--mount": handleBindMounts, - }, - "compose": { - "--file": handleFilePath, - }, -} - -var commandHandlerMap = map[string]commandHandler{ - "container cp": cpHandler, - "image build": imageBuildHandler, -} - func mapToString(m map[string]string) string { var parts []string for k, v := range m { diff --git a/pkg/config/config.go b/pkg/config/config.go index a479d9431..ceff93ea4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -38,6 +38,7 @@ type SharedSystemSettings struct { type SharedSettings struct { Snapshotters []string `yaml:"snapshotters,omitempty"` CredsHelpers []string `yaml:"creds_helpers,omitempty"` + DockerCompat bool `yaml:"dockercompat,omitempty"` } // Nerdctl is a copy from github.com/containerd/nerdctl/cmd/nerdctl/main.go