dotcloud/docker

View on GitHub
integration/internal/container/container.go

Summary

Maintainability
A
35 mins
Test Coverage
package container

import (
    "bytes"
    "context"
    "errors"
    "runtime"
    "sync"
    "testing"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/network"
    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/stdcopy"
    ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    "gotest.tools/v3/assert"
)

// TestContainerConfig holds container configuration struct that
// are used in api calls.
type TestContainerConfig struct {
    Name             string
    Config           *container.Config
    HostConfig       *container.HostConfig
    NetworkingConfig *network.NetworkingConfig
    Platform         *ocispec.Platform
}

// NewTestConfig creates a new TestContainerConfig with the provided options.
//
// If no options are passed, it creates a default config, which is a busybox
// container running "top" (on Linux) or "sleep" (on Windows).
func NewTestConfig(ops ...func(*TestContainerConfig)) *TestContainerConfig {
    cmd := []string{"top"}
    if runtime.GOOS == "windows" {
        cmd = []string{"sleep", "240"}
    }
    config := &TestContainerConfig{
        Config: &container.Config{
            Image: "busybox",
            Cmd:   cmd,
        },
        HostConfig:       &container.HostConfig{},
        NetworkingConfig: &network.NetworkingConfig{},
    }

    for _, op := range ops {
        op(config)
    }

    return config
}

// Create creates a container with the specified options, asserting that there was no error.
func Create(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(*TestContainerConfig)) string {
    t.Helper()
    config := NewTestConfig(ops...)
    c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
    assert.NilError(t, err)

    return c.ID
}

// CreateFromConfig creates a container from the given TestContainerConfig.
//
// Example use:
//
//    ctr, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithAutoRemove))
//    assert.Check(t, err)
func CreateFromConfig(ctx context.Context, apiClient client.APIClient, config *TestContainerConfig) (container.CreateResponse, error) {
    return apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
}

// Run creates and start a container with the specified options
func Run(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(*TestContainerConfig)) string {
    t.Helper()
    id := Create(ctx, t, apiClient, ops...)

    err := apiClient.ContainerStart(ctx, id, container.StartOptions{})
    assert.NilError(t, err)

    return id
}

type RunResult struct {
    ContainerID string
    ExitCode    int
    Stdout      *bytes.Buffer
    Stderr      *bytes.Buffer
}

func RunAttach(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(config *TestContainerConfig)) RunResult {
    t.Helper()

    ops = append(ops, func(c *TestContainerConfig) {
        c.Config.AttachStdout = true
        c.Config.AttachStderr = true
    })
    id := Create(ctx, t, apiClient, ops...)

    aresp, err := apiClient.ContainerAttach(ctx, id, container.AttachOptions{
        Stream: true,
        Stdout: true,
        Stderr: true,
    })
    assert.NilError(t, err)

    err = apiClient.ContainerStart(ctx, id, container.StartOptions{})
    assert.NilError(t, err)

    s, err := demultiplexStreams(ctx, aresp)
    if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
        assert.NilError(t, err)
    }

    // Inspect to get the exit code. A new context is used here to make sure that if the context passed as argument as
    // reached timeout during the demultiplexStream call, we still return a RunResult.
    resp, err := apiClient.ContainerInspect(context.Background(), id)
    assert.NilError(t, err)

    return RunResult{ContainerID: id, ExitCode: resp.State.ExitCode, Stdout: &s.stdout, Stderr: &s.stderr}
}

type streams struct {
    stdout, stderr bytes.Buffer
}

// demultiplexStreams starts a goroutine to demultiplex stdout and stderr from the types.HijackedResponse resp and
// waits until either multiplexed stream reaches EOF or the context expires. It unconditionally closes resp and waits
// until the demultiplexing goroutine has finished its work before returning.
func demultiplexStreams(ctx context.Context, resp types.HijackedResponse) (streams, error) {
    var s streams
    outputDone := make(chan error, 1)

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        _, err := stdcopy.StdCopy(&s.stdout, &s.stderr, resp.Reader)
        outputDone <- err
        wg.Done()
    }()

    var err error
    select {
    case copyErr := <-outputDone:
        err = copyErr
        break
    case <-ctx.Done():
        err = ctx.Err()
    }

    resp.Close()
    wg.Wait()
    return s, err
}

func Remove(ctx context.Context, t *testing.T, apiClient client.APIClient, container string, options container.RemoveOptions) {
    t.Helper()

    err := apiClient.ContainerRemove(ctx, container, options)
    assert.NilError(t, err)
}

func Inspect(ctx context.Context, t *testing.T, apiClient client.APIClient, containerRef string) container.InspectResponse {
    t.Helper()

    c, err := apiClient.ContainerInspect(ctx, containerRef)
    assert.NilError(t, err)

    return c
}

type ContainerOutput struct {
    Stdout, Stderr string
}

// Output waits for the container to end running and returns its output.
func Output(ctx context.Context, client client.APIClient, id string) (ContainerOutput, error) {
    logs, err := client.ContainerLogs(ctx, id, container.LogsOptions{Follow: true, ShowStdout: true, ShowStderr: true})
    if err != nil {
        return ContainerOutput{}, err
    }

    defer logs.Close()

    var stdoutBuf, stderrBuf bytes.Buffer
    _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, logs)
    if err != nil {
        return ContainerOutput{}, err
    }

    return ContainerOutput{
        Stdout: stdoutBuf.String(),
        Stderr: stderrBuf.String(),
    }, nil
}