diff options
Diffstat (limited to 'pkg/container/host_environment.go')
-rw-r--r-- | pkg/container/host_environment.go | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/pkg/container/host_environment.go b/pkg/container/host_environment.go new file mode 100644 index 0000000..b12e69f --- /dev/null +++ b/pkg/container/host_environment.go @@ -0,0 +1,501 @@ +package container + +import ( + "archive/tar" + "bytes" + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "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" + "golang.org/x/term" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/filecollector" + "github.com/nektos/act/pkg/lookpath" +) + +type HostEnvironment struct { + Name string + Path string + TmpDir string + ToolCache string + Workdir string + ActPath string + Root string + CleanUp func() + StdOut io.Writer + LXC bool +} + +func (e *HostEnvironment) Create(_, _ []string) common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +func (e *HostEnvironment) ConnectToNetwork(name string) common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +func (e *HostEnvironment) Close() common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor { + return func(ctx context.Context) error { + for _, f := range files { + if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil { + return err + } + } + return nil + } +} + +func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { + if err := os.RemoveAll(destPath); err != nil { + return err + } + tr := tar.NewReader(tarStream) + cp := &filecollector.CopyCollector{ + DstDir: destPath, + } + for { + ti, err := tr.Next() + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } + if ti.FileInfo().IsDir() { + continue + } + if ctx.Err() != nil { + return fmt.Errorf("CopyTarStream has been cancelled") + } + if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil { + return err + } + } +} + +func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor { + return func(ctx context.Context) error { + logger := common.Logger(ctx) + srcPrefix := filepath.Dir(srcPath) + if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { + srcPrefix += string(filepath.Separator) + } + logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath) + var ignorer gitignore.Matcher + if useGitIgnore { + ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil) + if err != nil { + logger.Debugf("Error loading .gitignore: %v", err) + } + + ignorer = gitignore.NewMatcher(ps) + } + fc := &filecollector.FileCollector{ + Fs: &filecollector.DefaultFs{}, + Ignorer: ignorer, + SrcPath: srcPath, + SrcPrefix: srcPrefix, + Handler: &filecollector.CopyCollector{ + DstDir: destPath, + }, + } + return filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{})) + } +} + +func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { + buf := &bytes.Buffer{} + tw := tar.NewWriter(buf) + defer tw.Close() + srcPath = filepath.Clean(srcPath) + fi, err := os.Lstat(srcPath) + if err != nil { + return nil, err + } + tc := &filecollector.TarCollector{ + TarWriter: tw, + } + if fi.IsDir() { + srcPrefix := srcPath + if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) { + srcPrefix += string(filepath.Separator) + } + fc := &filecollector.FileCollector{ + Fs: &filecollector.DefaultFs{}, + SrcPath: srcPath, + SrcPrefix: srcPrefix, + Handler: tc, + } + err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{})) + if err != nil { + return nil, err + } + } else { + var f io.ReadCloser + var linkname string + if fi.Mode()&fs.ModeSymlink != 0 { + linkname, err = os.Readlink(srcPath) + if err != nil { + return nil, err + } + } else { + f, err = os.Open(srcPath) + if err != nil { + return nil, err + } + defer f.Close() + } + err := tc.WriteFile(fi.Name(), fi, linkname, f) + if err != nil { + return nil, err + } + } + return io.NopCloser(buf), nil +} + +func (e *HostEnvironment) Pull(_ bool) common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +func (e *HostEnvironment) Start(_ bool) common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +type ptyWriter struct { + Out io.Writer + AutoStop bool + dirtyLine bool +} + +func (w *ptyWriter) Write(buf []byte) (int, error) { + if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 { + n, err := w.Out.Write(buf[:len(buf)-1]) + if err != nil { + return n, err + } + if w.dirtyLine || len(buf) > 1 && buf[len(buf)-2] != '\n' { + _, _ = w.Out.Write([]byte("\n")) + return n, io.EOF + } + return n, io.EOF + } + w.dirtyLine = strings.LastIndex(string(buf), "\n") < len(buf)-1 + return w.Out.Write(buf) +} + +type localEnv struct { + env map[string]string +} + +func (l *localEnv) Getenv(name string) string { + if runtime.GOOS == "windows" { + for k, v := range l.env { + if strings.EqualFold(name, k) { + return v + } + } + return "" + } + return l.env[name] +} + +func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) { + f, err := lookpath.LookPath2(cmd, &localEnv{env: env}) + if err != nil { + err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH" + if _, _err := writer.Write([]byte(err + "\n")); _err != nil { + return "", fmt.Errorf("%v: %w", err, _err) + } + return "", errors.New(err) + } + return f, nil +} + +func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) { + ppty, tty, err := openPty() + if err != nil { + return nil, nil, err + } + if term.IsTerminal(int(tty.Fd())) { + _, err := term.MakeRaw(int(tty.Fd())) + if err != nil { + ppty.Close() + tty.Close() + return nil, nil, err + } + } + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + cmd.SysProcAttr = getSysProcAttr(cmdline, true) + return ppty, tty, nil +} + +func writeKeepAlive(ppty io.Writer) { + c := 1 + var err error + for c == 1 && err == nil { + c, err = ppty.Write([]byte{4}) + <-time.After(time.Second) + } +} + +func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFunc) { + defer func() { + finishLog() + }() + if _, err := io.Copy(writer, ppty); err != nil { + return + } +} + +func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor { + return func(ctx context.Context) error { + return nil + } +} + +func getEnvListFromMap(env map[string]string) []string { + envList := make([]string, 0) + for k, v := range env { + envList = append(envList, fmt.Sprintf("%s=%s", k, v)) + } + return envList +} + +func (e *HostEnvironment) exec(ctx context.Context, commandparam []string, cmdline string, env map[string]string, user, workdir string) error { + envList := getEnvListFromMap(env) + var wd string + if workdir != "" { + if filepath.IsAbs(workdir) { + wd = workdir + } else { + wd = filepath.Join(e.Path, workdir) + } + } else { + wd = e.Path + } + + if _, err := os.Stat(wd); err != nil { + common.Logger(ctx).Debugf("Failed to stat working directory %s %v\n", wd, err.Error()) + } + + command := make([]string, len(commandparam)) + copy(command, commandparam) + + if e.GetLXC() { + if user == "root" { + command = append([]string{"/usr/bin/sudo"}, command...) + } else { + common.Logger(ctx).Debugf("lxc-attach --name %v %v", e.Name, command) + command = append([]string{"/usr/bin/sudo", "--preserve-env", "--preserve-env=PATH", "/usr/bin/lxc-attach", "--keep-env", "--name", e.Name, "--"}, command...) + } + } + + f, err := lookupPathHost(command[0], env, e.StdOut) + if err != nil { + return err + } + cmd := exec.CommandContext(ctx, f) + cmd.Path = f + cmd.Args = command + cmd.Stdin = nil + cmd.Stdout = e.StdOut + cmd.Env = envList + cmd.Stderr = e.StdOut + cmd.Dir = wd + cmd.SysProcAttr = getSysProcAttr(cmdline, false) + var ppty *os.File + var tty *os.File + defer func() { + if ppty != nil { + ppty.Close() + } + if tty != nil { + tty.Close() + } + }() + if true /* allocate Terminal */ { + var err error + ppty, tty, err = setupPty(cmd, cmdline) + if err != nil { + common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error()) + } + } + writer := &ptyWriter{Out: e.StdOut} + logctx, finishLog := context.WithCancel(context.Background()) + if ppty != nil { + go copyPtyOutput(writer, ppty, finishLog) + } else { + finishLog() + } + if ppty != nil { + go writeKeepAlive(ppty) + } + err = cmd.Run() + if err != nil { + return fmt.Errorf("RUN %w", err) + } + if tty != nil { + writer.AutoStop = true + if _, err := tty.Write([]byte("\x04")); err != nil { + common.Logger(ctx).Debug("Failed to write EOT") + } + } + <-logctx.Done() + + if ppty != nil { + ppty.Close() + ppty = nil + } + return err +} + +func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor { + return e.ExecWithCmdLine(command, "", env, user, workdir) +} + +func (e *HostEnvironment) ExecWithCmdLine(command []string, cmdline string, env map[string]string, user, workdir string) common.Executor { + return func(ctx context.Context) error { + if err := e.exec(ctx, command, cmdline, env, user, workdir); err != nil { + select { + case <-ctx.Done(): + return fmt.Errorf("this step has been cancelled: %w", err) + default: + return err + } + } + return nil + } +} + +func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor { + return parseEnvFile(e, srcPath, env) +} + +func (e *HostEnvironment) Remove() common.Executor { + return func(ctx context.Context) error { + if e.CleanUp != nil { + e.CleanUp() + } + return os.RemoveAll(e.Path) + } +} + +func (e *HostEnvironment) ToContainerPath(path string) string { + if bp, err := filepath.Rel(e.Workdir, path); err != nil { + return filepath.Join(e.Path, bp) + } else if filepath.Clean(e.Workdir) == filepath.Clean(path) { + return e.Path + } + return path +} + +func (e *HostEnvironment) GetLXC() bool { + return e.LXC +} + +func (e *HostEnvironment) GetName() string { + return e.Name +} + +func (e *HostEnvironment) GetRoot() string { + return e.Root +} + +func (e *HostEnvironment) GetActPath() string { + actPath := e.ActPath + if runtime.GOOS == "windows" { + actPath = strings.ReplaceAll(actPath, "\\", "/") + } + return actPath +} + +func (*HostEnvironment) GetPathVariableName() string { + if runtime.GOOS == "plan9" { + return "path" + } else if runtime.GOOS == "windows" { + return "Path" // Actually we need a case insensitive map + } + return "PATH" +} + +func (e *HostEnvironment) DefaultPathVariable() string { + v, _ := os.LookupEnv(e.GetPathVariableName()) + return v +} + +func (*HostEnvironment) JoinPathVariable(paths ...string) string { + return strings.Join(paths, string(filepath.ListSeparator)) +} + +// Reference for Arch values for runner.arch +// https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context +func goArchToActionArch(arch string) string { + archMapper := map[string]string{ + "x86_64": "X64", + "386": "X86", + "aarch64": "ARM64", + } + if arch, ok := archMapper[arch]; ok { + return arch + } + return arch +} + +func goOsToActionOs(os string) string { + osMapper := map[string]string{ + "darwin": "macOS", + } + if os, ok := osMapper[os]; ok { + return os + } + return os +} + +func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interface{} { + return map[string]interface{}{ + "os": goOsToActionOs(runtime.GOOS), + "arch": goArchToActionArch(runtime.GOARCH), + "temp": e.TmpDir, + "tool_cache": e.ToolCache, + } +} + +func (e *HostEnvironment) ReplaceLogWriter(stdout, _ io.Writer) (io.Writer, io.Writer) { + org := e.StdOut + e.StdOut = stdout + return org, org +} + +func (*HostEnvironment) IsEnvironmentCaseInsensitive() bool { + return runtime.GOOS == "windows" +} |