github.com/moby/docker@v26.1.3+incompatible/integration/container/create_test.go (about)

     1  package container // import "github.com/docker/docker/integration/container"
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/api/types/network"
    15  	"github.com/docker/docker/api/types/versions"
    16  	"github.com/docker/docker/client"
    17  	"github.com/docker/docker/errdefs"
    18  	ctr "github.com/docker/docker/integration/internal/container"
    19  	net "github.com/docker/docker/integration/internal/network"
    20  	"github.com/docker/docker/oci"
    21  	"github.com/docker/docker/testutil"
    22  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    23  	"gotest.tools/v3/assert"
    24  	is "gotest.tools/v3/assert/cmp"
    25  	"gotest.tools/v3/poll"
    26  	"gotest.tools/v3/skip"
    27  )
    28  
    29  func TestCreateFailsWhenIdentifierDoesNotExist(t *testing.T) {
    30  	ctx := setupTest(t)
    31  	client := testEnv.APIClient()
    32  
    33  	testCases := []struct {
    34  		doc           string
    35  		image         string
    36  		expectedError string
    37  	}{
    38  		{
    39  			doc:           "image and tag",
    40  			image:         "test456:v1",
    41  			expectedError: "No such image: test456:v1",
    42  		},
    43  		{
    44  			doc:           "image no tag",
    45  			image:         "test456",
    46  			expectedError: "No such image: test456",
    47  		},
    48  		{
    49  			doc:           "digest",
    50  			image:         "sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa",
    51  			expectedError: "No such image: sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa",
    52  		},
    53  	}
    54  
    55  	for _, tc := range testCases {
    56  		tc := tc
    57  		t.Run(tc.doc, func(t *testing.T) {
    58  			t.Parallel()
    59  			ctx := testutil.StartSpan(ctx, t)
    60  			_, err := client.ContainerCreate(ctx,
    61  				&container.Config{Image: tc.image},
    62  				&container.HostConfig{},
    63  				&network.NetworkingConfig{},
    64  				nil,
    65  				"",
    66  			)
    67  			assert.Check(t, is.ErrorContains(err, tc.expectedError))
    68  			assert.Check(t, errdefs.IsNotFound(err))
    69  		})
    70  	}
    71  }
    72  
    73  // TestCreateLinkToNonExistingContainer verifies that linking to a non-existing
    74  // container returns an "invalid parameter" (400) status, and not the underlying
    75  // "non exists" (404).
    76  func TestCreateLinkToNonExistingContainer(t *testing.T) {
    77  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "legacy links are not supported on windows")
    78  	ctx := setupTest(t)
    79  	c := testEnv.APIClient()
    80  
    81  	_, err := c.ContainerCreate(ctx,
    82  		&container.Config{
    83  			Image: "busybox",
    84  		},
    85  		&container.HostConfig{
    86  			Links: []string{"no-such-container"},
    87  		},
    88  		&network.NetworkingConfig{},
    89  		nil,
    90  		"",
    91  	)
    92  	assert.Check(t, is.ErrorContains(err, "could not get container for no-such-container"))
    93  	assert.Check(t, errdefs.IsInvalidParameter(err))
    94  }
    95  
    96  func TestCreateWithInvalidEnv(t *testing.T) {
    97  	ctx := setupTest(t)
    98  	client := testEnv.APIClient()
    99  
   100  	testCases := []struct {
   101  		env           string
   102  		expectedError string
   103  	}{
   104  		{
   105  			env:           "",
   106  			expectedError: "invalid environment variable:",
   107  		},
   108  		{
   109  			env:           "=",
   110  			expectedError: "invalid environment variable: =",
   111  		},
   112  		{
   113  			env:           "=foo",
   114  			expectedError: "invalid environment variable: =foo",
   115  		},
   116  	}
   117  
   118  	for index, tc := range testCases {
   119  		tc := tc
   120  		t.Run(strconv.Itoa(index), func(t *testing.T) {
   121  			t.Parallel()
   122  			ctx := testutil.StartSpan(ctx, t)
   123  			_, err := client.ContainerCreate(ctx,
   124  				&container.Config{
   125  					Image: "busybox",
   126  					Env:   []string{tc.env},
   127  				},
   128  				&container.HostConfig{},
   129  				&network.NetworkingConfig{},
   130  				nil,
   131  				"",
   132  			)
   133  			assert.Check(t, is.ErrorContains(err, tc.expectedError))
   134  			assert.Check(t, errdefs.IsInvalidParameter(err))
   135  		})
   136  	}
   137  }
   138  
   139  // Test case for #30166 (target was not validated)
   140  func TestCreateTmpfsMountsTarget(t *testing.T) {
   141  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   142  	ctx := setupTest(t)
   143  
   144  	client := testEnv.APIClient()
   145  
   146  	testCases := []struct {
   147  		target        string
   148  		expectedError string
   149  	}{
   150  		{
   151  			target:        ".",
   152  			expectedError: "mount path must be absolute",
   153  		},
   154  		{
   155  			target:        "foo",
   156  			expectedError: "mount path must be absolute",
   157  		},
   158  		{
   159  			target:        "/",
   160  			expectedError: "destination can't be '/'",
   161  		},
   162  		{
   163  			target:        "//",
   164  			expectedError: "destination can't be '/'",
   165  		},
   166  	}
   167  
   168  	for _, tc := range testCases {
   169  		_, err := client.ContainerCreate(ctx,
   170  			&container.Config{
   171  				Image: "busybox",
   172  			},
   173  			&container.HostConfig{
   174  				Tmpfs: map[string]string{tc.target: ""},
   175  			},
   176  			&network.NetworkingConfig{},
   177  			nil,
   178  			"",
   179  		)
   180  		assert.Check(t, is.ErrorContains(err, tc.expectedError))
   181  		assert.Check(t, errdefs.IsInvalidParameter(err))
   182  	}
   183  }
   184  
   185  func TestCreateWithCustomMaskedPaths(t *testing.T) {
   186  	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
   187  
   188  	ctx := setupTest(t)
   189  	apiClient := testEnv.APIClient()
   190  
   191  	testCases := []struct {
   192  		maskedPaths []string
   193  		expected    []string
   194  	}{
   195  		{
   196  			maskedPaths: []string{},
   197  			expected:    []string{},
   198  		},
   199  		{
   200  			maskedPaths: nil,
   201  			expected:    oci.DefaultSpec().Linux.MaskedPaths,
   202  		},
   203  		{
   204  			maskedPaths: []string{"/proc/kcore", "/proc/keys"},
   205  			expected:    []string{"/proc/kcore", "/proc/keys"},
   206  		},
   207  	}
   208  
   209  	checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) {
   210  		_, b, err := apiClient.ContainerInspectWithRaw(ctx, name, false)
   211  		assert.NilError(t, err)
   212  
   213  		var inspectJSON map[string]interface{}
   214  		err = json.Unmarshal(b, &inspectJSON)
   215  		assert.NilError(t, err)
   216  
   217  		cfg, ok := inspectJSON["HostConfig"].(map[string]interface{})
   218  		assert.Check(t, is.Equal(true, ok), name)
   219  
   220  		maskedPaths, ok := cfg["MaskedPaths"].([]interface{})
   221  		assert.Check(t, is.Equal(true, ok), name)
   222  
   223  		mps := []string{}
   224  		for _, mp := range maskedPaths {
   225  			mps = append(mps, mp.(string))
   226  		}
   227  
   228  		assert.DeepEqual(t, expected, mps)
   229  	}
   230  
   231  	// TODO: This should be using subtests
   232  
   233  	for i, tc := range testCases {
   234  		name := fmt.Sprintf("create-masked-paths-%d", i)
   235  		config := container.Config{
   236  			Image: "busybox",
   237  			Cmd:   []string{"true"},
   238  		}
   239  		hc := container.HostConfig{}
   240  		if tc.maskedPaths != nil {
   241  			hc.MaskedPaths = tc.maskedPaths
   242  		}
   243  
   244  		// Create the container.
   245  		c, err := apiClient.ContainerCreate(ctx,
   246  			&config,
   247  			&hc,
   248  			&network.NetworkingConfig{},
   249  			nil,
   250  			name,
   251  		)
   252  		assert.NilError(t, err)
   253  
   254  		checkInspect(t, ctx, name, tc.expected)
   255  
   256  		// Start the container.
   257  		err = apiClient.ContainerStart(ctx, c.ID, container.StartOptions{})
   258  		assert.NilError(t, err)
   259  
   260  		poll.WaitOn(t, ctr.IsInState(ctx, apiClient, c.ID, "exited"), poll.WithDelay(100*time.Millisecond))
   261  
   262  		checkInspect(t, ctx, name, tc.expected)
   263  	}
   264  }
   265  
   266  func TestCreateWithCustomReadonlyPaths(t *testing.T) {
   267  	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
   268  
   269  	ctx := setupTest(t)
   270  	apiClient := testEnv.APIClient()
   271  
   272  	testCases := []struct {
   273  		readonlyPaths []string
   274  		expected      []string
   275  	}{
   276  		{
   277  			readonlyPaths: []string{},
   278  			expected:      []string{},
   279  		},
   280  		{
   281  			readonlyPaths: nil,
   282  			expected:      oci.DefaultSpec().Linux.ReadonlyPaths,
   283  		},
   284  		{
   285  			readonlyPaths: []string{"/proc/asound", "/proc/bus"},
   286  			expected:      []string{"/proc/asound", "/proc/bus"},
   287  		},
   288  	}
   289  
   290  	checkInspect := func(t *testing.T, ctx context.Context, name string, expected []string) {
   291  		_, b, err := apiClient.ContainerInspectWithRaw(ctx, name, false)
   292  		assert.NilError(t, err)
   293  
   294  		var inspectJSON map[string]interface{}
   295  		err = json.Unmarshal(b, &inspectJSON)
   296  		assert.NilError(t, err)
   297  
   298  		cfg, ok := inspectJSON["HostConfig"].(map[string]interface{})
   299  		assert.Check(t, is.Equal(true, ok), name)
   300  
   301  		readonlyPaths, ok := cfg["ReadonlyPaths"].([]interface{})
   302  		assert.Check(t, is.Equal(true, ok), name)
   303  
   304  		rops := []string{}
   305  		for _, rop := range readonlyPaths {
   306  			rops = append(rops, rop.(string))
   307  		}
   308  		assert.DeepEqual(t, expected, rops)
   309  	}
   310  
   311  	for i, tc := range testCases {
   312  		name := fmt.Sprintf("create-readonly-paths-%d", i)
   313  		config := container.Config{
   314  			Image: "busybox",
   315  			Cmd:   []string{"true"},
   316  		}
   317  		hc := container.HostConfig{}
   318  		if tc.readonlyPaths != nil {
   319  			hc.ReadonlyPaths = tc.readonlyPaths
   320  		}
   321  
   322  		// Create the container.
   323  		c, err := apiClient.ContainerCreate(ctx,
   324  			&config,
   325  			&hc,
   326  			&network.NetworkingConfig{},
   327  			nil,
   328  			name,
   329  		)
   330  		assert.NilError(t, err)
   331  
   332  		checkInspect(t, ctx, name, tc.expected)
   333  
   334  		// Start the container.
   335  		err = apiClient.ContainerStart(ctx, c.ID, container.StartOptions{})
   336  		assert.NilError(t, err)
   337  
   338  		poll.WaitOn(t, ctr.IsInState(ctx, apiClient, c.ID, "exited"), poll.WithDelay(100*time.Millisecond))
   339  
   340  		checkInspect(t, ctx, name, tc.expected)
   341  	}
   342  }
   343  
   344  func TestCreateWithInvalidHealthcheckParams(t *testing.T) {
   345  	ctx := setupTest(t)
   346  	apiClient := testEnv.APIClient()
   347  
   348  	testCases := []struct {
   349  		doc           string
   350  		interval      time.Duration
   351  		timeout       time.Duration
   352  		retries       int
   353  		startPeriod   time.Duration
   354  		startInterval time.Duration
   355  		expectedErr   string
   356  	}{
   357  		{
   358  			doc:         "test invalid Interval in Healthcheck: less than 0s",
   359  			interval:    -10 * time.Millisecond,
   360  			timeout:     time.Second,
   361  			retries:     1000,
   362  			expectedErr: fmt.Sprintf("Interval in Healthcheck cannot be less than %s", container.MinimumDuration),
   363  		},
   364  		{
   365  			doc:         "test invalid Interval in Healthcheck: larger than 0s but less than 1ms",
   366  			interval:    500 * time.Microsecond,
   367  			timeout:     time.Second,
   368  			retries:     1000,
   369  			expectedErr: fmt.Sprintf("Interval in Healthcheck cannot be less than %s", container.MinimumDuration),
   370  		},
   371  		{
   372  			doc:         "test invalid Timeout in Healthcheck: less than 1ms",
   373  			interval:    time.Second,
   374  			timeout:     -100 * time.Millisecond,
   375  			retries:     1000,
   376  			expectedErr: fmt.Sprintf("Timeout in Healthcheck cannot be less than %s", container.MinimumDuration),
   377  		},
   378  		{
   379  			doc:         "test invalid Retries in Healthcheck: less than 0",
   380  			interval:    time.Second,
   381  			timeout:     time.Second,
   382  			retries:     -10,
   383  			expectedErr: "Retries in Healthcheck cannot be negative",
   384  		},
   385  		{
   386  			doc:         "test invalid StartPeriod in Healthcheck: not 0 and less than 1ms",
   387  			interval:    time.Second,
   388  			timeout:     time.Second,
   389  			retries:     1000,
   390  			startPeriod: 100 * time.Microsecond,
   391  			expectedErr: fmt.Sprintf("StartPeriod in Healthcheck cannot be less than %s", container.MinimumDuration),
   392  		},
   393  		{
   394  			doc:           "test invalid StartInterval in Healthcheck: not 0 and less than 1ms",
   395  			interval:      time.Second,
   396  			timeout:       time.Second,
   397  			retries:       1000,
   398  			startPeriod:   time.Second,
   399  			startInterval: 100 * time.Microsecond,
   400  			expectedErr:   fmt.Sprintf("StartInterval in Healthcheck cannot be less than %s", container.MinimumDuration),
   401  		},
   402  	}
   403  
   404  	for _, tc := range testCases {
   405  		tc := tc
   406  		t.Run(tc.doc, func(t *testing.T) {
   407  			t.Parallel()
   408  			ctx := testutil.StartSpan(ctx, t)
   409  			cfg := container.Config{
   410  				Image: "busybox",
   411  				Healthcheck: &container.HealthConfig{
   412  					Interval:      tc.interval,
   413  					Timeout:       tc.timeout,
   414  					Retries:       tc.retries,
   415  					StartInterval: tc.startInterval,
   416  				},
   417  			}
   418  			if tc.startPeriod != 0 {
   419  				cfg.Healthcheck.StartPeriod = tc.startPeriod
   420  			}
   421  
   422  			resp, err := apiClient.ContainerCreate(ctx, &cfg, &container.HostConfig{}, nil, nil, "")
   423  			assert.Check(t, is.Equal(len(resp.Warnings), 0))
   424  			assert.Check(t, errdefs.IsInvalidParameter(err))
   425  			assert.ErrorContains(t, err, tc.expectedErr)
   426  		})
   427  	}
   428  }
   429  
   430  // Make sure that anonymous volumes can be overritten by tmpfs
   431  // https://github.com/moby/moby/issues/40446
   432  func TestCreateTmpfsOverrideAnonymousVolume(t *testing.T) {
   433  	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "windows does not support tmpfs")
   434  	ctx := setupTest(t)
   435  	apiClient := testEnv.APIClient()
   436  
   437  	id := ctr.Create(ctx, t, apiClient,
   438  		ctr.WithVolume("/foo"),
   439  		ctr.WithTmpfs("/foo"),
   440  		ctr.WithVolume("/bar"),
   441  		ctr.WithTmpfs("/bar:size=999"),
   442  		ctr.WithCmd("/bin/sh", "-c", "mount | grep '/foo' | grep tmpfs && mount | grep '/bar' | grep tmpfs"),
   443  	)
   444  
   445  	defer func() {
   446  		err := apiClient.ContainerRemove(ctx, id, container.RemoveOptions{Force: true})
   447  		assert.NilError(t, err)
   448  	}()
   449  
   450  	inspect, err := apiClient.ContainerInspect(ctx, id)
   451  	assert.NilError(t, err)
   452  	// tmpfs do not currently get added to inspect.Mounts
   453  	// Normally an anonymous volume would, except now tmpfs should prevent that.
   454  	assert.Assert(t, is.Len(inspect.Mounts, 0))
   455  
   456  	chWait, chErr := apiClient.ContainerWait(ctx, id, container.WaitConditionNextExit)
   457  	assert.NilError(t, apiClient.ContainerStart(ctx, id, container.StartOptions{}))
   458  
   459  	timeout := time.NewTimer(30 * time.Second)
   460  	defer timeout.Stop()
   461  
   462  	select {
   463  	case <-timeout.C:
   464  		t.Fatal("timeout waiting for container to exit")
   465  	case status := <-chWait:
   466  		var errMsg string
   467  		if status.Error != nil {
   468  			errMsg = status.Error.Message
   469  		}
   470  		assert.Equal(t, int(status.StatusCode), 0, errMsg)
   471  	case err := <-chErr:
   472  		assert.NilError(t, err)
   473  	}
   474  }
   475  
   476  // Test that if the referenced image platform does not match the requested platform on container create that we get an
   477  // error.
   478  func TestCreateDifferentPlatform(t *testing.T) {
   479  	ctx := setupTest(t)
   480  	apiClient := testEnv.APIClient()
   481  
   482  	img, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:latest")
   483  	assert.NilError(t, err)
   484  	assert.Assert(t, img.Architecture != "")
   485  
   486  	t.Run("different os", func(t *testing.T) {
   487  		ctx := testutil.StartSpan(ctx, t)
   488  		p := ocispec.Platform{
   489  			OS:           img.Os + "DifferentOS",
   490  			Architecture: img.Architecture,
   491  			Variant:      img.Variant,
   492  		}
   493  		_, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "")
   494  		assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
   495  	})
   496  	t.Run("different cpu arch", func(t *testing.T) {
   497  		ctx := testutil.StartSpan(ctx, t)
   498  		p := ocispec.Platform{
   499  			OS:           img.Os,
   500  			Architecture: img.Architecture + "DifferentArch",
   501  			Variant:      img.Variant,
   502  		}
   503  		_, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "")
   504  		assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
   505  	})
   506  }
   507  
   508  func TestCreateVolumesFromNonExistingContainer(t *testing.T) {
   509  	ctx := setupTest(t)
   510  	cli := testEnv.APIClient()
   511  
   512  	_, err := cli.ContainerCreate(
   513  		ctx,
   514  		&container.Config{Image: "busybox"},
   515  		&container.HostConfig{VolumesFrom: []string{"nosuchcontainer"}},
   516  		nil,
   517  		nil,
   518  		"",
   519  	)
   520  	assert.Check(t, errdefs.IsInvalidParameter(err))
   521  }
   522  
   523  // Test that we can create a container from an image that is for a different platform even if a platform was not specified
   524  // This is for the regression detailed here: https://github.com/moby/moby/issues/41552
   525  func TestCreatePlatformSpecificImageNoPlatform(t *testing.T) {
   526  	ctx := setupTest(t)
   527  
   528  	skip.If(t, testEnv.DaemonInfo.Architecture == "arm", "test only makes sense to run on non-arm systems")
   529  	skip.If(t, testEnv.DaemonInfo.OSType != "linux", "test image is only available on linux")
   530  	cli := testEnv.APIClient()
   531  
   532  	_, err := cli.ContainerCreate(
   533  		ctx,
   534  		&container.Config{Image: "arm32v7/hello-world"},
   535  		&container.HostConfig{},
   536  		nil,
   537  		nil,
   538  		"",
   539  	)
   540  	assert.NilError(t, err)
   541  }
   542  
   543  func TestCreateInvalidHostConfig(t *testing.T) {
   544  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   545  
   546  	ctx := setupTest(t)
   547  	apiClient := testEnv.APIClient()
   548  
   549  	testCases := []struct {
   550  		doc         string
   551  		hc          container.HostConfig
   552  		expectedErr string
   553  	}{
   554  		{
   555  			doc:         "invalid IpcMode",
   556  			hc:          container.HostConfig{IpcMode: "invalid"},
   557  			expectedErr: "Error response from daemon: invalid IPC mode: invalid",
   558  		},
   559  		{
   560  			doc:         "invalid PidMode",
   561  			hc:          container.HostConfig{PidMode: "invalid"},
   562  			expectedErr: "Error response from daemon: invalid PID mode: invalid",
   563  		},
   564  		{
   565  			doc:         "invalid PidMode without container ID",
   566  			hc:          container.HostConfig{PidMode: "container"},
   567  			expectedErr: "Error response from daemon: invalid PID mode: container",
   568  		},
   569  		{
   570  			doc:         "invalid UTSMode",
   571  			hc:          container.HostConfig{UTSMode: "invalid"},
   572  			expectedErr: "Error response from daemon: invalid UTS mode: invalid",
   573  		},
   574  		{
   575  			doc:         "invalid Annotations",
   576  			hc:          container.HostConfig{Annotations: map[string]string{"": "a"}},
   577  			expectedErr: "Error response from daemon: invalid Annotations: the empty string is not permitted as an annotation key",
   578  		},
   579  	}
   580  
   581  	for _, tc := range testCases {
   582  		tc := tc
   583  		t.Run(tc.doc, func(t *testing.T) {
   584  			t.Parallel()
   585  			ctx := testutil.StartSpan(ctx, t)
   586  			cfg := container.Config{
   587  				Image: "busybox",
   588  			}
   589  			resp, err := apiClient.ContainerCreate(ctx, &cfg, &tc.hc, nil, nil, "")
   590  			assert.Check(t, is.Equal(len(resp.Warnings), 0))
   591  			assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err)
   592  			assert.Error(t, err, tc.expectedErr)
   593  		})
   594  	}
   595  }
   596  
   597  func TestCreateWithMultipleEndpointSettings(t *testing.T) {
   598  	ctx := setupTest(t)
   599  
   600  	testcases := []struct {
   601  		apiVersion  string
   602  		expectedErr string
   603  	}{
   604  		{apiVersion: "1.44"},
   605  		{apiVersion: "1.43", expectedErr: "Container cannot be created with multiple network endpoints"},
   606  	}
   607  
   608  	for _, tc := range testcases {
   609  		t.Run("with API v"+tc.apiVersion, func(t *testing.T) {
   610  			apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(tc.apiVersion))
   611  			assert.NilError(t, err)
   612  
   613  			config := container.Config{
   614  				Image: "busybox",
   615  			}
   616  			networkingConfig := network.NetworkingConfig{
   617  				EndpointsConfig: map[string]*network.EndpointSettings{
   618  					"net1": {},
   619  					"net2": {},
   620  					"net3": {},
   621  				},
   622  			}
   623  			_, err = apiClient.ContainerCreate(ctx, &config, &container.HostConfig{}, &networkingConfig, nil, "")
   624  			if tc.expectedErr == "" {
   625  				assert.NilError(t, err)
   626  			} else {
   627  				assert.ErrorContains(t, err, tc.expectedErr)
   628  			}
   629  		})
   630  	}
   631  }
   632  
   633  func TestCreateWithCustomMACs(t *testing.T) {
   634  	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
   635  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44")
   636  
   637  	ctx := setupTest(t)
   638  	apiClient := testEnv.APIClient()
   639  
   640  	net.CreateNoError(ctx, t, apiClient, "testnet")
   641  
   642  	attachCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
   643  	defer cancel()
   644  	res := ctr.RunAttach(attachCtx, t, apiClient,
   645  		ctr.WithCmd("ip", "-o", "link", "show"),
   646  		ctr.WithNetworkMode("bridge"),
   647  		ctr.WithMacAddress("bridge", "02:32:1c:23:00:04"))
   648  
   649  	assert.Equal(t, res.ExitCode, 0)
   650  	assert.Equal(t, res.Stderr.String(), "")
   651  
   652  	scanner := bufio.NewScanner(res.Stdout)
   653  	for scanner.Scan() {
   654  		fields := strings.Fields(scanner.Text())
   655  		// The expected output is:
   656  		// 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000\    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   657  		// 134: eth0@if135: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1400 qdisc noqueue \    link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
   658  		if len(fields) < 11 {
   659  			continue
   660  		}
   661  
   662  		ifaceName := fields[1]
   663  		if ifaceName[:3] != "eth" {
   664  			continue
   665  		}
   666  
   667  		mac := fields[len(fields)-3]
   668  		assert.Equal(t, mac, "02:32:1c:23:00:04")
   669  	}
   670  }