123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- 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) types.ContainerJSON {
- t.Helper()
- c, err := apiClient.ContainerInspect(ctx, containerRef)
- assert.NilError(t, err)
- return c
- }
|