github.com/nektos/act@v0.2.63-0.20240520024548-8acde99bfa9c/pkg/container/docker_run.go (about)

     1  //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
     2  
     3  package container
     4  
     5  import (
     6  	"archive/tar"
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/Masterminds/semver"
    20  	"github.com/docker/cli/cli/connhelper"
    21  	"github.com/docker/docker/api/types"
    22  	"github.com/docker/docker/api/types/container"
    23  	"github.com/docker/docker/api/types/mount"
    24  	"github.com/docker/docker/api/types/network"
    25  	"github.com/docker/docker/api/types/system"
    26  	"github.com/docker/docker/client"
    27  	"github.com/docker/docker/pkg/stdcopy"
    28  	"github.com/go-git/go-billy/v5/helper/polyfill"
    29  	"github.com/go-git/go-billy/v5/osfs"
    30  	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
    31  	"github.com/imdario/mergo"
    32  	"github.com/joho/godotenv"
    33  	"github.com/kballard/go-shellquote"
    34  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    35  	"github.com/spf13/pflag"
    36  	"golang.org/x/term"
    37  
    38  	"github.com/nektos/act/pkg/common"
    39  	"github.com/nektos/act/pkg/filecollector"
    40  )
    41  
    42  // NewContainer creates a reference to a container
    43  func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
    44  	cr := new(containerReference)
    45  	cr.input = input
    46  	return cr
    47  }
    48  
    49  // supportsContainerImagePlatform returns true if the underlying Docker server
    50  // API version is 1.41 and beyond
    51  func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool {
    52  	logger := common.Logger(ctx)
    53  	ver, err := cli.ServerVersion(ctx)
    54  	if err != nil {
    55  		logger.Panicf("Failed to get Docker API Version: %s", err)
    56  		return false
    57  	}
    58  	sv, err := semver.NewVersion(ver.APIVersion)
    59  	if err != nil {
    60  		logger.Panicf("Failed to unmarshal Docker Version: %s", err)
    61  		return false
    62  	}
    63  	constraint, _ := semver.NewConstraint(">= 1.41")
    64  	return constraint.Check(sv)
    65  }
    66  
    67  func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor {
    68  	return common.
    69  		NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
    70  		Then(
    71  			common.NewPipelineExecutor(
    72  				cr.connect(),
    73  				cr.find(),
    74  				cr.create(capAdd, capDrop),
    75  			).IfNot(common.Dryrun),
    76  		)
    77  }
    78  
    79  func (cr *containerReference) Start(attach bool) common.Executor {
    80  	return common.
    81  		NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
    82  		Then(
    83  			common.NewPipelineExecutor(
    84  				cr.connect(),
    85  				cr.find(),
    86  				cr.attach().IfBool(attach),
    87  				cr.start(),
    88  				cr.wait().IfBool(attach),
    89  				cr.tryReadUID(),
    90  				cr.tryReadGID(),
    91  				func(ctx context.Context) error {
    92  					// If this fails, then folders have wrong permissions on non root container
    93  					if cr.UID != 0 || cr.GID != 0 {
    94  						_ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), cr.input.WorkingDir}, nil, "0", "")(ctx)
    95  					}
    96  					return nil
    97  				},
    98  			).IfNot(common.Dryrun),
    99  		)
   100  }
   101  
   102  func (cr *containerReference) Pull(forcePull bool) common.Executor {
   103  	return common.
   104  		NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
   105  		Then(
   106  			NewDockerPullExecutor(NewDockerPullExecutorInput{
   107  				Image:     cr.input.Image,
   108  				ForcePull: forcePull,
   109  				Platform:  cr.input.Platform,
   110  				Username:  cr.input.Username,
   111  				Password:  cr.input.Password,
   112  			}),
   113  		)
   114  }
   115  
   116  func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor {
   117  	return common.NewPipelineExecutor(
   118  		cr.connect(),
   119  		cr.find(),
   120  		cr.copyContent(destPath, files...),
   121  	).IfNot(common.Dryrun)
   122  }
   123  
   124  func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
   125  	return common.NewPipelineExecutor(
   126  		common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
   127  		cr.copyDir(destPath, srcPath, useGitIgnore),
   128  		func(ctx context.Context) error {
   129  			// If this fails, then folders have wrong permissions on non root container
   130  			if cr.UID != 0 || cr.GID != 0 {
   131  				_ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx)
   132  			}
   133  			return nil
   134  		},
   135  	).IfNot(common.Dryrun)
   136  }
   137  
   138  func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
   139  	if common.Dryrun(ctx) {
   140  		return nil, fmt.Errorf("DRYRUN is not supported in GetContainerArchive")
   141  	}
   142  	a, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath)
   143  	return a, err
   144  }
   145  
   146  func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
   147  	return parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun)
   148  }
   149  
   150  func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor {
   151  	return cr.extractFromImageEnv(env).IfNot(common.Dryrun)
   152  }
   153  
   154  func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
   155  	return common.NewPipelineExecutor(
   156  		common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
   157  		cr.connect(),
   158  		cr.find(),
   159  		cr.exec(command, env, user, workdir),
   160  	).IfNot(common.Dryrun)
   161  }
   162  
   163  func (cr *containerReference) Remove() common.Executor {
   164  	return common.NewPipelineExecutor(
   165  		cr.connect(),
   166  		cr.find(),
   167  	).Finally(
   168  		cr.remove(),
   169  	).IfNot(common.Dryrun)
   170  }
   171  
   172  func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
   173  	out := cr.input.Stdout
   174  	err := cr.input.Stderr
   175  
   176  	cr.input.Stdout = stdout
   177  	cr.input.Stderr = stderr
   178  
   179  	return out, err
   180  }
   181  
   182  type containerReference struct {
   183  	cli   client.APIClient
   184  	id    string
   185  	input *NewContainerInput
   186  	UID   int
   187  	GID   int
   188  	LinuxContainerEnvironmentExtensions
   189  }
   190  
   191  func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) {
   192  	dockerHost := os.Getenv("DOCKER_HOST")
   193  
   194  	if strings.HasPrefix(dockerHost, "ssh://") {
   195  		var helper *connhelper.ConnectionHelper
   196  
   197  		helper, err = connhelper.GetConnectionHelper(dockerHost)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  		cli, err = client.NewClientWithOpts(
   202  			client.WithHost(helper.Host),
   203  			client.WithDialContext(helper.Dialer),
   204  		)
   205  	} else {
   206  		cli, err = client.NewClientWithOpts(client.FromEnv)
   207  	}
   208  	if err != nil {
   209  		return nil, fmt.Errorf("failed to connect to docker daemon: %w", err)
   210  	}
   211  	cli.NegotiateAPIVersion(ctx)
   212  
   213  	return cli, nil
   214  }
   215  
   216  func GetHostInfo(ctx context.Context) (info system.Info, err error) {
   217  	var cli client.APIClient
   218  	cli, err = GetDockerClient(ctx)
   219  	if err != nil {
   220  		return info, err
   221  	}
   222  	defer cli.Close()
   223  
   224  	info, err = cli.Info(ctx)
   225  	if err != nil {
   226  		return info, err
   227  	}
   228  
   229  	return info, nil
   230  }
   231  
   232  // Arch fetches values from docker info and translates architecture to
   233  // GitHub actions compatible runner.arch values
   234  // https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md
   235  func RunnerArch(ctx context.Context) string {
   236  	info, err := GetHostInfo(ctx)
   237  	if err != nil {
   238  		return ""
   239  	}
   240  
   241  	archMapper := map[string]string{
   242  		"x86_64":  "X64",
   243  		"amd64":   "X64",
   244  		"386":     "X86",
   245  		"aarch64": "ARM64",
   246  		"arm64":   "ARM64",
   247  	}
   248  	if arch, ok := archMapper[info.Architecture]; ok {
   249  		return arch
   250  	}
   251  	return info.Architecture
   252  }
   253  
   254  func (cr *containerReference) connect() common.Executor {
   255  	return func(ctx context.Context) error {
   256  		if cr.cli != nil {
   257  			return nil
   258  		}
   259  		cli, err := GetDockerClient(ctx)
   260  		if err != nil {
   261  			return err
   262  		}
   263  		cr.cli = cli
   264  		return nil
   265  	}
   266  }
   267  
   268  func (cr *containerReference) Close() common.Executor {
   269  	return func(ctx context.Context) error {
   270  		if cr.cli != nil {
   271  			err := cr.cli.Close()
   272  			cr.cli = nil
   273  			if err != nil {
   274  				return fmt.Errorf("failed to close client: %w", err)
   275  			}
   276  		}
   277  		return nil
   278  	}
   279  }
   280  
   281  func (cr *containerReference) find() common.Executor {
   282  	return func(ctx context.Context) error {
   283  		if cr.id != "" {
   284  			return nil
   285  		}
   286  		containers, err := cr.cli.ContainerList(ctx, container.ListOptions{
   287  			All: true,
   288  		})
   289  		if err != nil {
   290  			return fmt.Errorf("failed to list containers: %w", err)
   291  		}
   292  
   293  		for _, c := range containers {
   294  			for _, name := range c.Names {
   295  				if name[1:] == cr.input.Name {
   296  					cr.id = c.ID
   297  					return nil
   298  				}
   299  			}
   300  		}
   301  
   302  		cr.id = ""
   303  		return nil
   304  	}
   305  }
   306  
   307  func (cr *containerReference) remove() common.Executor {
   308  	return func(ctx context.Context) error {
   309  		if cr.id == "" {
   310  			return nil
   311  		}
   312  
   313  		logger := common.Logger(ctx)
   314  		err := cr.cli.ContainerRemove(ctx, cr.id, container.RemoveOptions{
   315  			RemoveVolumes: true,
   316  			Force:         true,
   317  		})
   318  		if err != nil {
   319  			logger.Error(fmt.Errorf("failed to remove container: %w", err))
   320  		}
   321  
   322  		logger.Debugf("Removed container: %v", cr.id)
   323  		cr.id = ""
   324  		return nil
   325  	}
   326  }
   327  
   328  func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config *container.Config, hostConfig *container.HostConfig) (*container.Config, *container.HostConfig, error) {
   329  	logger := common.Logger(ctx)
   330  	input := cr.input
   331  
   332  	if input.Options == "" {
   333  		return config, hostConfig, nil
   334  	}
   335  
   336  	// parse configuration from CLI container.options
   337  	flags := pflag.NewFlagSet("container_flags", pflag.ContinueOnError)
   338  	copts := addFlags(flags)
   339  
   340  	optionsArgs, err := shellquote.Split(input.Options)
   341  	if err != nil {
   342  		return nil, nil, fmt.Errorf("Cannot split container options: '%s': '%w'", input.Options, err)
   343  	}
   344  
   345  	err = flags.Parse(optionsArgs)
   346  	if err != nil {
   347  		return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err)
   348  	}
   349  
   350  	if len(copts.netMode.Value()) == 0 {
   351  		if err = copts.netMode.Set(cr.input.NetworkMode); err != nil {
   352  			return nil, nil, fmt.Errorf("Cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err)
   353  		}
   354  	}
   355  
   356  	containerConfig, err := parse(flags, copts, runtime.GOOS)
   357  	if err != nil {
   358  		return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err)
   359  	}
   360  
   361  	logger.Debugf("Custom container.Config from options ==> %+v", containerConfig.Config)
   362  
   363  	err = mergo.Merge(config, containerConfig.Config, mergo.WithOverride)
   364  	if err != nil {
   365  		return nil, nil, fmt.Errorf("Cannot merge container.Config options: '%s': '%w'", input.Options, err)
   366  	}
   367  	logger.Debugf("Merged container.Config ==> %+v", config)
   368  
   369  	logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
   370  
   371  	hostConfig.Binds = append(hostConfig.Binds, containerConfig.HostConfig.Binds...)
   372  	hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...)
   373  	binds := hostConfig.Binds
   374  	mounts := hostConfig.Mounts
   375  	err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride)
   376  	if err != nil {
   377  		return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err)
   378  	}
   379  	hostConfig.Binds = binds
   380  	hostConfig.Mounts = mounts
   381  	logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
   382  
   383  	return config, hostConfig, nil
   384  }
   385  
   386  func (cr *containerReference) create(capAdd []string, capDrop []string) common.Executor {
   387  	return func(ctx context.Context) error {
   388  		if cr.id != "" {
   389  			return nil
   390  		}
   391  		logger := common.Logger(ctx)
   392  		isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
   393  		input := cr.input
   394  
   395  		config := &container.Config{
   396  			Image:        input.Image,
   397  			WorkingDir:   input.WorkingDir,
   398  			Env:          input.Env,
   399  			ExposedPorts: input.ExposedPorts,
   400  			Tty:          isTerminal,
   401  		}
   402  		logger.Debugf("Common container.Config ==> %+v", config)
   403  
   404  		if len(input.Cmd) != 0 {
   405  			config.Cmd = input.Cmd
   406  		}
   407  
   408  		if len(input.Entrypoint) != 0 {
   409  			config.Entrypoint = input.Entrypoint
   410  		}
   411  
   412  		mounts := make([]mount.Mount, 0)
   413  		for mountSource, mountTarget := range input.Mounts {
   414  			mounts = append(mounts, mount.Mount{
   415  				Type:   mount.TypeVolume,
   416  				Source: mountSource,
   417  				Target: mountTarget,
   418  			})
   419  		}
   420  
   421  		var platSpecs *specs.Platform
   422  		if supportsContainerImagePlatform(ctx, cr.cli) && cr.input.Platform != "" {
   423  			desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2)
   424  
   425  			if len(desiredPlatform) != 2 {
   426  				return fmt.Errorf("incorrect container platform option '%s'", cr.input.Platform)
   427  			}
   428  
   429  			platSpecs = &specs.Platform{
   430  				Architecture: desiredPlatform[1],
   431  				OS:           desiredPlatform[0],
   432  			}
   433  		}
   434  
   435  		hostConfig := &container.HostConfig{
   436  			CapAdd:       capAdd,
   437  			CapDrop:      capDrop,
   438  			Binds:        input.Binds,
   439  			Mounts:       mounts,
   440  			NetworkMode:  container.NetworkMode(input.NetworkMode),
   441  			Privileged:   input.Privileged,
   442  			UsernsMode:   container.UsernsMode(input.UsernsMode),
   443  			PortBindings: input.PortBindings,
   444  		}
   445  		logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
   446  
   447  		config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig)
   448  		if err != nil {
   449  			return err
   450  		}
   451  
   452  		var networkingConfig *network.NetworkingConfig
   453  		logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases)
   454  		n := hostConfig.NetworkMode
   455  		// IsUserDefined and IsHost are broken on windows
   456  		if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 {
   457  			endpointConfig := &network.EndpointSettings{
   458  				Aliases: input.NetworkAliases,
   459  			}
   460  			networkingConfig = &network.NetworkingConfig{
   461  				EndpointsConfig: map[string]*network.EndpointSettings{
   462  					input.NetworkMode: endpointConfig,
   463  				},
   464  			}
   465  		}
   466  
   467  		resp, err := cr.cli.ContainerCreate(ctx, config, hostConfig, networkingConfig, platSpecs, input.Name)
   468  		if err != nil {
   469  			return fmt.Errorf("failed to create container: '%w'", err)
   470  		}
   471  
   472  		logger.Debugf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, resp.ID, input.Image, input.Platform)
   473  		logger.Debugf("ENV ==> %v", input.Env)
   474  
   475  		cr.id = resp.ID
   476  		return nil
   477  	}
   478  }
   479  
   480  func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor {
   481  	envMap := *env
   482  	return func(ctx context.Context) error {
   483  		logger := common.Logger(ctx)
   484  
   485  		inspect, _, err := cr.cli.ImageInspectWithRaw(ctx, cr.input.Image)
   486  		if err != nil {
   487  			logger.Error(err)
   488  			return fmt.Errorf("inspect image: %w", err)
   489  		}
   490  
   491  		if inspect.Config == nil {
   492  			return nil
   493  		}
   494  
   495  		imageEnv, err := godotenv.Unmarshal(strings.Join(inspect.Config.Env, "\n"))
   496  		if err != nil {
   497  			logger.Error(err)
   498  			return fmt.Errorf("unmarshal image env: %w", err)
   499  		}
   500  
   501  		for k, v := range imageEnv {
   502  			if k == "PATH" {
   503  				if envMap[k] == "" {
   504  					envMap[k] = v
   505  				} else {
   506  					envMap[k] += `:` + v
   507  				}
   508  			} else if envMap[k] == "" {
   509  				envMap[k] = v
   510  			}
   511  		}
   512  
   513  		env = &envMap
   514  		return nil
   515  	}
   516  }
   517  
   518  func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
   519  	return func(ctx context.Context) error {
   520  		logger := common.Logger(ctx)
   521  		// Fix slashes when running on Windows
   522  		if runtime.GOOS == "windows" {
   523  			var newCmd []string
   524  			for _, v := range cmd {
   525  				newCmd = append(newCmd, strings.ReplaceAll(v, `\`, `/`))
   526  			}
   527  			cmd = newCmd
   528  		}
   529  
   530  		logger.Debugf("Exec command '%s'", cmd)
   531  		isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
   532  		envList := make([]string, 0)
   533  		for k, v := range env {
   534  			envList = append(envList, fmt.Sprintf("%s=%s", k, v))
   535  		}
   536  
   537  		var wd string
   538  		if workdir != "" {
   539  			if strings.HasPrefix(workdir, "/") {
   540  				wd = workdir
   541  			} else {
   542  				wd = fmt.Sprintf("%s/%s", cr.input.WorkingDir, workdir)
   543  			}
   544  		} else {
   545  			wd = cr.input.WorkingDir
   546  		}
   547  		logger.Debugf("Working directory '%s'", wd)
   548  
   549  		idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
   550  			User:         user,
   551  			Cmd:          cmd,
   552  			WorkingDir:   wd,
   553  			Env:          envList,
   554  			Tty:          isTerminal,
   555  			AttachStderr: true,
   556  			AttachStdout: true,
   557  		})
   558  		if err != nil {
   559  			return fmt.Errorf("failed to create exec: %w", err)
   560  		}
   561  
   562  		resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{
   563  			Tty: isTerminal,
   564  		})
   565  		if err != nil {
   566  			return fmt.Errorf("failed to attach to exec: %w", err)
   567  		}
   568  		defer resp.Close()
   569  
   570  		err = cr.waitForCommand(ctx, isTerminal, resp, idResp, user, workdir)
   571  		if err != nil {
   572  			return err
   573  		}
   574  
   575  		inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID)
   576  		if err != nil {
   577  			return fmt.Errorf("failed to inspect exec: %w", err)
   578  		}
   579  
   580  		switch inspectResp.ExitCode {
   581  		case 0:
   582  			return nil
   583  		case 127:
   584  			return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode)
   585  		default:
   586  			return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
   587  		}
   588  	}
   589  }
   590  
   591  func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor {
   592  	return func(ctx context.Context) error {
   593  		idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
   594  			Cmd:          []string{"id", opt},
   595  			AttachStdout: true,
   596  			AttachStderr: true,
   597  		})
   598  		if err != nil {
   599  			return nil
   600  		}
   601  
   602  		resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{})
   603  		if err != nil {
   604  			return nil
   605  		}
   606  		defer resp.Close()
   607  
   608  		sid, err := resp.Reader.ReadString('\n')
   609  		if err != nil {
   610  			return nil
   611  		}
   612  		exp := regexp.MustCompile(`\d+\n`)
   613  		found := exp.FindString(sid)
   614  		id, err := strconv.ParseInt(strings.TrimSpace(found), 10, 32)
   615  		if err != nil {
   616  			return nil
   617  		}
   618  		cbk(int(id))
   619  
   620  		return nil
   621  	}
   622  }
   623  
   624  func (cr *containerReference) tryReadUID() common.Executor {
   625  	return cr.tryReadID("-u", func(id int) { cr.UID = id })
   626  }
   627  
   628  func (cr *containerReference) tryReadGID() common.Executor {
   629  	return cr.tryReadID("-g", func(id int) { cr.GID = id })
   630  }
   631  
   632  func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ types.IDResponse, _ string, _ string) error {
   633  	logger := common.Logger(ctx)
   634  
   635  	cmdResponse := make(chan error)
   636  
   637  	go func() {
   638  		var outWriter io.Writer
   639  		outWriter = cr.input.Stdout
   640  		if outWriter == nil {
   641  			outWriter = os.Stdout
   642  		}
   643  		errWriter := cr.input.Stderr
   644  		if errWriter == nil {
   645  			errWriter = os.Stderr
   646  		}
   647  
   648  		var err error
   649  		if !isTerminal || os.Getenv("NORAW") != "" {
   650  			_, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader)
   651  		} else {
   652  			_, err = io.Copy(outWriter, resp.Reader)
   653  		}
   654  		cmdResponse <- err
   655  	}()
   656  
   657  	select {
   658  	case <-ctx.Done():
   659  		// send ctrl + c
   660  		_, err := resp.Conn.Write([]byte{3})
   661  		if err != nil {
   662  			logger.Warnf("Failed to send CTRL+C: %+s", err)
   663  		}
   664  
   665  		// we return the context canceled error to prevent other steps
   666  		// from executing
   667  		return ctx.Err()
   668  	case err := <-cmdResponse:
   669  		if err != nil {
   670  			logger.Error(err)
   671  		}
   672  
   673  		return nil
   674  	}
   675  }
   676  
   677  func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
   678  	// Mkdir
   679  	buf := &bytes.Buffer{}
   680  	tw := tar.NewWriter(buf)
   681  	_ = tw.WriteHeader(&tar.Header{
   682  		Name:     destPath,
   683  		Mode:     0o777,
   684  		Typeflag: tar.TypeDir,
   685  	})
   686  	tw.Close()
   687  	err := cr.cli.CopyToContainer(ctx, cr.id, "/", buf, types.CopyToContainerOptions{})
   688  	if err != nil {
   689  		return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
   690  	}
   691  	// Copy Content
   692  	err = cr.cli.CopyToContainer(ctx, cr.id, destPath, tarStream, types.CopyToContainerOptions{})
   693  	if err != nil {
   694  		return fmt.Errorf("failed to copy content to container: %w", err)
   695  	}
   696  	// If this fails, then folders have wrong permissions on non root container
   697  	if cr.UID != 0 || cr.GID != 0 {
   698  		_ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx)
   699  	}
   700  	return nil
   701  }
   702  
   703  func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgnore bool) common.Executor {
   704  	return func(ctx context.Context) error {
   705  		logger := common.Logger(ctx)
   706  		tarFile, err := os.CreateTemp("", "act")
   707  		if err != nil {
   708  			return err
   709  		}
   710  		logger.Debugf("Writing tarball %s from %s", tarFile.Name(), srcPath)
   711  		defer func(tarFile *os.File) {
   712  			name := tarFile.Name()
   713  			err := tarFile.Close()
   714  			if !errors.Is(err, os.ErrClosed) {
   715  				logger.Error(err)
   716  			}
   717  			err = os.Remove(name)
   718  			if err != nil {
   719  				logger.Error(err)
   720  			}
   721  		}(tarFile)
   722  		tw := tar.NewWriter(tarFile)
   723  
   724  		srcPrefix := filepath.Dir(srcPath)
   725  		if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
   726  			srcPrefix += string(filepath.Separator)
   727  		}
   728  		logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
   729  
   730  		var ignorer gitignore.Matcher
   731  		if useGitIgnore {
   732  			ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
   733  			if err != nil {
   734  				logger.Debugf("Error loading .gitignore: %v", err)
   735  			}
   736  
   737  			ignorer = gitignore.NewMatcher(ps)
   738  		}
   739  
   740  		fc := &filecollector.FileCollector{
   741  			Fs:        &filecollector.DefaultFs{},
   742  			Ignorer:   ignorer,
   743  			SrcPath:   srcPath,
   744  			SrcPrefix: srcPrefix,
   745  			Handler: &filecollector.TarCollector{
   746  				TarWriter: tw,
   747  				UID:       cr.UID,
   748  				GID:       cr.GID,
   749  				DstDir:    dstPath[1:],
   750  			},
   751  		}
   752  
   753  		err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
   754  		if err != nil {
   755  			return err
   756  		}
   757  		if err := tw.Close(); err != nil {
   758  			return err
   759  		}
   760  
   761  		logger.Debugf("Extracting content from '%s' to '%s'", tarFile.Name(), dstPath)
   762  		_, err = tarFile.Seek(0, 0)
   763  		if err != nil {
   764  			return fmt.Errorf("failed to seek tar archive: %w", err)
   765  		}
   766  		err = cr.cli.CopyToContainer(ctx, cr.id, "/", tarFile, types.CopyToContainerOptions{})
   767  		if err != nil {
   768  			return fmt.Errorf("failed to copy content to container: %w", err)
   769  		}
   770  		return nil
   771  	}
   772  }
   773  
   774  func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor {
   775  	return func(ctx context.Context) error {
   776  		logger := common.Logger(ctx)
   777  		var buf bytes.Buffer
   778  		tw := tar.NewWriter(&buf)
   779  		for _, file := range files {
   780  			logger.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body))
   781  			hdr := &tar.Header{
   782  				Name: file.Name,
   783  				Mode: file.Mode,
   784  				Size: int64(len(file.Body)),
   785  				Uid:  cr.UID,
   786  				Gid:  cr.GID,
   787  			}
   788  			if err := tw.WriteHeader(hdr); err != nil {
   789  				return err
   790  			}
   791  			if _, err := tw.Write([]byte(file.Body)); err != nil {
   792  				return err
   793  			}
   794  		}
   795  		if err := tw.Close(); err != nil {
   796  			return err
   797  		}
   798  
   799  		logger.Debugf("Extracting content to '%s'", dstPath)
   800  		err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{})
   801  		if err != nil {
   802  			return fmt.Errorf("failed to copy content to container: %w", err)
   803  		}
   804  		return nil
   805  	}
   806  }
   807  
   808  func (cr *containerReference) attach() common.Executor {
   809  	return func(ctx context.Context) error {
   810  		out, err := cr.cli.ContainerAttach(ctx, cr.id, container.AttachOptions{
   811  			Stream: true,
   812  			Stdout: true,
   813  			Stderr: true,
   814  		})
   815  		if err != nil {
   816  			return fmt.Errorf("failed to attach to container: %w", err)
   817  		}
   818  		isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
   819  
   820  		var outWriter io.Writer
   821  		outWriter = cr.input.Stdout
   822  		if outWriter == nil {
   823  			outWriter = os.Stdout
   824  		}
   825  		errWriter := cr.input.Stderr
   826  		if errWriter == nil {
   827  			errWriter = os.Stderr
   828  		}
   829  		go func() {
   830  			if !isTerminal || os.Getenv("NORAW") != "" {
   831  				_, err = stdcopy.StdCopy(outWriter, errWriter, out.Reader)
   832  			} else {
   833  				_, err = io.Copy(outWriter, out.Reader)
   834  			}
   835  			if err != nil {
   836  				common.Logger(ctx).Error(err)
   837  			}
   838  		}()
   839  		return nil
   840  	}
   841  }
   842  
   843  func (cr *containerReference) start() common.Executor {
   844  	return func(ctx context.Context) error {
   845  		logger := common.Logger(ctx)
   846  		logger.Debugf("Starting container: %v", cr.id)
   847  
   848  		if err := cr.cli.ContainerStart(ctx, cr.id, container.StartOptions{}); err != nil {
   849  			return fmt.Errorf("failed to start container: %w", err)
   850  		}
   851  
   852  		logger.Debugf("Started container: %v", cr.id)
   853  		return nil
   854  	}
   855  }
   856  
   857  func (cr *containerReference) wait() common.Executor {
   858  	return func(ctx context.Context) error {
   859  		logger := common.Logger(ctx)
   860  		statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning)
   861  		var statusCode int64
   862  		select {
   863  		case err := <-errCh:
   864  			if err != nil {
   865  				return fmt.Errorf("failed to wait for container: %w", err)
   866  			}
   867  		case status := <-statusCh:
   868  			statusCode = status.StatusCode
   869  		}
   870  
   871  		logger.Debugf("Return status: %v", statusCode)
   872  
   873  		if statusCode == 0 {
   874  			return nil
   875  		}
   876  
   877  		return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
   878  	}
   879  }