command-runner/internal/containers/finch/finch_run.go (304 lines of code) (raw):

package finch import ( "bufio" "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "runtime" "slices" "strings" "github.com/aws/codecatalyst-runner-cli/command-runner/internal/containers/types" "github.com/aws/codecatalyst-runner-cli/command-runner/internal/fs" "github.com/aws/codecatalyst-runner-cli/command-runner/pkg/common" "github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/rs/zerolog/log" ) func (cr *finchContainer) Create(capAdd []string, capDrop []string) common.Executor { return common.NewPipelineExecutor( cr.connect(), cr.find(), cr.create(capAdd, capDrop), ).IfNot(common.Dryrun) } func (cr *finchContainer) Start(attach bool) common.Executor { return common. NewInfoExecutor("🐦 finch run image=%s", cr.input.Image). Then( common.NewPipelineExecutor( cr.connect(), cr.find(), cr.start(attach), cr.wait().IfBool(attach), cr.tryReadUID(), cr.tryReadGID(), ).IfNot(common.Dryrun), ) } func (cr *finchContainer) start(attach bool) common.Executor { return func(ctx context.Context) error { log.Ctx(ctx).Printf("Starting container: %v", cr.id) args := []string{"start", cr.id} if attach { args = append(args, "--attach") } if rout, rerr, err := cr.f.RunWithoutStdio(ctx, args...); err != nil { return fmt.Errorf("failed to start container: %w\n%s\n%s", err, rout, rerr) } log.Ctx(ctx).Printf("Started container: %v", cr.id) return nil } } func (cr *finchContainer) wait() common.Executor { return func(ctx context.Context) error { if rout, rerr, err := cr.f.RunWithoutStdio(ctx, "wait", cr.id); err != nil { return fmt.Errorf("failed to wait container: %w\n%s\n%s", err, rout, rerr) } else { statusCode := string(rout) if statusCode != "0" { return fmt.Errorf("exit with `FAILURE`: %v", statusCode) } } return nil } } func (cr *finchContainer) tryReadID(opt string, cbk func(id int)) common.Executor { return func(ctx context.Context) error { // TODO: implement return nil } } func (cr *finchContainer) tryReadUID() common.Executor { return cr.tryReadID("-u", func(id int) { cr.uid = id }) } func (cr *finchContainer) tryReadGID() common.Executor { return cr.tryReadID("-g", func(id int) { cr.gid = id }) } func (cr *finchContainer) Pull(forcePull bool) common.Executor { return common. NewInfoExecutor("🐦 finch pull image=%s", cr.input.Image). Then( newPullExecutor(newPullExecutorInput{ Image: cr.input.Image, ForcePull: forcePull, Platform: cr.input.Platform, Username: cr.input.Username, Password: cr.input.Password, }), ) } func (cr *finchContainer) CopyIn(containerPath string, hostPath string, useGitIgnore bool) common.Executor { return common.NewPipelineExecutor( common.NewDebugExecutor("🐦 finch copyIn hostPath=%s containerPath=%s", hostPath, containerPath), cr.connect(), cr.find(), cr.copyIn(containerPath, hostPath, useGitIgnore), ).IfNot(common.Dryrun) } func (cr *finchContainer) CopyOut(hostPath string, containerPath string) common.Executor { return common.NewPipelineExecutor( common.NewDebugExecutor("🐦 finch copyOut hostPath=%s containerPath=%s", hostPath, containerPath), cr.connect(), cr.find(), cr.copyOut(hostPath, containerPath), ).IfNot(common.Dryrun) } func (cr *finchContainer) Exec(command []string, env map[string]string, user, workdir string) common.Executor { return common.NewPipelineExecutor( common.NewDebugExecutor("🐦 finch exec cmd=[%s] user=%s workdir=%s", strings.Join(command, " "), user, workdir), cr.connect(), cr.find(), cr.exec(command, env, user, workdir), ).IfNot(common.Dryrun) } func (cr *finchContainer) Remove() common.Executor { return common.NewPipelineExecutor( cr.connect(), cr.find(), ).Finally( cr.remove(), ).IfNot(common.Dryrun) } func (cr *finchContainer) connect() common.Executor { return func(ctx context.Context) error { if cr.f != nil { return nil } f, err := newFinch(finchInstallDir) if err != nil { return err } cr.f = f return nil } } type finchContainerSpec struct { ID string `json:"ID"` Names interface{} `json:"Names"` } func (cr *finchContainer) find() common.Executor { return func(ctx context.Context) error { if cr.id != "" { return nil } rout, rerr, err := cr.f.RunWithoutStdio(ctx, "container", "ls", "--all", "--format", "{{json .}}") if err != nil { return fmt.Errorf("failed to list containers: %w\n%s", err, rerr) } scanner := bufio.NewScanner(bytes.NewReader(rout)) for scanner.Scan() { cs := new(finchContainerSpec) if err := json.Unmarshal(scanner.Bytes(), cs); err != nil { return fmt.Errorf("failed unmarshalling container spec: %w", err) } names := make([]string, 0) switch typedNames := cs.Names.(type) { case []interface{}: for _, inf := range typedNames { names = append(names, inf.(string)) } case []string: names = typedNames case string: names = []string{typedNames} default: return fmt.Errorf("invalid names type: %T", cs.Names) } log.Ctx(ctx).Debug().Msgf("got back container %+v with names %v", cs, names) if slices.Contains(names, cr.input.Name) { cr.id = cs.ID return nil } } cr.id = "" return nil } } func (cr *finchContainer) create(capAdd []string, capDrop []string) common.Executor { return func(ctx context.Context) error { if cr.id != "" { return nil } input := cr.input flags := []string{"--tty", "--workdir", input.WorkingDir, "--name", input.Name} if input.Privileged { flags = append(flags, "--privileged") } for src, dst := range input.Mounts { flags = append(flags, "--mount", fmt.Sprintf("type=volume,src=%s,dst=%s", src, dst)) } for _, bind := range input.Binds { flags = append(flags, "--volume", bind) } if len(capAdd) != 0 { flags = append(flags, "--cap-add") flags = append(flags, capAdd...) } if len(capDrop) != 0 { flags = append(flags, "--cap-drop") flags = append(flags, capDrop...) } for _, e := range input.Env { flags = append(flags, "--env", e) } if len(input.Entrypoint) != 0 { flags = append(flags, "--entrypoint") flags = append(flags, input.Entrypoint...) } args := []string{"create"} args = append(args, flags...) args = append(args, input.Image) if len(input.Cmd) != 0 { args = append(args, input.Cmd...) } rout, rerr, err := cr.f.RunWithoutStdio(ctx, args...) if err != nil { return fmt.Errorf("failed to create container: '%w'\n%s\n%s", err, rout, rerr) } id := strings.TrimRight(string(rout), "\r\n") log.Ctx(ctx).Printf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, id, input.Image, input.Platform) log.Ctx(ctx).Printf("ENV ==> %v", input.Env) cr.id = id return nil } } func (cr *finchContainer) remove() common.Executor { return func(ctx context.Context) error { if cr.id == "" { return nil } log.Ctx(ctx).Debug().Msgf("🐦 finch rm %s", cr.id) _, lerr, err := cr.f.RunWithoutStdio(ctx, "rm", "--force", "--volumes", cr.id) if err != nil { return fmt.Errorf("failed to list containers: %w\n%s", err, lerr) } if err != nil { log.Ctx(ctx).Err(fmt.Errorf("failed to remove container: %w", err)) } log.Ctx(ctx).Printf("Removed container: %v", cr.id) cr.id = "" return nil } } func (cr *finchContainer) Close() common.Executor { return func(ctx context.Context) error { return nil } } type finchContainer struct { id string input types.NewContainerInput f *finch uid int gid int } func (cr *finchContainer) copyIn(containerPath string, hostPath string, useGitIgnore bool) common.Executor { return func(ctx context.Context) error { log.Ctx(ctx).Debug().Msgf("Writing %s from %s", containerPath, hostPath) if f, err := os.Stat(hostPath); err != nil { return fmt.Errorf("unable to copyIn from hostPath=%s: %w", hostPath, err) } else if filepath.Ext(hostPath) == ".tar" { // TODO: implement return fmt.Errorf("copyIn for tar file is not implemented") } else if f.IsDir() { if useGitIgnore { tempDir, err := os.MkdirTemp(fs.TmpDir(), "finch-copyin") if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } defer os.RemoveAll(tempDir) err = copyDir(ctx, tempDir, hostPath, useGitIgnore) if err != nil { return fmt.Errorf("failed to copyDir: %w", err) } hostPath = fmt.Sprintf("%s/.", tempDir) } if _, rerr, err := cr.f.RunWithoutStdio(ctx, "cp", hostPath, fmt.Sprintf("%s:%s", cr.id, containerPath)); err != nil { return fmt.Errorf("failed to copy content to container: %w\n%s", err, rerr) } return nil } else { return fmt.Errorf("unsupported srcPath=%s", hostPath) } } } func (cr *finchContainer) copyOut(hostPath string, containerPath string) common.Executor { return func(ctx context.Context) error { if err := os.MkdirAll(filepath.Dir(hostPath), 0755); err != nil { return fmt.Errorf("failed to create hostPath=%s: %w", hostPath, err) } log.Ctx(ctx).Debug().Msgf("Writing %s from %s", hostPath, containerPath) if _, rerr, err := cr.f.RunWithoutStdio(ctx, "cp", fmt.Sprintf("%s:%s", cr.id, containerPath), hostPath); err != nil { return fmt.Errorf("failed to copy content from container: %w\n%s", err, rerr) } return nil } } func (cr *finchContainer) exec(cmd []string, env map[string]string, user, workdir string) common.Executor { return func(ctx context.Context) error { // Fix slashes when running on Windows if runtime.GOOS == "windows" { var newCmd []string for _, v := range cmd { newCmd = append(newCmd, strings.ReplaceAll(v, `\`, `/`)) } cmd = newCmd } log.Ctx(ctx).Printf("Exec command '%s'", cmd) var wd string if workdir != "" { if strings.HasPrefix(workdir, "/") { wd = workdir } else { wd = fmt.Sprintf("%s/%s", cr.input.WorkingDir, workdir) } } else { wd = cr.input.WorkingDir } log.Ctx(ctx).Printf("Working directory '%s'", wd) flags := []string{"--workdir", wd} if user != "" { flags = append(flags, "--user", user) } for k, v := range env { flags = append(flags, "--env", fmt.Sprintf("%s=%s", k, v)) } args := []string{"exec"} args = append(args, flags...) args = append(args, cr.id) args = append(args, cmd...) err := cr.f.RunWithStdio(ctx, nil, cr.input.Stdout, cr.input.Stderr, args...) if err != nil { return fmt.Errorf("exec failed: %w", err) } return err } } func copyDir(ctx context.Context, destdir string, sourcedir string, useGitIgnore bool) error { if sourcedir == destdir { return fmt.Errorf("unable to copyDir when sourcedir==destdir: %s", destdir) } log.Ctx(ctx).Debug().Msgf("Copying from %s to %s", sourcedir, destdir) srcPrefix := filepath.Dir(sourcedir) if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { srcPrefix += string(filepath.Separator) } log.Ctx(ctx).Debug().Msgf("Stripping prefix:%s src:%s", srcPrefix, sourcedir) var ignorer gitignore.Matcher if useGitIgnore { ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(sourcedir)), nil) if err != nil { log.Ctx(ctx).Debug().Msgf("Error loading .gitignore: %v", err) } ignorer = gitignore.NewMatcher(ps) } fc := &fs.FileCollector{ Fs: &fs.DefaultFs{}, Ignorer: ignorer, SrcPath: sourcedir, SrcPrefix: srcPrefix, Handler: &fs.CopyCollector{ DstDir: destdir, }, } return filepath.Walk(sourcedir, fc.CollectFiles(ctx, []string{})) }