shared container for job

This commit is contained in:
Casey Lee
2020-02-23 15:01:25 -08:00
parent 9179d8924d
commit 01876438c2
11 changed files with 445 additions and 531 deletions

View File

@@ -21,7 +21,7 @@ type NewDockerPullExecutorInput struct {
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
logger.Infof("%sdocker pull %v", logPrefix, input.Image)
logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
if common.Dryrun(ctx) {
return nil

View File

@@ -1,6 +1,8 @@
package container
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
@@ -8,60 +10,119 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/nektos/act/pkg/common"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"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
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string
Entrypoint []string
Cmd []string
WorkingDir string
Env []string
Binds []string
Mounts map[string]string
Name string
Stdout io.Writer
Stderr io.Writer
}
// NewDockerRunExecutor function to create a run executor for the container
func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor {
// FileEntry is a file to copy to a container
type FileEntry struct {
Name string
Mode int64
Body string
}
// Container for managing docker run containers
type Container interface {
Create() common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor
Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor
Exec(command []string, env map[string]string) common.Executor
Remove() common.Executor
}
// NewContainer creates a reference to a container
func NewContainer(input *NewContainerInput) Container {
cr := new(containerReference)
cr.input = input
return cr
}
func (cr *containerReference) Create() common.Executor {
return common.
NewInfoExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, input.Image, input.Entrypoint, input.Cmd).
NewDebugExecutor("%sdocker create image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.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),
)
}
func (cr *containerReference) Start(attach bool) common.Executor {
return common.
NewDebugExecutor("%sdocker run image=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Entrypoint, cr.input.Cmd).
Then(
common.NewPipelineExecutor(
cr.connect(),
cr.find(),
cr.attach().IfBool(attach),
cr.start(),
cr.wait().IfBool(attach),
).IfNot(common.Dryrun),
)
}
func (cr *containerReference) Pull(forcePull bool) common.Executor {
return NewDockerPullExecutor(NewDockerPullExecutorInput{
Image: cr.input.Image,
ForcePull: forcePull,
})
}
func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor {
return common.NewPipelineExecutor(
cr.connect(),
cr.find(),
cr.copyContent(destPath, files...),
).IfNot(common.Dryrun)
}
func (cr *containerReference) Exec(command []string, env map[string]string) common.Executor {
return common.NewPipelineExecutor(
cr.connect(),
cr.find(),
cr.exec(command, env),
).IfNot(common.Dryrun)
}
func (cr *containerReference) Remove() common.Executor {
return common.NewPipelineExecutor(
cr.connect(),
cr.find(),
).Finally(
cr.remove(),
).IfNot(common.Dryrun)
}
type containerReference struct {
input NewDockerRunExecutorInput
cli *client.Client
id string
input *NewContainerInput
}
func (cr *containerReference) connect() common.Executor {
return func(ctx context.Context) error {
if cr.cli != nil {
return nil
}
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return errors.WithStack(err)
@@ -74,6 +135,9 @@ func (cr *containerReference) connect() common.Executor {
func (cr *containerReference) find() common.Executor {
return func(ctx context.Context) error {
if cr.id != "" {
return nil
}
containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{
All: true,
})
@@ -134,15 +198,18 @@ func (cr *containerReference) create() common.Executor {
Tty: isTerminal,
}
if len(input.Volumes) > 0 {
config.Volumes = make(map[string]struct{})
for _, vol := range input.Volumes {
config.Volumes[vol] = struct{}{}
}
mounts := make([]mount.Mount, 0)
for mountSource, mountTarget := range input.Mounts {
mounts = append(mounts, mount.Mount{
Type: mount.TypeVolume,
Source: mountSource,
Target: mountTarget,
})
}
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
Binds: input.Binds,
Binds: input.Binds,
Mounts: mounts,
}, nil, input.Name)
if err != nil {
return errors.WithStack(err)
@@ -155,15 +222,100 @@ func (cr *containerReference) create() common.Executor {
}
}
func (cr *containerReference) copyContent() common.Executor {
func (cr *containerReference) exec(cmd []string, env map[string]string) 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)
logger.Debugf("Exec command '%s'", cmd)
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
envList := make([]string, 0)
for k, v := range env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
}
idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
Cmd: cmd,
WorkingDir: cr.input.WorkingDir,
Env: envList,
Tty: isTerminal,
AttachStderr: true,
AttachStdout: true,
})
if err != nil {
return errors.WithStack(err)
}
resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{
Tty: isTerminal,
})
if err != nil {
return errors.WithStack(err)
}
var outWriter io.Writer
outWriter = cr.input.Stdout
if outWriter == nil {
outWriter = os.Stdout
}
errWriter := cr.input.Stderr
if errWriter == nil {
errWriter = os.Stderr
}
err = cr.cli.ContainerExecStart(ctx, idResp.ID, types.ExecStartCheck{
Tty: isTerminal,
})
if err != nil {
return errors.WithStack(err)
}
if !isTerminal || os.Getenv("NORAW") != "" {
_, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader)
} else {
_, err = io.Copy(outWriter, resp.Reader)
}
if err != nil {
logger.Error(err)
}
inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID)
if err != nil {
return errors.WithStack(err)
}
if inspectResp.ExitCode == 0 {
return nil
}
return fmt.Errorf("exit with `FAILURE`: %v", inspectResp.ExitCode)
}
}
func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
for _, file := range files {
log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body))
hdr := &tar.Header{
Name: file.Name,
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
return err
}
}
if err := tw.Close(); err != nil {
return err
}
logger.Debugf("Extracting content to '%s'", dstPath)
err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{})
if err != nil {
return errors.WithStack(err)
}
return nil
}
@@ -207,7 +359,7 @@ func (cr *containerReference) attach() common.Executor {
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)
logger.Debugf("Starting container: %v", cr.id)
if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil {
return errors.WithStack(err)