e8dc902781
Integration tests will now configure clients to propagate traces as well as create spans for all tests. Some extra changes were needed (or desired for trace propagation) in the test helpers to pass through tracing spans via context. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
481 lines
13 KiB
Go
481 lines
13 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/client"
|
|
"github.com/docker/docker/integration-cli/cli"
|
|
"github.com/docker/docker/integration-cli/daemon"
|
|
"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 dockerCmd(c testing.TB, args ...string) (string, int) {
|
|
c.Helper()
|
|
result := cli.DockerCmd(c, args...)
|
|
return result.Combined(), result.ExitCode
|
|
}
|
|
|
|
// 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, _ := dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id)
|
|
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) (types.MountPoint, error) {
|
|
out, err := inspectFilter(name, "json .Mounts")
|
|
if err != nil {
|
|
return types.MountPoint{}, err
|
|
}
|
|
|
|
var mp []types.MountPoint
|
|
if err := json.Unmarshal([]byte(out), &mp); err != nil {
|
|
return types.MountPoint{}, err
|
|
}
|
|
|
|
var m *types.MountPoint
|
|
for _, c := range mp {
|
|
if c.Destination == destination {
|
|
m = &c
|
|
break
|
|
}
|
|
}
|
|
|
|
if m == nil {
|
|
return types.MountPoint{}, errMountNotFound
|
|
}
|
|
|
|
return *m, nil
|
|
}
|
|
|
|
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.Assert(c, os.MkdirAll(path.Dir(dst), 0o700) == nil)
|
|
f, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o700)
|
|
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())
|
|
if err := waitRun(contID); err != nil {
|
|
c.Fatalf("%v: %q", contID, err)
|
|
}
|
|
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
|
|
}
|
|
|
|
// waitRun will wait for the specified container to be running, maximum 5 seconds.
|
|
// Deprecated: use cli.WaitFor
|
|
func waitRun(contID string) error {
|
|
return daemon.WaitInspectWithArgs(dockerBinary, contID, "{{.State.Running}}", "true", 5*time.Second)
|
|
}
|
|
|
|
// 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 waitForStableGourtineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int {
|
|
var out int
|
|
poll.WaitOn(t, stableGoroutineCount(ctx, apiClient, &out), 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 > 3 {
|
|
*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("exepcted %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.Assert(c, json.Unmarshal(body, &resp) == nil)
|
|
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(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
|
|
}
|