github.com/nektos/act@v0.2.63/pkg/container/host_environment.go (about)

     1  package container
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/go-git/go-billy/v5/helper/polyfill"
    19  	"github.com/go-git/go-billy/v5/osfs"
    20  	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
    21  	"golang.org/x/term"
    22  
    23  	"github.com/nektos/act/pkg/common"
    24  	"github.com/nektos/act/pkg/filecollector"
    25  	"github.com/nektos/act/pkg/lookpath"
    26  )
    27  
    28  type HostEnvironment struct {
    29  	Path      string
    30  	TmpDir    string
    31  	ToolCache string
    32  	Workdir   string
    33  	ActPath   string
    34  	CleanUp   func()
    35  	StdOut    io.Writer
    36  }
    37  
    38  func (e *HostEnvironment) Create(_ []string, _ []string) common.Executor {
    39  	return func(ctx context.Context) error {
    40  		return nil
    41  	}
    42  }
    43  
    44  func (e *HostEnvironment) Close() common.Executor {
    45  	return func(ctx context.Context) error {
    46  		return nil
    47  	}
    48  }
    49  
    50  func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor {
    51  	return func(ctx context.Context) error {
    52  		for _, f := range files {
    53  			if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil {
    54  				return err
    55  			}
    56  			if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil {
    57  				return err
    58  			}
    59  		}
    60  		return nil
    61  	}
    62  }
    63  
    64  func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
    65  	if err := os.RemoveAll(destPath); err != nil {
    66  		return err
    67  	}
    68  	tr := tar.NewReader(tarStream)
    69  	cp := &filecollector.CopyCollector{
    70  		DstDir: destPath,
    71  	}
    72  	for {
    73  		ti, err := tr.Next()
    74  		if errors.Is(err, io.EOF) {
    75  			return nil
    76  		} else if err != nil {
    77  			return err
    78  		}
    79  		if ti.FileInfo().IsDir() {
    80  			continue
    81  		}
    82  		if ctx.Err() != nil {
    83  			return fmt.Errorf("CopyTarStream has been cancelled")
    84  		}
    85  		if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil {
    86  			return err
    87  		}
    88  	}
    89  }
    90  
    91  func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
    92  	return func(ctx context.Context) error {
    93  		logger := common.Logger(ctx)
    94  		srcPrefix := filepath.Dir(srcPath)
    95  		if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
    96  			srcPrefix += string(filepath.Separator)
    97  		}
    98  		logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
    99  		var ignorer gitignore.Matcher
   100  		if useGitIgnore {
   101  			ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
   102  			if err != nil {
   103  				logger.Debugf("Error loading .gitignore: %v", err)
   104  			}
   105  
   106  			ignorer = gitignore.NewMatcher(ps)
   107  		}
   108  		fc := &filecollector.FileCollector{
   109  			Fs:        &filecollector.DefaultFs{},
   110  			Ignorer:   ignorer,
   111  			SrcPath:   srcPath,
   112  			SrcPrefix: srcPrefix,
   113  			Handler: &filecollector.CopyCollector{
   114  				DstDir: destPath,
   115  			},
   116  		}
   117  		return filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
   118  	}
   119  }
   120  
   121  func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
   122  	buf := &bytes.Buffer{}
   123  	tw := tar.NewWriter(buf)
   124  	defer tw.Close()
   125  	srcPath = filepath.Clean(srcPath)
   126  	fi, err := os.Lstat(srcPath)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	tc := &filecollector.TarCollector{
   131  		TarWriter: tw,
   132  	}
   133  	if fi.IsDir() {
   134  		srcPrefix := srcPath
   135  		if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
   136  			srcPrefix += string(filepath.Separator)
   137  		}
   138  		fc := &filecollector.FileCollector{
   139  			Fs:        &filecollector.DefaultFs{},
   140  			SrcPath:   srcPath,
   141  			SrcPrefix: srcPrefix,
   142  			Handler:   tc,
   143  		}
   144  		err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
   145  		if err != nil {
   146  			return nil, err
   147  		}
   148  	} else {
   149  		var f io.ReadCloser
   150  		var linkname string
   151  		if fi.Mode()&fs.ModeSymlink != 0 {
   152  			linkname, err = os.Readlink(srcPath)
   153  			if err != nil {
   154  				return nil, err
   155  			}
   156  		} else {
   157  			f, err = os.Open(srcPath)
   158  			if err != nil {
   159  				return nil, err
   160  			}
   161  			defer f.Close()
   162  		}
   163  		err := tc.WriteFile(fi.Name(), fi, linkname, f)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  	}
   168  	return io.NopCloser(buf), nil
   169  }
   170  
   171  func (e *HostEnvironment) Pull(_ bool) common.Executor {
   172  	return func(ctx context.Context) error {
   173  		return nil
   174  	}
   175  }
   176  
   177  func (e *HostEnvironment) Start(_ bool) common.Executor {
   178  	return func(ctx context.Context) error {
   179  		return nil
   180  	}
   181  }
   182  
   183  type ptyWriter struct {
   184  	Out       io.Writer
   185  	AutoStop  bool
   186  	dirtyLine bool
   187  }
   188  
   189  func (w *ptyWriter) Write(buf []byte) (int, error) {
   190  	if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
   191  		n, err := w.Out.Write(buf[:len(buf)-1])
   192  		if err != nil {
   193  			return n, err
   194  		}
   195  		if w.dirtyLine || len(buf) > 1 && buf[len(buf)-2] != '\n' {
   196  			_, _ = w.Out.Write([]byte("\n"))
   197  			return n, io.EOF
   198  		}
   199  		return n, io.EOF
   200  	}
   201  	w.dirtyLine = strings.LastIndex(string(buf), "\n") < len(buf)-1
   202  	return w.Out.Write(buf)
   203  }
   204  
   205  type localEnv struct {
   206  	env map[string]string
   207  }
   208  
   209  func (l *localEnv) Getenv(name string) string {
   210  	if runtime.GOOS == "windows" {
   211  		for k, v := range l.env {
   212  			if strings.EqualFold(name, k) {
   213  				return v
   214  			}
   215  		}
   216  		return ""
   217  	}
   218  	return l.env[name]
   219  }
   220  
   221  func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) {
   222  	f, err := lookpath.LookPath2(cmd, &localEnv{env: env})
   223  	if err != nil {
   224  		err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH"
   225  		if _, _err := writer.Write([]byte(err + "\n")); _err != nil {
   226  			return "", fmt.Errorf("%v: %w", err, _err)
   227  		}
   228  		return "", errors.New(err)
   229  	}
   230  	return f, nil
   231  }
   232  
   233  func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) {
   234  	ppty, tty, err := openPty()
   235  	if err != nil {
   236  		return nil, nil, err
   237  	}
   238  	if term.IsTerminal(int(tty.Fd())) {
   239  		_, err := term.MakeRaw(int(tty.Fd()))
   240  		if err != nil {
   241  			ppty.Close()
   242  			tty.Close()
   243  			return nil, nil, err
   244  		}
   245  	}
   246  	cmd.Stdin = tty
   247  	cmd.Stdout = tty
   248  	cmd.Stderr = tty
   249  	cmd.SysProcAttr = getSysProcAttr(cmdline, true)
   250  	return ppty, tty, nil
   251  }
   252  
   253  func writeKeepAlive(ppty io.Writer) {
   254  	c := 1
   255  	var err error
   256  	for c == 1 && err == nil {
   257  		c, err = ppty.Write([]byte{4})
   258  		<-time.After(time.Second)
   259  	}
   260  }
   261  
   262  func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFunc) {
   263  	defer func() {
   264  		finishLog()
   265  	}()
   266  	if _, err := io.Copy(writer, ppty); err != nil {
   267  		return
   268  	}
   269  }
   270  
   271  func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor {
   272  	return func(ctx context.Context) error {
   273  		return nil
   274  	}
   275  }
   276  
   277  func getEnvListFromMap(env map[string]string) []string {
   278  	envList := make([]string, 0)
   279  	for k, v := range env {
   280  		envList = append(envList, fmt.Sprintf("%s=%s", k, v))
   281  	}
   282  	return envList
   283  }
   284  
   285  func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, _, workdir string) error {
   286  	envList := getEnvListFromMap(env)
   287  	var wd string
   288  	if workdir != "" {
   289  		if filepath.IsAbs(workdir) {
   290  			wd = workdir
   291  		} else {
   292  			wd = filepath.Join(e.Path, workdir)
   293  		}
   294  	} else {
   295  		wd = e.Path
   296  	}
   297  	f, err := lookupPathHost(command[0], env, e.StdOut)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	cmd := exec.CommandContext(ctx, f)
   302  	cmd.Path = f
   303  	cmd.Args = command
   304  	cmd.Stdin = nil
   305  	cmd.Stdout = e.StdOut
   306  	cmd.Env = envList
   307  	cmd.Stderr = e.StdOut
   308  	cmd.Dir = wd
   309  	cmd.SysProcAttr = getSysProcAttr(cmdline, false)
   310  	var ppty *os.File
   311  	var tty *os.File
   312  	defer func() {
   313  		if ppty != nil {
   314  			ppty.Close()
   315  		}
   316  		if tty != nil {
   317  			tty.Close()
   318  		}
   319  	}()
   320  	if true /* allocate Terminal */ {
   321  		var err error
   322  		ppty, tty, err = setupPty(cmd, cmdline)
   323  		if err != nil {
   324  			common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
   325  		}
   326  	}
   327  	writer := &ptyWriter{Out: e.StdOut}
   328  	logctx, finishLog := context.WithCancel(context.Background())
   329  	if ppty != nil {
   330  		go copyPtyOutput(writer, ppty, finishLog)
   331  	} else {
   332  		finishLog()
   333  	}
   334  	if ppty != nil {
   335  		go writeKeepAlive(ppty)
   336  	}
   337  	err = cmd.Run()
   338  	if err != nil {
   339  		return err
   340  	}
   341  	if tty != nil {
   342  		writer.AutoStop = true
   343  		if _, err := tty.Write([]byte("\x04")); err != nil {
   344  			common.Logger(ctx).Debug("Failed to write EOT")
   345  		}
   346  	}
   347  	<-logctx.Done()
   348  
   349  	if ppty != nil {
   350  		ppty.Close()
   351  		ppty = nil
   352  	}
   353  	return err
   354  }
   355  
   356  func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor {
   357  	return e.ExecWithCmdLine(command, "", env, user, workdir)
   358  }
   359  
   360  func (e *HostEnvironment) ExecWithCmdLine(command []string, cmdline string, env map[string]string, user, workdir string) common.Executor {
   361  	return func(ctx context.Context) error {
   362  		if err := e.exec(ctx, command, cmdline, env, user, workdir); err != nil {
   363  			select {
   364  			case <-ctx.Done():
   365  				return fmt.Errorf("this step has been cancelled: %w", err)
   366  			default:
   367  				return err
   368  			}
   369  		}
   370  		return nil
   371  	}
   372  }
   373  
   374  func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
   375  	return parseEnvFile(e, srcPath, env)
   376  }
   377  
   378  func (e *HostEnvironment) Remove() common.Executor {
   379  	return func(ctx context.Context) error {
   380  		if e.CleanUp != nil {
   381  			e.CleanUp()
   382  		}
   383  		return os.RemoveAll(e.Path)
   384  	}
   385  }
   386  
   387  func (e *HostEnvironment) ToContainerPath(path string) string {
   388  	if bp, err := filepath.Rel(e.Workdir, path); err != nil {
   389  		return filepath.Join(e.Path, bp)
   390  	} else if filepath.Clean(e.Workdir) == filepath.Clean(path) {
   391  		return e.Path
   392  	}
   393  	return path
   394  }
   395  
   396  func (e *HostEnvironment) GetActPath() string {
   397  	actPath := e.ActPath
   398  	if runtime.GOOS == "windows" {
   399  		actPath = strings.ReplaceAll(actPath, "\\", "/")
   400  	}
   401  	return actPath
   402  }
   403  
   404  func (*HostEnvironment) GetPathVariableName() string {
   405  	if runtime.GOOS == "plan9" {
   406  		return "path"
   407  	} else if runtime.GOOS == "windows" {
   408  		return "Path" // Actually we need a case insensitive map
   409  	}
   410  	return "PATH"
   411  }
   412  
   413  func (e *HostEnvironment) DefaultPathVariable() string {
   414  	v, _ := os.LookupEnv(e.GetPathVariableName())
   415  	return v
   416  }
   417  
   418  func (*HostEnvironment) JoinPathVariable(paths ...string) string {
   419  	return strings.Join(paths, string(filepath.ListSeparator))
   420  }
   421  
   422  // Reference for Arch values for runner.arch
   423  // https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context
   424  func goArchToActionArch(arch string) string {
   425  	archMapper := map[string]string{
   426  		"x86_64":  "X64",
   427  		"386":     "X86",
   428  		"aarch64": "ARM64",
   429  	}
   430  	if arch, ok := archMapper[arch]; ok {
   431  		return arch
   432  	}
   433  	return arch
   434  }
   435  
   436  func goOsToActionOs(os string) string {
   437  	osMapper := map[string]string{
   438  		"darwin": "macOS",
   439  	}
   440  	if os, ok := osMapper[os]; ok {
   441  		return os
   442  	}
   443  	return os
   444  }
   445  
   446  func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interface{} {
   447  	return map[string]interface{}{
   448  		"os":         goOsToActionOs(runtime.GOOS),
   449  		"arch":       goArchToActionArch(runtime.GOARCH),
   450  		"temp":       e.TmpDir,
   451  		"tool_cache": e.ToolCache,
   452  	}
   453  }
   454  
   455  func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) {
   456  	org := e.StdOut
   457  	e.StdOut = stdout
   458  	return org, org
   459  }
   460  
   461  func (*HostEnvironment) IsEnvironmentCaseInsensitive() bool {
   462  	return runtime.GOOS == "windows"
   463  }