github.com/moby/docker@v26.1.3+incompatible/integration-cli/docker_utils_test.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/docker/docker/api/types"
    18  	"github.com/docker/docker/client"
    19  	"github.com/docker/docker/integration-cli/cli"
    20  	"github.com/docker/docker/integration-cli/daemon"
    21  	"github.com/docker/docker/internal/testutils/specialimage"
    22  	"github.com/docker/docker/pkg/archive"
    23  	"github.com/docker/docker/testutil"
    24  	"gotest.tools/v3/assert"
    25  	"gotest.tools/v3/assert/cmp"
    26  	"gotest.tools/v3/icmd"
    27  	"gotest.tools/v3/poll"
    28  )
    29  
    30  func deleteImages(images ...string) error {
    31  	args := []string{dockerBinary, "rmi", "-f"}
    32  	return icmd.RunCmd(icmd.Cmd{Command: append(args, images...)}).Error
    33  }
    34  
    35  // Deprecated: use cli.Docker or cli.DockerCmd
    36  func dockerCmdWithError(args ...string) (string, int, error) {
    37  	result := cli.Docker(cli.Args(args...))
    38  	if result.Error != nil {
    39  		return result.Combined(), result.ExitCode, result.Compare(icmd.Success)
    40  	}
    41  	return result.Combined(), result.ExitCode, result.Error
    42  }
    43  
    44  // Deprecated: use cli.Docker or cli.DockerCmd
    45  func dockerCmdWithResult(args ...string) *icmd.Result {
    46  	return cli.Docker(cli.Args(args...))
    47  }
    48  
    49  func findContainerIP(c *testing.T, id string, network string) string {
    50  	c.Helper()
    51  	out := cli.DockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", network), id).Stdout()
    52  	return strings.Trim(out, " \r\n'")
    53  }
    54  
    55  func getContainerCount(c *testing.T) int {
    56  	c.Helper()
    57  	const containers = "Containers:"
    58  
    59  	result := icmd.RunCommand(dockerBinary, "info")
    60  	result.Assert(c, icmd.Success)
    61  
    62  	lines := strings.Split(result.Combined(), "\n")
    63  	for _, line := range lines {
    64  		if strings.Contains(line, containers) {
    65  			output := strings.TrimSpace(line)
    66  			output = strings.TrimPrefix(output, containers)
    67  			output = strings.Trim(output, " ")
    68  			containerCount, err := strconv.Atoi(output)
    69  			assert.NilError(c, err)
    70  			return containerCount
    71  		}
    72  	}
    73  	return 0
    74  }
    75  
    76  func inspectFieldAndUnmarshall(c *testing.T, name, field string, output interface{}) {
    77  	c.Helper()
    78  	str := inspectFieldJSON(c, name, field)
    79  	err := json.Unmarshal([]byte(str), output)
    80  	assert.Assert(c, err == nil, "failed to unmarshal: %v", err)
    81  }
    82  
    83  // Deprecated: use cli.Docker
    84  func inspectFilter(name, filter string) (string, error) {
    85  	format := fmt.Sprintf("{{%s}}", filter)
    86  	result := icmd.RunCommand(dockerBinary, "inspect", "-f", format, name)
    87  	if result.Error != nil || result.ExitCode != 0 {
    88  		return "", fmt.Errorf("failed to inspect %s: %s", name, result.Combined())
    89  	}
    90  	return strings.TrimSpace(result.Combined()), nil
    91  }
    92  
    93  // Deprecated: use cli.Docker
    94  func inspectFieldWithError(name, field string) (string, error) {
    95  	return inspectFilter(name, "."+field)
    96  }
    97  
    98  // Deprecated: use cli.Docker
    99  func inspectField(c *testing.T, name, field string) string {
   100  	c.Helper()
   101  	out, err := inspectFilter(name, "."+field)
   102  	assert.NilError(c, err)
   103  	return out
   104  }
   105  
   106  // Deprecated: use cli.Docker
   107  func inspectFieldJSON(c *testing.T, name, field string) string {
   108  	c.Helper()
   109  	out, err := inspectFilter(name, "json ."+field)
   110  	assert.NilError(c, err)
   111  	return out
   112  }
   113  
   114  // Deprecated: use cli.Docker
   115  func inspectFieldMap(c *testing.T, name, path, field string) string {
   116  	c.Helper()
   117  	out, err := inspectFilter(name, fmt.Sprintf("index .%s %q", path, field))
   118  	assert.NilError(c, err)
   119  	return out
   120  }
   121  
   122  // Deprecated: use cli.Docker
   123  func inspectMountSourceField(name, destination string) (string, error) {
   124  	m, err := inspectMountPoint(name, destination)
   125  	if err != nil {
   126  		return "", err
   127  	}
   128  	return m.Source, nil
   129  }
   130  
   131  var errMountNotFound = errors.New("mount point not found")
   132  
   133  // Deprecated: use cli.Docker
   134  func inspectMountPoint(name, destination string) (types.MountPoint, error) {
   135  	out, err := inspectFilter(name, "json .Mounts")
   136  	if err != nil {
   137  		return types.MountPoint{}, err
   138  	}
   139  
   140  	var mp []types.MountPoint
   141  	if err := json.Unmarshal([]byte(out), &mp); err != nil {
   142  		return types.MountPoint{}, err
   143  	}
   144  
   145  	var m *types.MountPoint
   146  	for _, c := range mp {
   147  		if c.Destination == destination {
   148  			m = &c
   149  			break
   150  		}
   151  	}
   152  
   153  	if m == nil {
   154  		return types.MountPoint{}, errMountNotFound
   155  	}
   156  
   157  	return *m, nil
   158  }
   159  
   160  func getIDByName(c *testing.T, name string) string {
   161  	c.Helper()
   162  	id, err := inspectFieldWithError(name, "Id")
   163  	assert.NilError(c, err)
   164  	return id
   165  }
   166  
   167  // Deprecated: use cli.Docker
   168  func buildImageSuccessfully(c *testing.T, name string, cmdOperators ...cli.CmdOperator) {
   169  	c.Helper()
   170  	buildImage(name, cmdOperators...).Assert(c, icmd.Success)
   171  }
   172  
   173  // Deprecated: use cli.Docker
   174  func buildImage(name string, cmdOperators ...cli.CmdOperator) *icmd.Result {
   175  	return cli.Docker(cli.Args("build", "-t", name), cmdOperators...)
   176  }
   177  
   178  // Write `content` to the file at path `dst`, creating it if necessary,
   179  // as well as any missing directories.
   180  // The file is truncated if it already exists.
   181  // Fail the test when error occurs.
   182  func writeFile(dst, content string, c *testing.T) {
   183  	c.Helper()
   184  	// Create subdirectories if necessary
   185  	assert.Assert(c, os.MkdirAll(path.Dir(dst), 0o700) == nil)
   186  	f, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o700)
   187  	assert.NilError(c, err)
   188  	defer f.Close()
   189  	// Write content (truncate if it exists)
   190  	_, err = io.Copy(f, strings.NewReader(content))
   191  	assert.NilError(c, err)
   192  }
   193  
   194  // Return the contents of file at path `src`.
   195  // Fail the test when error occurs.
   196  func readFile(src string, c *testing.T) (content string) {
   197  	c.Helper()
   198  	data, err := os.ReadFile(src)
   199  	assert.NilError(c, err)
   200  
   201  	return string(data)
   202  }
   203  
   204  func containerStorageFile(containerID, basename string) string {
   205  	return filepath.Join(testEnv.PlatformDefaults.ContainerStoragePath, containerID, basename)
   206  }
   207  
   208  // docker commands that use this function must be run with the '-d' switch.
   209  func runCommandAndReadContainerFile(c *testing.T, filename string, command string, args ...string) []byte {
   210  	c.Helper()
   211  	result := icmd.RunCommand(command, args...)
   212  	result.Assert(c, icmd.Success)
   213  	contID := strings.TrimSpace(result.Combined())
   214  	cli.WaitRun(c, contID)
   215  	return readContainerFile(c, contID, filename)
   216  }
   217  
   218  func readContainerFile(c *testing.T, containerID, filename string) []byte {
   219  	c.Helper()
   220  	f, err := os.Open(containerStorageFile(containerID, filename))
   221  	assert.NilError(c, err)
   222  	defer f.Close()
   223  
   224  	content, err := io.ReadAll(f)
   225  	assert.NilError(c, err)
   226  	return content
   227  }
   228  
   229  func readContainerFileWithExec(c *testing.T, containerID, filename string) []byte {
   230  	c.Helper()
   231  	result := icmd.RunCommand(dockerBinary, "exec", containerID, "cat", filename)
   232  	result.Assert(c, icmd.Success)
   233  	return []byte(result.Combined())
   234  }
   235  
   236  // daemonTime provides the current time on the daemon host
   237  func daemonTime(c *testing.T) time.Time {
   238  	c.Helper()
   239  	if testEnv.IsLocalDaemon() {
   240  		return time.Now()
   241  	}
   242  	apiClient, err := client.NewClientWithOpts(client.FromEnv)
   243  	assert.NilError(c, err)
   244  	defer apiClient.Close()
   245  
   246  	info, err := apiClient.Info(testutil.GetContext(c))
   247  	assert.NilError(c, err)
   248  
   249  	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
   250  	assert.Assert(c, err == nil, "invalid time format in GET /info response")
   251  	return dt
   252  }
   253  
   254  // daemonUnixTime returns the current time on the daemon host with nanoseconds precision.
   255  // It return the time formatted how the client sends timestamps to the server.
   256  func daemonUnixTime(c *testing.T) string {
   257  	c.Helper()
   258  	return parseEventTime(daemonTime(c))
   259  }
   260  
   261  func parseEventTime(t time.Time) string {
   262  	return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond()))
   263  }
   264  
   265  // appendBaseEnv appends the minimum set of environment variables to exec the
   266  // docker cli binary for testing with correct configuration to the given env
   267  // list.
   268  func appendBaseEnv(isTLS bool, env ...string) []string {
   269  	preserveList := []string{
   270  		// preserve remote test host
   271  		"DOCKER_HOST",
   272  
   273  		// windows: requires preserving SystemRoot, otherwise dial tcp fails
   274  		// with "GetAddrInfoW: A non-recoverable error occurred during a database lookup."
   275  		"SystemRoot",
   276  
   277  		// testing help text requires the $PATH to dockerd is set
   278  		"PATH",
   279  	}
   280  	if isTLS {
   281  		preserveList = append(preserveList, "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH")
   282  	}
   283  
   284  	for _, key := range preserveList {
   285  		if val := os.Getenv(key); val != "" {
   286  			env = append(env, fmt.Sprintf("%s=%s", key, val))
   287  		}
   288  	}
   289  	return env
   290  }
   291  
   292  func createTmpFile(c *testing.T, content string) string {
   293  	c.Helper()
   294  	f, err := os.CreateTemp("", "testfile")
   295  	assert.NilError(c, err)
   296  
   297  	filename := f.Name()
   298  
   299  	err = os.WriteFile(filename, []byte(content), 0o644)
   300  	assert.NilError(c, err)
   301  
   302  	return filename
   303  }
   304  
   305  // waitInspect will wait for the specified container to have the specified string
   306  // in the inspect output. It will wait until the specified timeout (in seconds)
   307  // is reached.
   308  // Deprecated: use cli.WaitFor
   309  func waitInspect(name, expr, expected string, timeout time.Duration) error {
   310  	return daemon.WaitInspectWithArgs(dockerBinary, name, expr, expected, timeout)
   311  }
   312  
   313  func getInspectBody(c *testing.T, version, id string) []byte {
   314  	c.Helper()
   315  	apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(version))
   316  	assert.NilError(c, err)
   317  	defer apiClient.Close()
   318  	_, body, err := apiClient.ContainerInspectWithRaw(testutil.GetContext(c), id, false)
   319  	assert.NilError(c, err)
   320  	return body
   321  }
   322  
   323  // Run a long running idle task in a background container using the
   324  // system-specific default image and command.
   325  func runSleepingContainer(c *testing.T, extraArgs ...string) string {
   326  	c.Helper()
   327  	return runSleepingContainerInImage(c, "busybox", extraArgs...)
   328  }
   329  
   330  // Run a long running idle task in a background container using the specified
   331  // image and the system-specific command.
   332  func runSleepingContainerInImage(c *testing.T, image string, extraArgs ...string) string {
   333  	c.Helper()
   334  	args := []string{"run", "-d"}
   335  	args = append(args, extraArgs...)
   336  	args = append(args, image)
   337  	args = append(args, sleepCommandForDaemonPlatform()...)
   338  	return strings.TrimSpace(cli.DockerCmd(c, args...).Combined())
   339  }
   340  
   341  // minimalBaseImage returns the name of the minimal base image for the current
   342  // daemon platform.
   343  func minimalBaseImage() string {
   344  	return testEnv.PlatformDefaults.BaseImage
   345  }
   346  
   347  func getGoroutineNumber(ctx context.Context, apiClient client.APIClient) (int, error) {
   348  	info, err := apiClient.Info(ctx)
   349  	if err != nil {
   350  		return 0, err
   351  	}
   352  	return info.NGoroutines, nil
   353  }
   354  
   355  func waitForStableGourtineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int {
   356  	var out int
   357  	poll.WaitOn(t, stableGoroutineCount(ctx, apiClient, &out), poll.WithTimeout(30*time.Second))
   358  	return out
   359  }
   360  
   361  func stableGoroutineCount(ctx context.Context, apiClient client.APIClient, count *int) poll.Check {
   362  	var (
   363  		numStable int
   364  		nRoutines int
   365  	)
   366  
   367  	return func(t poll.LogT) poll.Result {
   368  		n, err := getGoroutineNumber(ctx, apiClient)
   369  		if err != nil {
   370  			return poll.Error(err)
   371  		}
   372  
   373  		last := nRoutines
   374  
   375  		if nRoutines == n {
   376  			numStable++
   377  		} else {
   378  			numStable = 0
   379  			nRoutines = n
   380  		}
   381  
   382  		if numStable > 3 {
   383  			*count = n
   384  			return poll.Success()
   385  		}
   386  		return poll.Continue("goroutine count is not stable: last %d, current %d, stable iters: %d", last, n, numStable)
   387  	}
   388  }
   389  
   390  func checkGoroutineCount(ctx context.Context, apiClient client.APIClient, expected int) poll.Check {
   391  	first := true
   392  	return func(t poll.LogT) poll.Result {
   393  		n, err := getGoroutineNumber(ctx, apiClient)
   394  		if err != nil {
   395  			return poll.Error(err)
   396  		}
   397  		if n > expected {
   398  			if first {
   399  				t.Log("Waiting for goroutines to stabilize")
   400  				first = false
   401  			}
   402  			return poll.Continue("exepcted %d goroutines, got %d", expected, n)
   403  		}
   404  		return poll.Success()
   405  	}
   406  }
   407  
   408  func waitForGoroutines(ctx context.Context, t poll.TestingT, apiClient client.APIClient, expected int) {
   409  	poll.WaitOn(t, checkGoroutineCount(ctx, apiClient, expected), poll.WithDelay(500*time.Millisecond), poll.WithTimeout(30*time.Second))
   410  }
   411  
   412  // getErrorMessage returns the error message from an error API response
   413  func getErrorMessage(c *testing.T, body []byte) string {
   414  	c.Helper()
   415  	var resp types.ErrorResponse
   416  	assert.Assert(c, json.Unmarshal(body, &resp) == nil)
   417  	return strings.TrimSpace(resp.Message)
   418  }
   419  
   420  type (
   421  	checkF  func(*testing.T) (interface{}, string)
   422  	reducer func(...interface{}) interface{}
   423  )
   424  
   425  func pollCheck(t *testing.T, f checkF, compare func(x interface{}) assert.BoolOrComparison) poll.Check {
   426  	return func(poll.LogT) poll.Result {
   427  		t.Helper()
   428  		v, comment := f(t)
   429  		r := compare(v)
   430  		switch r := r.(type) {
   431  		case bool:
   432  			if r {
   433  				return poll.Success()
   434  			}
   435  		case cmp.Comparison:
   436  			if r().Success() {
   437  				return poll.Success()
   438  			}
   439  		default:
   440  			panic(fmt.Errorf("pollCheck: type %T not implemented", r))
   441  		}
   442  		return poll.Continue(comment)
   443  	}
   444  }
   445  
   446  func reducedCheck(r reducer, funcs ...checkF) checkF {
   447  	return func(c *testing.T) (interface{}, string) {
   448  		c.Helper()
   449  		var values []interface{}
   450  		var comments []string
   451  		for _, f := range funcs {
   452  			v, comment := f(c)
   453  			values = append(values, v)
   454  			if len(comment) > 0 {
   455  				comments = append(comments, comment)
   456  			}
   457  		}
   458  		return r(values...), fmt.Sprintf("%v", strings.Join(comments, ", "))
   459  	}
   460  }
   461  
   462  func sumAsIntegers(vals ...interface{}) interface{} {
   463  	var s int
   464  	for _, v := range vals {
   465  		s += v.(int)
   466  	}
   467  	return s
   468  }
   469  
   470  func loadSpecialImage(c *testing.T, imageFunc specialimage.SpecialImageFunc) string {
   471  	tmpDir := c.TempDir()
   472  
   473  	imgDir := filepath.Join(tmpDir, "image")
   474  	assert.NilError(c, os.Mkdir(imgDir, 0o755))
   475  
   476  	_, err := imageFunc(imgDir)
   477  	assert.NilError(c, err)
   478  
   479  	rc, err := archive.TarWithOptions(imgDir, &archive.TarOptions{})
   480  	assert.NilError(c, err)
   481  	defer rc.Close()
   482  
   483  	imgTar := filepath.Join(tmpDir, "image.tar")
   484  	tarFile, err := os.OpenFile(imgTar, os.O_CREATE|os.O_WRONLY, 0o644)
   485  	assert.NilError(c, err)
   486  
   487  	defer tarFile.Close()
   488  
   489  	_, err = io.Copy(tarFile, rc)
   490  	assert.NilError(c, err)
   491  
   492  	tarFile.Close()
   493  
   494  	out := cli.DockerCmd(c, "load", "-i", imgTar).Stdout()
   495  
   496  	for _, line := range strings.Split(out, "\n") {
   497  		line := strings.TrimSpace(line)
   498  
   499  		if _, imageID, hasID := strings.Cut(line, "Loaded image ID: "); hasID {
   500  			return imageID
   501  		}
   502  		if _, imageRef, hasRef := strings.Cut(line, "Loaded image: "); hasRef {
   503  			return imageRef
   504  		}
   505  	}
   506  
   507  	c.Fatalf("failed to extract image ref from %q", out)
   508  	return ""
   509  }