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/nektos/act/common" "golang.org/x/crypto/ssh/terminal" ) // NewDockerRunExecutorInput the input for the NewDockerRunExecutor function type NewDockerRunExecutorInput struct { DockerExecutorInput Image string Entrypoint []string Cmd []string WorkingDir string Env []string Binds []string Content map[string]io.Reader Volumes []string Name string ReuseContainers bool } // NewDockerRunExecutor function to create a run executor for the container func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor { return func() error { input.Logger.Infof("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd) if input.Dryrun { return nil } cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return err } cli.NegotiateAPIVersion(input.Ctx) // check if container exists containerID, err := findContainer(input, cli, input.Name) if err != nil { return err } // if we have an old container and we aren't reusing, remove it! if !input.ReuseContainers && containerID != "" { input.Logger.Debugf("Found existing container for %s...removing", input.Name) removeContainer(input, cli, containerID) containerID = "" } // create a new container if we don't have one to reuse if containerID == "" { containerID, err = createContainer(input, cli) if err != nil { return err } } // be sure to cleanup container if we aren't reusing if !input.ReuseContainers { defer removeContainer(input, cli, containerID) } executor := common.NewPipelineExecutor( func() error { return copyContentToContainer(input, cli, containerID) }, func() error { return attachContainer(input, cli, containerID) }, func() error { return startContainer(input, cli, containerID) }, func() error { return waitContainer(input, cli, containerID) }, ) return executor() } } func createContainer(input NewDockerRunExecutorInput, cli *client.Client) (string, error) { isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) 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 := cli.ContainerCreate(input.Ctx, config, &container.HostConfig{ Binds: input.Binds, }, nil, input.Name) if err != nil { return "", err } input.Logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image) input.Logger.Debugf("ENV ==> %v", input.Env) return resp.ID, nil } func findContainer(input NewDockerRunExecutorInput, cli *client.Client, containerName string) (string, error) { containers, err := cli.ContainerList(input.Ctx, types.ContainerListOptions{ All: true, }) if err != nil { return "", err } for _, container := range containers { for _, name := range container.Names { if name[1:] == containerName { return container.ID, nil } } } return "", nil } func removeContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) { err := cli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{ RemoveVolumes: true, Force: true, }) if err != nil { input.Logger.Errorf("%v", err) } input.Logger.Debugf("Removed container: %v", containerID) } func copyContentToContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { for dstPath, srcReader := range input.Content { input.Logger.Debugf("Extracting content to '%s'", dstPath) err := cli.CopyToContainer(input.Ctx, containerID, dstPath, srcReader, types.CopyToContainerOptions{}) if err != nil { return err } } return nil } func attachContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { out, err := cli.ContainerAttach(input.Ctx, containerID, types.ContainerAttachOptions{ Stream: true, Stdout: true, Stderr: true, }) if err != nil { return err } isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) if !isTerminal || os.Getenv("NORAW") != "" { go input.logDockerOutput(out.Reader) } else { go input.streamDockerOutput(out.Reader) } return nil } func startContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { input.Logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", input.Image, input.Entrypoint, input.Cmd) if err := cli.ContainerStart(input.Ctx, containerID, types.ContainerStartOptions{}); err != nil { return err } input.Logger.Debugf("Started container: %v", containerID) return nil } func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error { statusCh, errCh := cli.ContainerWait(input.Ctx, containerID, container.WaitConditionNotRunning) var statusCode int64 select { case err := <-errCh: if err != nil { return err } case status := <-statusCh: statusCode = status.StatusCode } input.Logger.Debugf("Return status: %v", statusCode) if statusCode == 0 { return nil } else if statusCode == 78 { return fmt.Errorf("exit with `NEUTRAL`: 78") } return fmt.Errorf("exit with `FAILURE`: %v", statusCode) }