summaryrefslogtreecommitdiffstats
path: root/pkg/container/host_environment.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/container/host_environment.go')
-rw-r--r--pkg/container/host_environment.go501
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"
+}