Pārlūkot izejas kodu

Merge pull request #45906 from akerouanton/create-with-several-networks

api: Allow ContainerCreate to take several EndpointsConfig for >= API 1.44
Albin Kerouanton 1 gadu atpakaļ
vecāks
revīzija
336e1e9b7e

+ 12 - 0
api/server/router/container/container_routes.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/http"
 	"runtime"
 	"runtime"
 	"strconv"
 	"strconv"
+	"strings"
 
 
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/log"
 	"github.com/containerd/containerd/platforms"
 	"github.com/containerd/containerd/platforms"
@@ -607,6 +608,17 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
 		}
 		}
 	}
 	}
 
 
+	if versions.LessThan(version, "1.44") {
+		// Creating a container connected to several networks is not supported until v1.44.
+		if networkingConfig != nil && len(networkingConfig.EndpointsConfig) > 1 {
+			l := make([]string, 0, len(networkingConfig.EndpointsConfig))
+			for k := range networkingConfig.EndpointsConfig {
+				l = append(l, k)
+			}
+			return errdefs.InvalidParameter(errors.Errorf("Container cannot be created with multiple network endpoints: %s", strings.Join(l, ", ")))
+		}
+	}
+
 	ccr, err := s.backend.ContainerCreate(ctx, types.ContainerCreateConfig{
 	ccr, err := s.backend.ContainerCreate(ctx, types.ContainerCreateConfig{
 		Name:             name,
 		Name:             name,
 		Config:           config,
 		Config:           config,

+ 7 - 5
api/swagger.yaml

@@ -1363,16 +1363,16 @@ definitions:
       EndpointsConfig:
       EndpointsConfig:
         description: |
         description: |
           A mapping of network name to endpoint configuration for that network.
           A mapping of network name to endpoint configuration for that network.
+          The endpoint configuration can be left empty to connect to that
+          network with no particular endpoint configuration.
         type: "object"
         type: "object"
         additionalProperties:
         additionalProperties:
           $ref: "#/definitions/EndpointSettings"
           $ref: "#/definitions/EndpointSettings"
     example:
     example:
       # putting an example here, instead of using the example values from
       # putting an example here, instead of using the example values from
-      # /definitions/EndpointSettings, because containers/create currently
-      # does not support attaching to multiple networks, so the example request
-      # would be confusing if it showed that multiple networks can be contained
-      # in the EndpointsConfig.
-      # TODO remove once we support multiple networks on container create (see https://github.com/moby/moby/blob/07e6b843594e061f82baa5fa23c2ff7d536c2a05/daemon/create.go#L323)
+      # /definitions/EndpointSettings, because EndpointSettings contains
+      # operational data returned when inspecting a container that we don't
+      # accept here.
       EndpointsConfig:
       EndpointsConfig:
         isolated_nw:
         isolated_nw:
           IPAMConfig:
           IPAMConfig:
@@ -1387,6 +1387,7 @@ definitions:
           Aliases:
           Aliases:
             - "server_x"
             - "server_x"
             - "server_y"
             - "server_y"
+        database_nw: {}
 
 
   NetworkSettings:
   NetworkSettings:
     description: "NetworkSettings exposes the network settings in the API"
     description: "NetworkSettings exposes the network settings in the API"
@@ -6439,6 +6440,7 @@ paths:
                     Aliases:
                     Aliases:
                       - "server_x"
                       - "server_x"
                       - "server_y"
                       - "server_y"
+                  database_nw: {}
 
 
           required: true
           required: true
       responses:
       responses:

+ 0 - 12
daemon/cluster/executor/container/adapter.go

@@ -300,18 +300,6 @@ func (c *containerAdapter) create(ctx context.Context) error {
 		return err
 		return err
 	}
 	}
 
 
-	// Docker daemon currently doesn't support multiple networks in container create
-	// Connect to all other networks
-	nc := c.container.connectNetworkingConfig(c.backend)
-
-	if nc != nil {
-		for n, ep := range nc.EndpointsConfig {
-			if err := c.backend.ConnectContainerToNetwork(cr.ID, n, ep); err != nil {
-				return err
-			}
-		}
-	}
-
 	container := c.container.task.Spec.GetContainer()
 	container := c.container.task.Spec.GetContainer()
 	if container == nil {
 	if container == nil {
 		return errors.New("unable to get container from task spec")
 		return errors.New("unable to get container from task spec")

+ 2 - 21
daemon/cluster/executor/container/container.go

@@ -503,7 +503,6 @@ func (c *containerConfig) resources() enginecontainer.Resources {
 	return resources
 	return resources
 }
 }
 
 
-// Docker daemon supports just 1 network during container create.
 func (c *containerConfig) createNetworkingConfig(b executorpkg.Backend) *network.NetworkingConfig {
 func (c *containerConfig) createNetworkingConfig(b executorpkg.Backend) *network.NetworkingConfig {
 	var networks []*api.NetworkAttachment
 	var networks []*api.NetworkAttachment
 	if c.task.Spec.GetContainer() != nil || c.task.Spec.GetAttachment() != nil {
 	if c.task.Spec.GetContainer() != nil || c.task.Spec.GetAttachment() != nil {
@@ -511,28 +510,10 @@ func (c *containerConfig) createNetworkingConfig(b executorpkg.Backend) *network
 	}
 	}
 
 
 	epConfig := make(map[string]*network.EndpointSettings)
 	epConfig := make(map[string]*network.EndpointSettings)
-	if len(networks) > 0 {
-		epConfig[networks[0].Network.Spec.Annotations.Name] = getEndpointConfig(networks[0], b)
-	}
-
-	return &network.NetworkingConfig{EndpointsConfig: epConfig}
-}
-
-// TODO: Merge this function with createNetworkingConfig after daemon supports multiple networks in container create
-func (c *containerConfig) connectNetworkingConfig(b executorpkg.Backend) *network.NetworkingConfig {
-	var networks []*api.NetworkAttachment
-	if c.task.Spec.GetContainer() != nil {
-		networks = c.task.Networks
-	}
-	// First network is used during container create. Other networks are used in "docker network connect"
-	if len(networks) < 2 {
-		return nil
-	}
-
-	epConfig := make(map[string]*network.EndpointSettings)
-	for _, na := range networks[1:] {
+	for _, na := range networks {
 		epConfig[na.Network.Spec.Annotations.Name] = getEndpointConfig(na, b)
 		epConfig[na.Network.Spec.Annotations.Name] = getEndpointConfig(na, b)
 	}
 	}
+
 	return &network.NetworkingConfig{EndpointsConfig: epConfig}
 	return &network.NetworkingConfig{EndpointsConfig: epConfig}
 }
 }
 
 

+ 2 - 10
daemon/create.go

@@ -321,19 +321,11 @@ func (daemon *Daemon) mergeAndVerifyConfig(config *containertypes.Config, img *i
 	return nil
 	return nil
 }
 }
 
 
-// Checks if the client set configurations for more than one network while creating a container
-// Also checks if the IPAMConfig is valid
+// verifyNetworkingConfig validates if the nwConfig is valid.
 func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
 func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
-	if nwConfig == nil || len(nwConfig.EndpointsConfig) == 0 {
+	if nwConfig == nil {
 		return nil
 		return nil
 	}
 	}
-	if len(nwConfig.EndpointsConfig) > 1 {
-		l := make([]string, 0, len(nwConfig.EndpointsConfig))
-		for k := range nwConfig.EndpointsConfig {
-			l = append(l, k)
-		}
-		return fmt.Errorf("container cannot be connected to network endpoints: %s", strings.Join(l, ", "))
-	}
 
 
 	for k, v := range nwConfig.EndpointsConfig {
 	for k, v := range nwConfig.EndpointsConfig {
 		if v == nil {
 		if v == nil {

+ 2 - 0
docs/api/version-history.md

@@ -45,6 +45,8 @@ keywords: "API, Docker, rcli, REST, documentation"
   such, the `CheckDuplicate` field is now deprecated. Note that this change is
   such, the `CheckDuplicate` field is now deprecated. Note that this change is
   _unversioned_ and applied to all API versions on daemon that support version
   _unversioned_ and applied to all API versions on daemon that support version
   1.44.
   1.44.
+* `POST /containers/create` now accepts multiple `EndpointSettings` in
+  `NetworkingConfig.EndpointSettings`.
 
 
 ## v1.43 API changes
 ## v1.43 API changes
 
 

+ 0 - 27
integration-cli/docker_api_containers_test.go

@@ -556,33 +556,6 @@ func (s *DockerAPISuite) TestContainerAPICreateEmptyConfig(c *testing.T) {
 	assert.ErrorContains(c, err, "no command specified")
 	assert.ErrorContains(c, err, "no command specified")
 }
 }
 
 
-func (s *DockerAPISuite) TestContainerAPICreateMultipleNetworksConfig(c *testing.T) {
-	// Container creation must fail if client specified configurations for more than one network
-	config := container.Config{
-		Image: "busybox",
-	}
-
-	networkingConfig := network.NetworkingConfig{
-		EndpointsConfig: map[string]*network.EndpointSettings{
-			"net1": {},
-			"net2": {},
-			"net3": {},
-		},
-	}
-
-	apiClient, err := client.NewClientWithOpts(client.FromEnv)
-	assert.NilError(c, err)
-	defer apiClient.Close()
-
-	_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &container.HostConfig{}, &networkingConfig, nil, "")
-	msg := err.Error()
-	// network name order in error message is not deterministic
-	assert.Assert(c, strings.Contains(msg, "container cannot be connected to network endpoints"))
-	assert.Assert(c, strings.Contains(msg, "net1"))
-	assert.Assert(c, strings.Contains(msg, "net2"))
-	assert.Assert(c, strings.Contains(msg, "net3"))
-}
-
 func (s *DockerAPISuite) TestContainerAPICreateBridgeNetworkMode(c *testing.T) {
 func (s *DockerAPISuite) TestContainerAPICreateBridgeNetworkMode(c *testing.T) {
 	// Windows does not support bridge
 	// Windows does not support bridge
 	testRequires(c, DaemonIsLinux)
 	testRequires(c, DaemonIsLinux)

+ 37 - 0
integration/container/create_test.go

@@ -13,6 +13,7 @@ import (
 	containertypes "github.com/docker/docker/api/types/container"
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/api/types/versions"
+	"github.com/docker/docker/client"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/errdefs"
 	ctr "github.com/docker/docker/integration/internal/container"
 	ctr "github.com/docker/docker/integration/internal/container"
 	"github.com/docker/docker/oci"
 	"github.com/docker/docker/oci"
@@ -585,3 +586,39 @@ func TestCreateInvalidHostConfig(t *testing.T) {
 		})
 		})
 	}
 	}
 }
 }
+
+func TestCreateWithMultipleEndpointSettings(t *testing.T) {
+	ctx := setupTest(t)
+
+	testcases := []struct {
+		apiVersion  string
+		expectedErr string
+	}{
+		{apiVersion: "1.44"},
+		{apiVersion: "1.43", expectedErr: "Container cannot be created with multiple network endpoints"},
+	}
+
+	for _, tc := range testcases {
+		t.Run("with API v"+tc.apiVersion, func(t *testing.T) {
+			apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(tc.apiVersion))
+			assert.NilError(t, err)
+
+			config := container.Config{
+				Image: "busybox",
+			}
+			networkingConfig := network.NetworkingConfig{
+				EndpointsConfig: map[string]*network.EndpointSettings{
+					"net1": {},
+					"net2": {},
+					"net3": {},
+				},
+			}
+			_, err = apiClient.ContainerCreate(ctx, &config, &container.HostConfig{}, &networkingConfig, nil, "")
+			if tc.expectedErr == "" {
+				assert.NilError(t, err)
+			} else {
+				assert.ErrorContains(t, err, tc.expectedErr)
+			}
+		})
+	}
+}

+ 11 - 0
integration/internal/container/ops.go

@@ -146,6 +146,17 @@ func WithIPv6(networkName, ip string) func(*TestContainerConfig) {
 	}
 	}
 }
 }
 
 
+func WithEndpointSettings(nw string, config *network.EndpointSettings) func(*TestContainerConfig) {
+	return func(c *TestContainerConfig) {
+		if c.NetworkingConfig.EndpointsConfig == nil {
+			c.NetworkingConfig.EndpointsConfig = map[string]*network.EndpointSettings{}
+		}
+		if _, ok := c.NetworkingConfig.EndpointsConfig[nw]; !ok {
+			c.NetworkingConfig.EndpointsConfig[nw] = config
+		}
+	}
+}
+
 // WithLogDriver sets the log driver to use for the container
 // WithLogDriver sets the log driver to use for the container
 func WithLogDriver(driver string) func(*TestContainerConfig) {
 func WithLogDriver(driver string) func(*TestContainerConfig) {
 	return func(c *TestContainerConfig) {
 	return func(c *TestContainerConfig) {

+ 7 - 0
integration/internal/network/network.go

@@ -33,3 +33,10 @@ func CreateNoError(ctx context.Context, t *testing.T, client client.APIClient, n
 	assert.NilError(t, err)
 	assert.NilError(t, err)
 	return name
 	return name
 }
 }
+
+func RemoveNoError(ctx context.Context, t *testing.T, apiClient client.APIClient, name string) {
+	t.Helper()
+
+	err := apiClient.NetworkRemove(ctx, name)
+	assert.NilError(t, err)
+}

+ 45 - 0
integration/network/bridge_test.go

@@ -0,0 +1,45 @@
+package network
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	networktypes "github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/api/types/versions"
+	ctr "github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/integration/internal/network"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/skip"
+)
+
+func TestCreateWithMultiNetworks(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows")
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44")
+
+	ctx := setupTest(t)
+	apiClient := testEnv.APIClient()
+
+	network.CreateNoError(ctx, t, apiClient, "testnet1")
+	defer network.RemoveNoError(ctx, t, apiClient, "testnet1")
+
+	network.CreateNoError(ctx, t, apiClient, "testnet2")
+	defer network.RemoveNoError(ctx, t, apiClient, "testnet2")
+
+	attachCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
+	defer cancel()
+	res := ctr.RunAttach(attachCtx, t, apiClient,
+		ctr.WithCmd("ip", "-o", "-4", "addr", "show"),
+		ctr.WithNetworkMode("testnet1"),
+		ctr.WithEndpointSettings("testnet1", &networktypes.EndpointSettings{}),
+		ctr.WithEndpointSettings("testnet2", &networktypes.EndpointSettings{}))
+
+	assert.Equal(t, res.ExitCode, 0)
+	assert.Equal(t, res.Stderr.String(), "")
+
+	// Only interfaces with an IPv4 address are printed by iproute2 when flag -4 is specified. Here, we should have two
+	// interfaces for testnet1 and testnet2, plus lo.
+	ifacesWithAddress := strings.Count(res.Stdout.String(), "\n")
+	assert.Equal(t, ifacesWithAddress, 3)
+}