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