mirror of
https://github.com/moby/moby.git
synced 2026-07-10 08:39:25 +00:00
Test 'TestDockerCLIRunSuite/TestRunAttachFailedNoLeak' does this ...
- start a container that exits immediately, its comment says:
- "Run a dummy container to ensure all goroutines are up and running
before we get a count"
- wait for the number of goroutines to be stable for 400ms, and remember
that number
- start a container
- start another container, expecting it to fail with a port-mapping clash
- stop the running container
- wait for up to 30s for the number of goroutines to fall back to the
remembered number.
In a CI run - hacking in some debug to count goroutines once a second,
before waiting for the number to stablilise for 400ms, showed that the
initial (dummy) container run had no immediate effect. But, three more
goroutines appeared within a few seconds. For example:
=== RUN TestDockerCLIRunSuite/TestRunAttachFailedNoLeak
docker_cli_run_test.go:3822: goroutines before container run: 47 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 47 i 0 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 48 i 1 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 48 i 2 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 48 i 3 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 48 i 4 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 50 i 5 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 50 i 6 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 50 i 7 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 50 i 8 err <nil>
docker_cli_run_test.go:3830: goroutines after container run: 50 i 9 err <nil>
That means a delay while running the rest of the test risks finding the
extra goroutines that are going to start anyway and not go away (regardless
of whether more containers are started).
So - wait for the goroutine count to be stable for 7s, rather than 400ms.
Signed-off-by: Rob Murray <rob.murray@docker.com>
505 lines
14 KiB
Go
505 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/docker/integration-cli/cli"
|
|
"github.com/docker/docker/integration-cli/daemon"
|
|
"github.com/docker/docker/internal/testutils/specialimage"
|
|
"github.com/docker/docker/pkg/archive"
|
|
"github.com/docker/docker/testutil"
|
|
"gotest.tools/v3/assert"
|
|
"gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/icmd"
|
|
"gotest.tools/v3/poll"
|
|
)
|
|
|
|
func deleteImages(images ...string) error {
|
|
args := []string{dockerBinary, "rmi", "-f"}
|
|
return icmd.RunCmd(icmd.Cmd{Command: append(args, images...)}).Error
|
|
}
|
|
|
|
// Deprecated: use cli.Docker or cli.DockerCmd
|
|
func dockerCmdWithError(args ...string) (string, int, error) {
|
|
result := cli.Docker(cli.Args(args...))
|
|
if result.Error != nil {
|
|
return result.Combined(), result.ExitCode, result.Compare(icmd.Success)
|
|
}
|
|
return result.Combined(), result.ExitCode, result.Error
|
|
}
|
|
|
|
// Deprecated: use cli.Docker or cli.DockerCmd
|
|
func dockerCmdWithResult(args ...string) *icmd.Result {
|
|
return cli.Docker(cli.Args(args...))
|
|
}
|
|
|
|
func findContainerIP(c *testing.T, id string, network string) string {
|
|
c.Helper()
|
|
out := cli.DockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id).Stdout()
|
|
return strings.Trim(out, " \r\n'")
|
|
}
|
|
|
|
func getContainerCount(c *testing.T) int {
|
|
c.Helper()
|
|
const containers = "Containers:"
|
|
|
|
result := icmd.RunCommand(dockerBinary, "info")
|
|
result.Assert(c, icmd.Success)
|
|
|
|
lines := strings.Split(result.Combined(), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, containers) {
|
|
output := strings.TrimSpace(line)
|
|
output = strings.TrimPrefix(output, containers)
|
|
output = strings.Trim(output, " ")
|
|
containerCount, err := strconv.Atoi(output)
|
|
assert.NilError(c, err)
|
|
return containerCount
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func inspectFieldAndUnmarshall(c *testing.T, name, field string, output interface{}) {
|
|
c.Helper()
|
|
str := inspectFieldJSON(c, name, field)
|
|
err := json.Unmarshal([]byte(str), output)
|
|
assert.Assert(c, err == nil, "failed to unmarshal: %v", err)
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectFilter(name, filter string) (string, error) {
|
|
format := fmt.Sprintf("{{%s}}", filter)
|
|
result := icmd.RunCommand(dockerBinary, "inspect", "-f", format, name)
|
|
if result.Error != nil || result.ExitCode != 0 {
|
|
return "", fmt.Errorf("failed to inspect %s: %s", name, result.Combined())
|
|
}
|
|
return strings.TrimSpace(result.Combined()), nil
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectFieldWithError(name, field string) (string, error) {
|
|
return inspectFilter(name, "."+field)
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectField(c *testing.T, name, field string) string {
|
|
c.Helper()
|
|
out, err := inspectFilter(name, "."+field)
|
|
assert.NilError(c, err)
|
|
return out
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectFieldJSON(c *testing.T, name, field string) string {
|
|
c.Helper()
|
|
out, err := inspectFilter(name, "json ."+field)
|
|
assert.NilError(c, err)
|
|
return out
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectFieldMap(c *testing.T, name, path, field string) string {
|
|
c.Helper()
|
|
out, err := inspectFilter(name, fmt.Sprintf("index .%s %q", path, field))
|
|
assert.NilError(c, err)
|
|
return out
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectMountSourceField(name, destination string) (string, error) {
|
|
m, err := inspectMountPoint(name, destination)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return m.Source, nil
|
|
}
|
|
|
|
var errMountNotFound = errors.New("mount point not found")
|
|
|
|
// Deprecated: use cli.Docker
|
|
func inspectMountPoint(name, destination string) (container.MountPoint, error) {
|
|
out, err := inspectFilter(name, "json .Mounts")
|
|
if err != nil {
|
|
return container.MountPoint{}, err
|
|
}
|
|
|
|
var mp []container.MountPoint
|
|
if err := json.Unmarshal([]byte(out), &mp); err != nil {
|
|
return container.MountPoint{}, err
|
|
}
|
|
|
|
for _, c := range mp {
|
|
if c.Destination == destination {
|
|
return c, nil
|
|
}
|
|
}
|
|
|
|
return container.MountPoint{}, errMountNotFound
|
|
}
|
|
|
|
func getIDByName(c *testing.T, name string) string {
|
|
c.Helper()
|
|
id, err := inspectFieldWithError(name, "Id")
|
|
assert.NilError(c, err)
|
|
return id
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func buildImageSuccessfully(c *testing.T, name string, cmdOperators ...cli.CmdOperator) {
|
|
c.Helper()
|
|
buildImage(name, cmdOperators...).Assert(c, icmd.Success)
|
|
}
|
|
|
|
// Deprecated: use cli.Docker
|
|
func buildImage(name string, cmdOperators ...cli.CmdOperator) *icmd.Result {
|
|
return cli.Docker(cli.Args("build", "-t", name), cmdOperators...)
|
|
}
|
|
|
|
// Write `content` to the file at path `dst`, creating it if necessary,
|
|
// as well as any missing directories.
|
|
// The file is truncated if it already exists.
|
|
// Fail the test when error occurs.
|
|
func writeFile(dst, content string, c *testing.T) {
|
|
c.Helper()
|
|
// Create subdirectories if necessary
|
|
assert.NilError(c, os.MkdirAll(path.Dir(dst), 0o700))
|
|
f, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o600)
|
|
assert.NilError(c, err)
|
|
defer f.Close()
|
|
// Write content (truncate if it exists)
|
|
_, err = io.Copy(f, strings.NewReader(content))
|
|
assert.NilError(c, err)
|
|
}
|
|
|
|
// Return the contents of file at path `src`.
|
|
// Fail the test when error occurs.
|
|
func readFile(src string, c *testing.T) (content string) {
|
|
c.Helper()
|
|
data, err := os.ReadFile(src)
|
|
assert.NilError(c, err)
|
|
|
|
return string(data)
|
|
}
|
|
|
|
func containerStorageFile(containerID, basename string) string {
|
|
return filepath.Join(testEnv.PlatformDefaults.ContainerStoragePath, containerID, basename)
|
|
}
|
|
|
|
// docker commands that use this function must be run with the '-d' switch.
|
|
func runCommandAndReadContainerFile(c *testing.T, filename string, command string, args ...string) []byte {
|
|
c.Helper()
|
|
result := icmd.RunCommand(command, args...)
|
|
result.Assert(c, icmd.Success)
|
|
contID := strings.TrimSpace(result.Combined())
|
|
cli.WaitRun(c, contID)
|
|
return readContainerFile(c, contID, filename)
|
|
}
|
|
|
|
func readContainerFile(c *testing.T, containerID, filename string) []byte {
|
|
c.Helper()
|
|
f, err := os.Open(containerStorageFile(containerID, filename))
|
|
assert.NilError(c, err)
|
|
defer f.Close()
|
|
|
|
content, err := io.ReadAll(f)
|
|
assert.NilError(c, err)
|
|
return content
|
|
}
|
|
|
|
func readContainerFileWithExec(c *testing.T, containerID, filename string) []byte {
|
|
c.Helper()
|
|
result := icmd.RunCommand(dockerBinary, "exec", containerID, "cat", filename)
|
|
result.Assert(c, icmd.Success)
|
|
return []byte(result.Combined())
|
|
}
|
|
|
|
// daemonTime provides the current time on the daemon host
|
|
func daemonTime(c *testing.T) time.Time {
|
|
c.Helper()
|
|
if testEnv.IsLocalDaemon() {
|
|
return time.Now()
|
|
}
|
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
|
assert.NilError(c, err)
|
|
defer apiClient.Close()
|
|
|
|
info, err := apiClient.Info(testutil.GetContext(c))
|
|
assert.NilError(c, err)
|
|
|
|
dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
|
|
assert.Assert(c, err == nil, "invalid time format in GET /info response")
|
|
return dt
|
|
}
|
|
|
|
// daemonUnixTime returns the current time on the daemon host with nanoseconds precision.
|
|
// It return the time formatted how the client sends timestamps to the server.
|
|
func daemonUnixTime(c *testing.T) string {
|
|
c.Helper()
|
|
return parseEventTime(daemonTime(c))
|
|
}
|
|
|
|
func parseEventTime(t time.Time) string {
|
|
return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond()))
|
|
}
|
|
|
|
// appendBaseEnv appends the minimum set of environment variables to exec the
|
|
// docker cli binary for testing with correct configuration to the given env
|
|
// list.
|
|
func appendBaseEnv(isTLS bool, env ...string) []string {
|
|
preserveList := []string{
|
|
// preserve remote test host
|
|
"DOCKER_HOST",
|
|
|
|
// windows: requires preserving SystemRoot, otherwise dial tcp fails
|
|
// with "GetAddrInfoW: A non-recoverable error occurred during a database lookup."
|
|
"SystemRoot",
|
|
|
|
// testing help text requires the $PATH to dockerd is set
|
|
"PATH",
|
|
}
|
|
if isTLS {
|
|
preserveList = append(preserveList, "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH")
|
|
}
|
|
|
|
for _, key := range preserveList {
|
|
if val := os.Getenv(key); val != "" {
|
|
env = append(env, fmt.Sprintf("%s=%s", key, val))
|
|
}
|
|
}
|
|
return env
|
|
}
|
|
|
|
func createTmpFile(c *testing.T, content string) string {
|
|
c.Helper()
|
|
f, err := os.CreateTemp("", "testfile")
|
|
assert.NilError(c, err)
|
|
|
|
filename := f.Name()
|
|
|
|
err = os.WriteFile(filename, []byte(content), 0o644)
|
|
assert.NilError(c, err)
|
|
|
|
return filename
|
|
}
|
|
|
|
// waitInspect will wait for the specified container to have the specified string
|
|
// in the inspect output. It will wait until the specified timeout (in seconds)
|
|
// is reached.
|
|
// Deprecated: use cli.WaitFor
|
|
func waitInspect(name, expr, expected string, timeout time.Duration) error {
|
|
return daemon.WaitInspectWithArgs(dockerBinary, name, expr, expected, timeout)
|
|
}
|
|
|
|
func getInspectBody(c *testing.T, version, id string) []byte {
|
|
c.Helper()
|
|
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(version))
|
|
assert.NilError(c, err)
|
|
defer apiClient.Close()
|
|
_, body, err := apiClient.ContainerInspectWithRaw(testutil.GetContext(c), id, false)
|
|
assert.NilError(c, err)
|
|
return body
|
|
}
|
|
|
|
// Run a long running idle task in a background container using the
|
|
// system-specific default image and command.
|
|
func runSleepingContainer(c *testing.T, extraArgs ...string) string {
|
|
c.Helper()
|
|
return runSleepingContainerInImage(c, "busybox", extraArgs...)
|
|
}
|
|
|
|
// Run a long running idle task in a background container using the specified
|
|
// image and the system-specific command.
|
|
func runSleepingContainerInImage(c *testing.T, image string, extraArgs ...string) string {
|
|
c.Helper()
|
|
args := []string{"run", "-d"}
|
|
args = append(args, extraArgs...)
|
|
args = append(args, image)
|
|
args = append(args, sleepCommandForDaemonPlatform()...)
|
|
return strings.TrimSpace(cli.DockerCmd(c, args...).Combined())
|
|
}
|
|
|
|
// minimalBaseImage returns the name of the minimal base image for the current
|
|
// daemon platform.
|
|
func minimalBaseImage() string {
|
|
return testEnv.PlatformDefaults.BaseImage
|
|
}
|
|
|
|
func getGoroutineNumber(ctx context.Context, apiClient client.APIClient) (int, error) {
|
|
info, err := apiClient.Info(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return info.NGoroutines, nil
|
|
}
|
|
|
|
func waitForStableGoroutineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int {
|
|
var out int
|
|
poll.WaitOn(t, stableGoroutineCount(ctx, apiClient, &out), poll.WithDelay(time.Second), poll.WithTimeout(30*time.Second))
|
|
return out
|
|
}
|
|
|
|
func stableGoroutineCount(ctx context.Context, apiClient client.APIClient, count *int) poll.Check {
|
|
var (
|
|
numStable int
|
|
nRoutines int
|
|
)
|
|
|
|
return func(t poll.LogT) poll.Result {
|
|
n, err := getGoroutineNumber(ctx, apiClient)
|
|
if err != nil {
|
|
return poll.Error(err)
|
|
}
|
|
|
|
last := nRoutines
|
|
|
|
if nRoutines == n {
|
|
numStable++
|
|
} else {
|
|
numStable = 0
|
|
nRoutines = n
|
|
}
|
|
|
|
if numStable > 6 {
|
|
*count = n
|
|
return poll.Success()
|
|
}
|
|
return poll.Continue("goroutine count is not stable: last %d, current %d, stable iters: %d", last, n, numStable)
|
|
}
|
|
}
|
|
|
|
func checkGoroutineCount(ctx context.Context, apiClient client.APIClient, expected int) poll.Check {
|
|
first := true
|
|
return func(t poll.LogT) poll.Result {
|
|
n, err := getGoroutineNumber(ctx, apiClient)
|
|
if err != nil {
|
|
return poll.Error(err)
|
|
}
|
|
if n > expected {
|
|
if first {
|
|
t.Log("Waiting for goroutines to stabilize")
|
|
first = false
|
|
}
|
|
return poll.Continue("expected %d goroutines, got %d", expected, n)
|
|
}
|
|
return poll.Success()
|
|
}
|
|
}
|
|
|
|
func waitForGoroutines(ctx context.Context, t poll.TestingT, apiClient client.APIClient, expected int) {
|
|
poll.WaitOn(t, checkGoroutineCount(ctx, apiClient, expected), poll.WithDelay(500*time.Millisecond), poll.WithTimeout(30*time.Second))
|
|
}
|
|
|
|
// getErrorMessage returns the error message from an error API response
|
|
func getErrorMessage(c *testing.T, body []byte) string {
|
|
c.Helper()
|
|
var resp types.ErrorResponse
|
|
assert.NilError(c, json.Unmarshal(body, &resp))
|
|
return strings.TrimSpace(resp.Message)
|
|
}
|
|
|
|
type (
|
|
checkF func(*testing.T) (interface{}, string)
|
|
reducer func(...interface{}) interface{}
|
|
)
|
|
|
|
func pollCheck(t *testing.T, f checkF, compare func(x interface{}) assert.BoolOrComparison) poll.Check {
|
|
return func(poll.LogT) poll.Result {
|
|
t.Helper()
|
|
v, comment := f(t)
|
|
r := compare(v)
|
|
switch r := r.(type) {
|
|
case bool:
|
|
if r {
|
|
return poll.Success()
|
|
}
|
|
case cmp.Comparison:
|
|
if r().Success() {
|
|
return poll.Success()
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("pollCheck: type %T not implemented", r))
|
|
}
|
|
return poll.Continue("%v", comment)
|
|
}
|
|
}
|
|
|
|
func reducedCheck(r reducer, funcs ...checkF) checkF {
|
|
return func(c *testing.T) (interface{}, string) {
|
|
c.Helper()
|
|
var values []interface{}
|
|
var comments []string
|
|
for _, f := range funcs {
|
|
v, comment := f(c)
|
|
values = append(values, v)
|
|
if len(comment) > 0 {
|
|
comments = append(comments, comment)
|
|
}
|
|
}
|
|
return r(values...), fmt.Sprintf("%v", strings.Join(comments, ", "))
|
|
}
|
|
}
|
|
|
|
func sumAsIntegers(vals ...interface{}) interface{} {
|
|
var s int
|
|
for _, v := range vals {
|
|
s += v.(int)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func loadSpecialImage(c *testing.T, imageFunc specialimage.SpecialImageFunc) string {
|
|
tmpDir := c.TempDir()
|
|
|
|
imgDir := filepath.Join(tmpDir, "image")
|
|
assert.NilError(c, os.Mkdir(imgDir, 0o755))
|
|
|
|
_, err := imageFunc(imgDir)
|
|
assert.NilError(c, err)
|
|
|
|
rc, err := archive.TarWithOptions(imgDir, &archive.TarOptions{})
|
|
assert.NilError(c, err)
|
|
defer rc.Close()
|
|
|
|
imgTar := filepath.Join(tmpDir, "image.tar")
|
|
tarFile, err := os.OpenFile(imgTar, os.O_CREATE|os.O_WRONLY, 0o644)
|
|
assert.NilError(c, err)
|
|
|
|
defer tarFile.Close()
|
|
|
|
_, err = io.Copy(tarFile, rc)
|
|
assert.NilError(c, err)
|
|
|
|
tarFile.Close()
|
|
|
|
out := cli.DockerCmd(c, "load", "-i", imgTar).Stdout()
|
|
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
|
|
if _, imageID, hasID := strings.Cut(line, "Loaded image ID: "); hasID {
|
|
return imageID
|
|
}
|
|
if _, imageRef, hasRef := strings.Cut(line, "Loaded image: "); hasRef {
|
|
return imageRef
|
|
}
|
|
}
|
|
|
|
c.Fatalf("failed to extract image ref from %q", out)
|
|
return ""
|
|
}
|