mirror of
https://github.com/moby/moby.git
synced 2026-01-11 10:41:43 +00:00
220 lines
6.2 KiB
Go
220 lines
6.2 KiB
Go
package container
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/moby/moby/api/pkg/stdcopy"
|
|
"github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/network"
|
|
"github.com/moby/moby/client"
|
|
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, client.ContainerCreateOptions{
|
|
Config: config.Config,
|
|
HostConfig: config.HostConfig,
|
|
NetworkingConfig: config.NetworkingConfig,
|
|
Platform: config.Platform,
|
|
ContainerName: 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) (client.ContainerCreateResult, error) {
|
|
return apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: config.Config,
|
|
HostConfig: config.HostConfig,
|
|
NetworkingConfig: config.NetworkingConfig,
|
|
Platform: config.Platform,
|
|
ContainerName: 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, client.ContainerStartOptions{})
|
|
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, client.ContainerAttachOptions{
|
|
Stream: true,
|
|
Stdout: true,
|
|
Stderr: true,
|
|
})
|
|
assert.NilError(t, err)
|
|
|
|
err = apiClient.ContainerStart(ctx, id, client.ContainerStartOptions{})
|
|
assert.NilError(t, err)
|
|
|
|
s, err := demultiplexStreams(ctx, aresp.HijackedResponse)
|
|
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 client.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 client.ContainerRemoveOptions) {
|
|
t.Helper()
|
|
|
|
err := apiClient.ContainerRemove(ctx, container, options)
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
func RemoveAll(ctx context.Context, t *testing.T, apiClient client.APIClient) {
|
|
t.Helper()
|
|
|
|
containers, err := apiClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
|
assert.NilError(t, err)
|
|
|
|
for _, c := range containers {
|
|
Remove(ctx, t, apiClient, c.ID, client.ContainerRemoveOptions{Force: true})
|
|
}
|
|
}
|
|
|
|
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, apiClient client.APIClient, id string) (ContainerOutput, error) {
|
|
logs, err := apiClient.ContainerLogs(ctx, id, client.ContainerLogsOptions{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
|
|
}
|