command-runner/pkg/runner/shell_command_executor.go (263 lines of code) (raw):
package runner
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"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"
)
type shellCommandExecutor struct {
Stdout io.Writer
Stderr io.Writer
Env []string
WorkingDir string
CloseExecutors []common.Executor
ctx context.Context
mceDir string
}
type newShellCommandExecutorParams struct {
*EnvironmentConfiguration
}
func newShellCommandExecutor(ctx context.Context, params *newShellCommandExecutorParams) (commandExecutor, error) {
mceDir, err := os.MkdirTemp(fs.TmpDir(), "mce")
if err != nil {
return nil, err
}
closeExecutors := []common.Executor{}
for _, filemap := range params.FileMaps {
switch filemap.Type {
case FileMapTypeCopyOut:
closeExecutors = append(closeExecutors, copyOut(ctx, mceDir, params.WorkingDir, filemap))
case FileMapTypeBind:
// treat bind mount as symlink
if err := symlink(mceDir, params.WorkingDir, filemap); err != nil {
return nil, err
}
case FileMapTypeCopyIn:
if err := copyDir(
ctx,
resolvePath(filemap.TargetPath, mceDir),
resolvePath(filemap.SourcePath, params.WorkingDir),
false,
); err != nil {
return nil, err
}
case FileMapTypeCopyInWithGitignore:
if err := copyDir(
ctx,
resolvePath(filemap.TargetPath, mceDir),
resolvePath(filemap.SourcePath, params.WorkingDir),
true,
); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown filemap Type")
}
}
closeExecutors = append(closeExecutors, setOutputs(mceDir, params.Stdout))
closeExecutors = append(closeExecutors, func(ctx context.Context) error {
log.Debug().Msgf("close() is removing %s", mceDir)
return os.RemoveAll(mceDir)
})
env := make([]string, 0)
var defaultDir string
if params.Env != nil {
for k, v := range params.Env {
if strings.HasPrefix(k, "CATALYST_SOURCE_DIR_") {
v = resolvePath(v, mceDir)
if k == "CATALYST_SOURCE_DIR_WorkflowSource" || defaultDir == "" {
defaultDir = v
}
}
env = append(env, fmt.Sprintf("%s=%s", k, interpolate(v, params.Env)))
}
}
if defaultDir == "" {
defaultDir = params.WorkingDir
}
env = append(env, fmt.Sprintf("PATH=%s", os.Getenv("PATH")))
env = append(env, fmt.Sprintf("CATALYST_DEFAULT_DIR=%s", defaultDir))
if err := os.WriteFile(filepath.Join(mceDir, "env.sh"), []byte(""), 00666); err != nil /* #nosec G306 */ {
return nil, err
}
if err := os.WriteFile(filepath.Join(mceDir, "dir.txt"), []byte(defaultDir), 00644); err != nil /* #nosec G306 */ {
return nil, err
}
return &shellCommandExecutor{
Stdout: params.Stdout,
Stderr: params.Stderr,
WorkingDir: defaultDir,
Env: env,
CloseExecutors: closeExecutors,
mceDir: mceDir,
ctx: ctx,
}, nil
}
func (sce *shellCommandExecutor) Close(isError bool) error {
var err error
if !isError {
err = common.NewPipelineExecutor(sce.CloseExecutors...).TraceRegion("close-executors")(sce.ctx)
}
return err
}
func (sce *shellCommandExecutor) ExecuteCommand(ctx context.Context, command Command) error {
script := fmt.Sprintf(`
MCE_DIR=%s
cd $(cat ${MCE_DIR}/dir.txt)
set -a
. ${MCE_DIR}/env.sh
%s
CODEBUILD_LAST_EXIT=$?
export -p > ${MCE_DIR}/env.sh
pwd > ${MCE_DIR}/dir.txt
exit $CODEBUILD_LAST_EXIT`, sce.mceDir, strings.Join(command, " "))
scriptName := fmt.Sprintf("script-%d.sh", time.Now().UnixNano())
scriptPath := filepath.Join(sce.mceDir, scriptName)
if err := os.WriteFile(scriptPath, []byte(script), 00755); err != nil /* #nosec G306 */ {
return err
}
cmd := exec.CommandContext(ctx, "/bin/sh", scriptPath) //#nosec G204
cmd.Stdin = nil
cmd.Dir = sce.WorkingDir
cmd.Env = sce.Env
log.Debug().Msgf("ExecuteCommand: path=%s args=%s dir=%s env=%#v script=%s", cmd.Path, cmd.Args, cmd.Dir, cmd.Env, script)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
log.Ctx(ctx).Debug().Msgf("%s shell run command=%+v workdir=%s", logPrefix, cmd, sce.WorkingDir)
if common.Dryrun(ctx) {
log.Ctx(ctx).Debug().Msgf("exit for dryrun")
return nil
}
if err := cmd.Start(); err != nil {
return err
}
if sce.Stdout != nil {
go streamPipe(sce.Stdout, stdout)
}
if sce.Stderr != nil {
go streamPipe(sce.Stderr, stderr)
}
return cmd.Wait()
}
const logPrefix = " \U0001F4BB "
func streamPipe(dst io.Writer, src io.ReadCloser) {
reader := bufio.NewReader(src)
_, _ = io.Copy(dst, reader)
}
func copyDir(ctx context.Context, destdir string, sourcedir string, useGitIgnore bool) error {
if sourcedir == destdir {
return fmt.Errorf("unable to copyDir when sourcedir==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{}))
}
func setOutputs(mceDir string, stdout io.Writer) common.Executor {
return func(context.Context) error {
f, err := os.Open(filepath.Join(mceDir, "env.sh"))
if err != nil {
return nil
}
defer f.Close()
scanner := bufio.NewScanner(f)
pattern := regexp.MustCompile(`^export (.+)="(.+)"$`)
for scanner.Scan() {
line := scanner.Text()
kv := pattern.FindStringSubmatch(line)
if len(kv) == 3 {
fmt.Fprintf(stdout, "::set-output name=%s::%s\n", kv[1], kv[2])
}
}
return scanner.Err()
}
}
func copyOut(ctx context.Context, mceDir string, workingDir string, filemap *FileMap) common.Executor {
sourcePath := resolvePath(filemap.SourcePath, mceDir)
targetPath := resolvePath(filemap.TargetPath, workingDir)
return func(context.Context) error {
sources, err := filepath.Glob(sourcePath)
if err != nil {
return err
}
log.Debug().Msgf("clearing cache %s", targetPath)
if err := os.RemoveAll(targetPath); err != nil {
return err
}
log.Debug().Msgf("copying %v (%s) to %s", sources, sourcePath, targetPath)
for _, source := range sources {
if err := copyDir(
ctx,
targetPath,
source,
false,
); err != nil {
return err
}
}
return nil
}
}
func symlink(mceDir string, workingDir string, filemap *FileMap) error {
if resolvePath(filemap.SourcePath, workingDir) != resolvePath(".", workingDir) {
sourcePath := resolvePath(filemap.SourcePath, workingDir)
targetPath := resolvePath(filemap.TargetPath, mceDir)
log.Debug().Msgf("symlink mount %s to %s", sourcePath, targetPath)
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
return os.Symlink(sourcePath, targetPath)
}
return nil
}
func interpolate(s string, vars map[string]string) string {
r := regexp.MustCompile(`\${?([a-zA-Z0-9_\-.]+)}?`)
symbols := regexp.MustCompile(`[${}]`)
repl := func(match string) string {
key := symbols.ReplaceAllString(match, "")
if val, ok := vars[key]; ok {
return val
}
return match
}
rtn := r.ReplaceAllStringFunc(s, repl)
log.Debug().Msgf("interpolate %s -> %s", s, rtn)
return rtn
}