moby/integration-cli/docker_utils_test.go
Brian Goff e8dc902781 Wire up tests to support otel tracing
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>
2023-09-07 18:38:22 +00:00

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
}