command-runner/pkg/runner/container_command_executor.go (280 lines of code) (raw):

package runner import ( "context" "fmt" "os" "path/filepath" "regexp" "runtime" "strings" "time" "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/opencontainers/selinux/go-selinux" "github.com/rs/zerolog/log" ) type containerCommandExecutor struct { Container types.Container ReuseContainers bool CloseExecutors []common.Executor mceDir string ctx context.Context } type newContainerCommandExecutorParams struct { *EnvironmentConfiguration ID string Image string Entrypoint Command ContainerService types.ContainerService } const containerSourceDir = "/codecatalyst/output/src" func newContainerCommandExecutor(ctx context.Context, params *newContainerCommandExecutorParams) (commandExecutor, error) { containerName := fmt.Sprintf("codecatalyst-%s", regexp.MustCompile(`[^a-zA-Z0-9_.-]`).ReplaceAllString(params.ID, "_")) containerName = strings.ToLower(containerName) var imagePrep common.Executor var image string platform := "" if i, found := strings.CutPrefix(params.Image, "docker://"); found { // pull image from registry image = i } else { // local docker build dockerfilePath := params.Image if !filepath.IsAbs(dockerfilePath) { dockerfilePath = filepath.Join(params.WorkingDir, params.Image) } _, err := os.Stat(dockerfilePath) if err != nil { return nil, err } image = fmt.Sprintf("%s:%s", containerName, "latest") exists, err := params.ContainerService.ImageExistsLocally(ctx, image, platform) log.Ctx(ctx).Debug().Msgf("%s exists? %v", image, exists) if err != nil { log.Ctx(ctx).Error().Err(err).Msg("unable to check for local image") } if params.Reuse && exists { imagePrep = func(ctx context.Context) error { return nil } } else { imagePrep = params.ContainerService.BuildImage(types.BuildImageInput{ ContextDir: filepath.Dir(dockerfilePath), Dockerfile: filepath.Base(dockerfilePath), ImageTag: image, Platform: platform, }).TraceRegion("image-build") } } env, containerDefaultDir, err := setupEnvironmentVariables(params.Env) if err != nil { return nil, err } mceDir, err := setupMceDir(env, containerDefaultDir) if err != nil { return nil, err } binds := []string{ fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"), fmt.Sprintf("%s:%s", mceDir, "/tmp/mce"), } for _, filemap := range params.FileMaps { srcPath := resolvePath(filemap.SourcePath, params.WorkingDir) targetPath := resolvePath(filemap.TargetPath, containerSourceDir) if filemap.Type == FileMapTypeBind { binds = append(binds, fmt.Sprintf( "%s:%s%s", srcPath, targetPath, bindModifiers(), ), ) } } log.Ctx(ctx).Debug().Msgf("Container binds: %#v", binds) log.Ctx(ctx).Debug().Msgf("Container env: %#v", env) actionContainer := params.ContainerService.NewContainer(types.NewContainerInput{ Image: image, Name: containerName, Stdout: params.Stdout, Stderr: params.Stderr, Env: env, WorkingDir: containerDefaultDir, Binds: binds, Entrypoint: params.Entrypoint, }) copyExecutors, closeExecutors, err := setupCopyAndCloseExecutors(actionContainer, params.WorkingDir, params.FileMaps) if err != nil { return nil, err } if imagePrep == nil { imagePrep = actionContainer.Pull(true).TraceRegion("image-pull") } if err := common.NewPipelineExecutor( imagePrep, actionContainer.Remove().IfBool(!params.Reuse), actionContainer.Create(nil, nil).TraceRegion("container-create"), common.NewPipelineExecutor(copyExecutors...).TraceRegion("container-copy"), actionContainer.Start(false).TraceRegion("container-start"), )(ctx); err != nil { return nil, fmt.Errorf("unable to create container executor: %w", err) } return &containerCommandExecutor{ Container: actionContainer, ReuseContainers: params.Reuse, CloseExecutors: closeExecutors, mceDir: mceDir, ctx: ctx, }, nil } func (cce *containerCommandExecutor) Close(isError bool) error { var err error if !isError { err = common.NewPipelineExecutor(cce.CloseExecutors...).TraceRegion("close-executors")(cce.ctx) } if !cce.ReuseContainers { if err := cce.Container.Remove()(context.Background()); err != nil { log.Ctx(cce.ctx).Error().Err(err).Msg("error removing container") } } if err := os.RemoveAll(cce.mceDir); err != nil { log.Ctx(cce.ctx).Error().Err(err).Msg("error removing temp mce directory") } return err } func (cce *containerCommandExecutor) ExecuteCommand(ctx context.Context, command Command) error { script := fmt.Sprintf(`cd $(cat /tmp/mce/tmp/dir.txt) set -a . /tmp/mce/tmp/env.sh while read line; do env "$line" > /dev/null done < /tmp/mce/tmp/init.env %s CODEBUILD_LAST_EXIT=$? export -p > /tmp/mce/tmp/env.sh pwd > /tmp/mce/tmp/dir.txt exit $CODEBUILD_LAST_EXIT`, strings.Join(command, " ")) log.Ctx(ctx).Debug().Msgf("script: %s", script) scriptName := fmt.Sprintf("script-%d.sh", time.Now().UnixNano()) if err := os.WriteFile(filepath.Join(cce.mceDir, "tmp", scriptName), []byte(script), 00755); err != nil /* #nosec G306 */ { return err } return cce.Container.Exec([]string{"/bin/sh", fmt.Sprintf("/tmp/mce/tmp/%s", scriptName)}, nil, "", "")(ctx) } func bindModifiers() string { var bindModifiers string if runtime.GOOS == "darwin" { bindModifiers = ":consistent" } if selinux.GetEnabled() { bindModifiers = ":z" } return bindModifiers } func resolvePath(path string, basePath string) string { p := path if !filepath.IsAbs(p) { absBasePath, err := filepath.Abs(basePath) if err != nil { log.Fatal().Err(err) } p = fmt.Sprintf("%s/%s", absBasePath, p) } return p } func clean(dir string) common.Executor { return func(ctx context.Context) error { if err := os.RemoveAll(dir); err != nil { return err } return os.MkdirAll(dir, 0755) } } func setupMceDir(env []string, containerDefaultDir string) (string, error) { mceDir, err := os.MkdirTemp(fs.TmpDir(), "mce") if err != nil { return "", err } if err := os.MkdirAll(filepath.Join(mceDir, "tmp"), 0755); err != nil { return "", fmt.Errorf("unable to create tmp dir: %w", err) } if err := os.WriteFile(filepath.Join(mceDir, "tmp", "init.env"), []byte(strings.Join(env, "\n")), 00777); err != nil /* #nosec G306 */ { return "", err } if err := os.WriteFile(filepath.Join(mceDir, "tmp", "env.sh"), []byte(""), 00666); err != nil /* #nosec G306 */ { return "", err } if err := os.WriteFile(filepath.Join(mceDir, "tmp", "dir.txt"), []byte(containerDefaultDir), 00666); err != nil /* #nosec G306 */ { return "", err } envout := `. /tmp/mce/tmp/env.sh env -0 | while IFS='=' read -r -d '' n v; do printf "::set-output name=%s::%s\n" "$n" "$v"; done` if err := os.WriteFile(filepath.Join(mceDir, "tmp", "envout.sh"), []byte(envout), 00755); err != nil /* #nosec G306 */ { return "", err } return mceDir, nil } func setupCopyAndCloseExecutors(actionContainer types.Container, workingDir string, filemaps []*FileMap) ([]common.Executor, []common.Executor, error) { copyExecutors := []common.Executor{} closeExecutors := []common.Executor{ actionContainer.Exec([]string{"/bin/bash", "/tmp/mce/tmp/envout.sh"}, nil, "", "/"), } for _, filemap := range filemaps { switch filemap.Type { case FileMapTypeBind: continue case FileMapTypeCopyOut: srcPath := resolvePath(filemap.SourcePath, containerSourceDir) closeExecutors = append( closeExecutors, actionContainer.Exec([]string{"mkdir", "-p", "/extract"}, nil, "", "/"), actionContainer.Exec([]string{"/bin/sh", "-c", fmt.Sprintf("cp -a %s /extract || echo 'nothing to cache' > /dev/null 2>&1", srcPath)}, nil, "", "/"), ) if !strings.HasSuffix(srcPath, "/.") { closeExecutors = append(closeExecutors, clean(filemap.TargetPath)) } closeExecutors = append( closeExecutors, actionContainer.CopyOut(filemap.TargetPath, "/extract/."), actionContainer.Exec([]string{"rm", "-rf", "/extract"}, nil, "", "/"), ) case FileMapTypeCopyIn: copyExecutors = append( copyExecutors, actionContainer.CopyIn( resolvePath(filemap.TargetPath, containerSourceDir), resolvePath(filemap.SourcePath, workingDir), false, ), ) case FileMapTypeCopyInWithGitignore: copyExecutors = append( copyExecutors, actionContainer.CopyIn( resolvePath(filemap.TargetPath, containerSourceDir), resolvePath(filemap.SourcePath, workingDir), true, ), ) default: return nil, nil, fmt.Errorf("unknown filemap Type") } } return copyExecutors, closeExecutors, nil } func setupEnvironmentVariables(env map[string]string) ([]string, string, error) { var containerDefaultDir string envVars := make([]string, 0) for k, v := range env { if strings.HasPrefix(k, "CATALYST_SOURCE_DIR_") { v = resolvePath(v, containerSourceDir) if k == "CATALYST_SOURCE_DIR_WorkflowSource" || containerDefaultDir == "" { containerDefaultDir = v } } envVars = append(envVars, fmt.Sprintf("%s=%s", k, v)) } if containerDefaultDir == "" { return nil, "", fmt.Errorf("input source or artifact is required") } envVars = append(envVars, fmt.Sprintf("CATALYST_DEFAULT_DIR=%s", containerDefaultDir)) return envVars, containerDefaultDir, nil }