package container import ( "context" "fmt" "io" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/nektos/act/pkg/common" "github.com/pkg/errors" "golang.org/x/crypto/ssh/terminal" ) // NewDockerRunExecutorInput the input for the NewDockerRunExecutor function type NewDockerRunExecutorInput struct { Image string Entrypoint []string Cmd []string WorkingDir string Env []string Binds []string Content map[string]io.Reader Volumes []string Name string ReuseContainers bool Stdout io.Writer Stderr io.Writer } // NewDockerRunExecutor function to create a run executor for the container func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor { cr := new(containerReference) cr.input = input return common. NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, input.Image, input.Entrypoint, input.Cmd). Then( common.NewPipelineExecutor( cr.connect(), cr.find(), cr.remove().IfBool(!input.ReuseContainers), cr.create(), cr.copyContent(), cr.attach(), cr.start(), cr.wait(), ).Finally( cr.remove().IfBool(!input.ReuseContainers), ).IfNot(common.Dryrun), ) } type containerReference struct { input NewDockerRunExecutorInput cli *client.Client id string } func (cr *containerReference) connect() common.Executor { return func(ctx context.Context) error { cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return errors.WithStack(err) } cli.NegotiateAPIVersion(ctx) cr.cli = cli return nil } } func (cr *containerReference) find() common.Executor { return func(ctx context.Context) error { containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{ All: true, }) if err != nil { return errors.WithStack(err) } for _, container := range containers { for _, name := range container.Names { if name[1:] == cr.input.Name { cr.id = container.ID return nil } } } cr.id = "" return nil } } func (cr *containerReference) remove() common.Executor { return func(ctx context.Context) error { if cr.id == "" { return nil } logger := common.Logger(ctx) err := cr.cli.ContainerRemove(context.Background(), cr.id, types.ContainerRemoveOptions{ RemoveVolumes: true, Force: true, }) if err != nil { return errors.WithStack(err) } cr.id = "" logger.Debugf("Removed container: %v", cr.id) return nil } } func (cr *containerReference) create() common.Executor { return func(ctx context.Context) error { if cr.id != "" { return nil } logger := common.Logger(ctx) isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) input := cr.input config := &container.Config{ Image: input.Image, Cmd: input.Cmd, Entrypoint: input.Entrypoint, WorkingDir: input.WorkingDir, Env: input.Env, Tty: isTerminal, } if len(input.Volumes) > 0 { config.Volumes = make(map[string]struct{}) for _, vol := range input.Volumes { config.Volumes[vol] = struct{}{} } } resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{ Binds: input.Binds, }, nil, input.Name) if err != nil { return errors.WithStack(err) } logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image) logger.Debugf("ENV ==> %v", input.Env) cr.id = resp.ID return nil } } func (cr *containerReference) copyContent() common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) for dstPath, srcReader := range cr.input.Content { logger.Debugf("Extracting content to '%s'", dstPath) err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{}) if err != nil { return errors.WithStack(err) } } return nil } } func (cr *containerReference) attach() common.Executor { return func(ctx context.Context) error { out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{ Stream: true, Stdout: true, Stderr: true, }) if err != nil { return errors.WithStack(err) } isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) var outWriter io.Writer outWriter = cr.input.Stdout if outWriter == nil { outWriter = os.Stdout } errWriter := cr.input.Stderr if errWriter == nil { errWriter = os.Stderr } go func() { if !isTerminal || os.Getenv("NORAW") != "" { _, err = stdcopy.StdCopy(outWriter, errWriter, out.Reader) } else { _, err = io.Copy(outWriter, out.Reader) } if err != nil { common.Logger(ctx).Error(err) } }() return nil } } func (cr *containerReference) start() common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", cr.input.Image, cr.input.Entrypoint, cr.input.Cmd) if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil { return errors.WithStack(err) } logger.Debugf("Started container: %v", cr.id) return nil } } func (cr *containerReference) wait() common.Executor { return func(ctx context.Context) error { logger := common.Logger(ctx) statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning) var statusCode int64 select { case err := <-errCh: if err != nil { return errors.WithStack(err) } case status := <-statusCh: statusCode = status.StatusCode } logger.Debugf("Return status: %v", statusCode) if statusCode == 0 { return nil } return fmt.Errorf("exit with `FAILURE`: %v", statusCode) } }