diff --git a/api/client/create.go b/api/client/create.go index 5f4f17c313..7b288b0f31 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -13,6 +13,7 @@ import ( "github.com/docker/engine-api/client" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" ) func (cli *DockerCli) pullImage(image string) error { @@ -79,7 +80,7 @@ func newCIDFile(path string) (*cidFile, error) { return &cidFile{path: path, file: f}, nil } -func (cli *DockerCli) createContainer(config *container.Config, hostConfig *container.HostConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { +func (cli *DockerCli) createContainer(config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { var containerIDFile *cidFile if cidfile != "" { var err error @@ -107,7 +108,8 @@ func (cli *DockerCli) createContainer(config *container.Config, hostConfig *cont } //create the container - response, err := cli.client.ContainerCreate(config, hostConfig, nil, name) + response, err := cli.client.ContainerCreate(config, hostConfig, networkingConfig, name) + //if image not found try to pull it if err != nil { if client.IsErrImageNotFound(err) { @@ -124,7 +126,7 @@ func (cli *DockerCli) createContainer(config *container.Config, hostConfig *cont } // Retry var retryErr error - response, retryErr = cli.client.ContainerCreate(config, hostConfig, nil, name) + response, retryErr = cli.client.ContainerCreate(config, hostConfig, networkingConfig, name) if retryErr != nil { return nil, retryErr } @@ -156,7 +158,8 @@ func (cli *DockerCli) CmdCreate(args ...string) error { flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") ) - config, hostConfig, cmd, err := runconfigopts.Parse(cmd, args) + config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + if err != nil { cmd.ReportError(err.Error(), true) os.Exit(1) @@ -165,7 +168,7 @@ func (cli *DockerCli) CmdCreate(args ...string) error { cmd.Usage() return nil } - response, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName) + response, err := cli.createContainer(config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) if err != nil { return err } diff --git a/api/client/network.go b/api/client/network.go index b0facb270a..393a2494bd 100644 --- a/api/client/network.go +++ b/api/client/network.go @@ -107,15 +107,22 @@ func (cli *DockerCli) CmdNetworkRm(args ...string) error { // CmdNetworkConnect connects a container to a network // -// Usage: docker network connect +// Usage: docker network connect [OPTIONS] func (cli *DockerCli) CmdNetworkConnect(args ...string) error { cmd := Cli.Subcmd("network connect", []string{"NETWORK CONTAINER"}, "Connects a container to a network", false) - cmd.Require(flag.Exact, 2) + flIPAddress := cmd.String([]string{"-ip"}, "", "IP Address") + flIPv6Address := cmd.String([]string{"-ip6"}, "", "IPv6 Address") + cmd.Require(flag.Min, 2) if err := cmd.ParseFlags(args, true); err != nil { return err } - - return cli.client.NetworkConnect(cmd.Arg(0), cmd.Arg(1), nil) + epConfig := &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: *flIPAddress, + IPv6Address: *flIPv6Address, + }, + } + return cli.client.NetworkConnect(cmd.Arg(0), cmd.Arg(1), epConfig) } // CmdNetworkDisconnect disconnects a container from a network diff --git a/api/client/run.go b/api/client/run.go index 3eea9dc667..dcd7f01f6b 100644 --- a/api/client/run.go +++ b/api/client/run.go @@ -82,7 +82,8 @@ func (cli *DockerCli) CmdRun(args ...string) error { ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d") ) - config, hostConfig, cmd, err := runconfigopts.Parse(cmd, args) + config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + // just in case the Parse does not exit if err != nil { cmd.ReportError(err.Error(), true) @@ -145,7 +146,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = cli.getTtySize() } - createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName) + createResponse, err := cli.createContainer(config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) if err != nil { cmd.ReportError(err.Error(), true) return runStartContainerErr(err) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 8a7de24fe1..ee2f1ef2f8 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -332,7 +332,7 @@ func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.Respon return err } - _, hostConfig, err := runconfig.DecodeContainerConfig(r.Body) + _, hostConfig, _, err := runconfig.DecodeContainerConfig(r.Body) if err != nil { return err } @@ -358,7 +358,7 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo name := r.Form.Get("name") - config, hostConfig, err := runconfig.DecodeContainerConfig(r.Body) + config, hostConfig, networkingConfig, err := runconfig.DecodeContainerConfig(r.Body) if err != nil { return err } @@ -366,10 +366,11 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo adjustCPUShares := version.LessThan("1.19") ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{ - Name: name, - Config: config, - HostConfig: hostConfig, - AdjustCPUShares: adjustCPUShares, + Name: name, + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + AdjustCPUShares: adjustCPUShares, }) if err != nil { return err diff --git a/api/server/router/local/image.go b/api/server/router/local/image.go index b43c8ddc2f..c1d1e830db 100644 --- a/api/server/router/local/image.go +++ b/api/server/router/local/image.go @@ -39,7 +39,7 @@ func (s *router) postCommit(ctx context.Context, w http.ResponseWriter, r *http. pause = true } - c, _, err := runconfig.DecodeContainerConfig(r.Body) + c, _, _, err := runconfig.DecodeContainerConfig(r.Body) if err != nil && err != io.EOF { //Do not fail if body is empty. return err } diff --git a/api/server/router/network/backend.go b/api/server/router/network/backend.go index 9b62e0c33f..60461de2de 100644 --- a/api/server/router/network/backend.go +++ b/api/server/router/network/backend.go @@ -2,7 +2,6 @@ package network import ( "github.com/docker/engine-api/types/network" - "github.com/docker/libnetwork" ) @@ -15,7 +14,7 @@ type Backend interface { GetAllNetworks() []libnetwork.Network CreateNetwork(name, driver string, ipam network.IPAM, options map[string]string) (libnetwork.Network, error) - ConnectContainerToNetwork(containerName, networkName string) error + ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error DisconnectContainerFromNetwork(containerName string, network libnetwork.Network) error NetworkControllerEnabled() bool diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go index 7ba59bd474..a8f6b97887 100644 --- a/api/server/router/network/network_routes.go +++ b/api/server/router/network/network_routes.go @@ -122,7 +122,7 @@ func (n *networkRouter) postNetworkConnect(ctx context.Context, w http.ResponseW return err } - return n.backend.ConnectContainerToNetwork(connect.Container, nw.Name()) + return n.backend.ConnectContainerToNetwork(connect.Container, nw.Name(), connect.EndpointConfig) } func (n *networkRouter) postNetworkDisconnect(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { diff --git a/container/container_unix.go b/container/container_unix.go index 361c77c15b..05ad60c417 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -261,6 +261,14 @@ func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network) ([] createOptions = append(createOptions, libnetwork.CreateOptionAnonymous()) } + if epConfig, ok := container.NetworkSettings.Networks[n.Name()]; ok { + ipam := epConfig.IPAMConfig + if ipam != nil && (ipam.IPv4Address != "" || ipam.IPv6Address != "") { + createOptions = append(createOptions, + libnetwork.CreateOptionIpam(net.ParseIP(ipam.IPv4Address), net.ParseIP(ipam.IPv6Address), nil)) + } + } + // Other configs are applicable only for the endpoint in the network // to which container was connected to on docker run. if n.Name() != container.HostConfig.NetworkMode.NetworkName() && diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index bc8e4294fd..6bf30cfb09 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -503,7 +503,10 @@ func (daemon *Daemon) updateNetworkSettings(container *container.Container, n li return runconfig.ErrConflictNoNetwork } } - container.NetworkSettings.Networks[n.Name()] = new(networktypes.EndpointSettings) + + if _, ok := container.NetworkSettings.Networks[n.Name()]; !ok { + container.NetworkSettings.Networks[n.Name()] = new(networktypes.EndpointSettings) + } return nil } @@ -562,7 +565,12 @@ func (daemon *Daemon) updateNetwork(container *container.Container) error { } // updateContainerNetworkSettings update the network settings -func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container) error { +func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container, endpointsConfig map[string]*networktypes.EndpointSettings) error { + var ( + n libnetwork.Network + err error + ) + mode := container.HostConfig.NetworkMode if container.Config.NetworkDisabled || mode.IsContainer() { return nil @@ -573,14 +581,35 @@ func (daemon *Daemon) updateContainerNetworkSettings(container *container.Contai networkName = daemon.netController.Config().Daemon.DefaultNetwork } if mode.IsUserDefined() { - n, err := daemon.FindNetwork(networkName) + n, err = daemon.FindNetwork(networkName) if err != nil { return err } networkName = n.Name() } - container.NetworkSettings.Networks = make(map[string]*networktypes.EndpointSettings) - container.NetworkSettings.Networks[networkName] = new(networktypes.EndpointSettings) + if container.NetworkSettings == nil { + container.NetworkSettings = &network.Settings{} + } + if endpointsConfig != nil { + container.NetworkSettings.Networks = endpointsConfig + } + if container.NetworkSettings.Networks == nil { + container.NetworkSettings.Networks = make(map[string]*networktypes.EndpointSettings) + container.NetworkSettings.Networks[networkName] = new(networktypes.EndpointSettings) + } + if !mode.IsUserDefined() { + return nil + } + // Make sure to internally store the per network endpoint config by network name + if _, ok := container.NetworkSettings.Networks[networkName]; ok { + return nil + } + if nwConfig, ok := container.NetworkSettings.Networks[n.ID()]; ok { + container.NetworkSettings.Networks[networkName] = nwConfig + delete(container.NetworkSettings.Networks, n.ID()) + return nil + } + return nil } @@ -598,15 +627,15 @@ func (daemon *Daemon) allocateNetwork(container *container.Container) error { return nil } - err := daemon.updateContainerNetworkSettings(container) + err := daemon.updateContainerNetworkSettings(container, nil) if err != nil { return err } updateSettings = true } - for n := range container.NetworkSettings.Networks { - if err := daemon.connectToNetwork(container, n, updateSettings); err != nil { + for n, nConf := range container.NetworkSettings.Networks { + if err := daemon.connectToNetwork(container, n, nConf, updateSettings); err != nil { return err } } @@ -626,12 +655,65 @@ func (daemon *Daemon) getNetworkSandbox(container *container.Container) libnetwo return sb } +// hasUserDefinedIPAddress returns whether the passed endpoint configuration contains IP address configuration +func hasUserDefinedIPAddress(epConfig *networktypes.EndpointSettings) bool { + return epConfig != nil && epConfig.IPAMConfig != nil && (len(epConfig.IPAMConfig.IPv4Address) > 0 || len(epConfig.IPAMConfig.IPv6Address) > 0) +} + +// User specified ip address is acceptable only for networks with user specified subnets. +func validateNetworkingConfig(n libnetwork.Network, epConfig *networktypes.EndpointSettings) error { + if !hasUserDefinedIPAddress(epConfig) { + return nil + } + _, nwIPv4Configs, nwIPv6Configs := n.Info().IpamConfig() + for _, s := range []struct { + ipConfigured bool + subnetConfigs []*libnetwork.IpamConf + }{ + { + ipConfigured: len(epConfig.IPAMConfig.IPv4Address) > 0, + subnetConfigs: nwIPv4Configs, + }, + { + ipConfigured: len(epConfig.IPAMConfig.IPv6Address) > 0, + subnetConfigs: nwIPv6Configs, + }, + } { + if s.ipConfigured { + foundSubnet := false + for _, cfg := range s.subnetConfigs { + if len(cfg.PreferredPool) > 0 { + foundSubnet = true + break + } + } + if !foundSubnet { + return runconfig.ErrUnsupportedNetworkNoSubnetAndIP + } + } + } + + return nil +} + +// cleanOperationalData resets the operational data from the passed endpoint settings +func cleanOperationalData(es *networktypes.EndpointSettings) { + es.EndpointID = "" + es.Gateway = "" + es.IPAddress = "" + es.IPPrefixLen = 0 + es.IPv6Gateway = "" + es.GlobalIPv6Address = "" + es.GlobalIPv6PrefixLen = 0 + es.MacAddress = "" +} + // ConnectToNetwork connects a container to a network -func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string) error { +func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings) error { if !container.Running { return derr.ErrorCodeNotRunning.WithArgs(container.ID) } - if err := daemon.connectToNetwork(container, idOrName, true); err != nil { + if err := daemon.connectToNetwork(container, idOrName, endpointConfig, true); err != nil { return err } if err := container.ToDiskLocking(); err != nil { @@ -640,11 +722,15 @@ func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName return nil } -func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName string, updateSettings bool) (err error) { +func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) { if container.HostConfig.NetworkMode.IsContainer() { return runconfig.ErrConflictSharedNetwork } + if !containertypes.NetworkMode(idOrName).IsUserDefined() && hasUserDefinedIPAddress(endpointConfig) { + return runconfig.ErrUnsupportedNetworkAndIP + } + if containertypes.NetworkMode(idOrName).IsBridge() && daemon.configStore.DisableBridge { container.Config.NetworkDisabled = true @@ -658,12 +744,20 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName return err } + if err := validateNetworkingConfig(n, endpointConfig); err != nil { + return err + } + if updateSettings { if err := daemon.updateNetworkSettings(container, n); err != nil { return err } } + if endpointConfig != nil { + container.NetworkSettings.Networks[n.Name()] = endpointConfig + } + ep, err := container.GetEndpointInNetwork(n) if err == nil { return fmt.Errorf("Conflict. A container with name %q is already connected to network %s.", strings.TrimPrefix(container.Name, "/"), idOrName) @@ -869,18 +963,16 @@ func (daemon *Daemon) releaseNetwork(container *container.Container) { sid := container.NetworkSettings.SandboxID settings := container.NetworkSettings.Networks + if sid == "" || len(settings) == 0 { + return + } + var networks []libnetwork.Network - for n := range settings { + for n, epSettings := range settings { if nw, err := daemon.FindNetwork(n); err == nil { networks = append(networks, nw) } - settings[n] = &networktypes.EndpointSettings{} - } - - container.NetworkSettings = &network.Settings{Networks: settings} - - if sid == "" || len(settings) == 0 { - return + cleanOperationalData(epSettings) } sb, err := daemon.netController.SandboxByID(sid) diff --git a/daemon/container_operations_windows.go b/daemon/container_operations_windows.go index 2c4a24c463..d713541b19 100644 --- a/daemon/container_operations_windows.go +++ b/daemon/container_operations_windows.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/daemon/execdriver/windows" derr "github.com/docker/docker/errors" "github.com/docker/docker/layer" + networktypes "github.com/docker/engine-api/types/network" "github.com/docker/libnetwork" ) @@ -18,7 +19,7 @@ func (daemon *Daemon) setupLinkedContainers(container *container.Container) ([]s } // updateContainerNetworkSettings update the network settings -func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container) error { +func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container, endpointsConfig map[string]*networktypes.EndpointSettings) error { return nil } @@ -27,7 +28,7 @@ func (daemon *Daemon) initializeNetworking(container *container.Container) error } // ConnectToNetwork connects a container to the network -func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string) error { +func (daemon *Daemon) ConnectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings) error { return nil } diff --git a/daemon/create.go b/daemon/create.go index 12e9449320..148f2b7a88 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -11,6 +11,7 @@ import ( volumestore "github.com/docker/docker/volume/store" "github.com/docker/engine-api/types" containertypes "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" "github.com/opencontainers/runc/libcontainer/label" ) @@ -108,7 +109,12 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig) (retC *containe return nil, err } - if err := daemon.updateContainerNetworkSettings(container); err != nil { + var endpointsConfigs map[string]*networktypes.EndpointSettings + if params.NetworkingConfig != nil { + endpointsConfigs = params.NetworkingConfig.EndpointsConfig + } + + if err := daemon.updateContainerNetworkSettings(container, endpointsConfigs); err != nil { return nil, err } diff --git a/daemon/network.go b/daemon/network.go index 4dba9a80e1..3f7b13f7c1 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -150,12 +150,12 @@ func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnet // ConnectContainerToNetwork connects the given container to the given // network. If either cannot be found, an err is returned. If the // network cannot be set up, an err is returned. -func (daemon *Daemon) ConnectContainerToNetwork(containerName, networkName string) error { +func (daemon *Daemon) ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error { container, err := daemon.GetContainer(containerName) if err != nil { return err } - return daemon.ConnectToNetwork(container, networkName) + return daemon.ConnectToNetwork(container, networkName, endpointConfig) } // DisconnectContainerFromNetwork disconnects the given container from diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index c18edbb167..028c41e362 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -110,6 +110,8 @@ This section lists each version from latest to oldest. Each listing includes a * `POST /containers/create` now allows you to set a read/write rate limit for a device (in bytes per second or IO per second). * `GET /networks` now supports filtering by `name`, `id` and `type`. +* `POST /containers/create` now allows you to set the static IPv4 and/or IPv6 address for the container. +* `POST /networks/(id)/connect` now allows you to set the static IPv4 and/or IPv6 address for the container. ### v1.21 API changes diff --git a/docs/reference/api/docker_remote_api_v1.22.md b/docs/reference/api/docker_remote_api_v1.22.md index 88c3d2cf5f..9483dd4eb4 100644 --- a/docs/reference/api/docker_remote_api_v1.22.md +++ b/docs/reference/api/docker_remote_api_v1.22.md @@ -3031,7 +3031,13 @@ POST /networks/22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30/ Content-Type: application/json { - "Container":"3613f73ba0e4" + "Container":"3613f73ba0e4", + "endpoint_config": { + "test_nw": { + "IPv4Address":"172.24.56.89", + "IPv6Address":"2001:db8::5689" + } + } } ``` diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md index ab78bf9d5c..dbecda6ddb 100644 --- a/docs/reference/commandline/network_connect.md +++ b/docs/reference/commandline/network_connect.md @@ -30,11 +30,18 @@ You can also use the `docker run --net=` option to start a contain $ docker run -itd --net=multi-host-network busybox ``` +You can specify the IP address you want to be assigned to the container's interface. + +```bash +$ docker network connect multi-host-network --ip 10.10.36.122 container2 +``` + You can pause, restart, and stop containers that are connected to a network. Paused containers remain connected and a revealed by a `network inspect`. When the container is stopped, it does not appear on the network until you restart it. The container's IP address is not guaranteed to remain the same when a -stopped container rejoins the network. +stopped container rejoins the network, unless you specified one when you run +`docker network connect` command. To verify the container is connected, use the `docker network inspect` command. Use `docker network disconnect` to remove a container from the network. diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index 3ea6ae5415..78643e56af 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -56,6 +56,8 @@ parent = "smn_cli" --log-opt=[] Log driver specific options -m, --memory="" Memory limit --mac-address="" Container MAC address (e.g. 92:d0:c6:0a:29:33) + --ip="" Container IPv4 address (e.g. 172.30.100.104) + --ip6="" Container IPv6 address (e.g. 2001:db8::33) --memory-reservation="" Memory soft limit --memory-swap="" A positive integer equal to memory plus swap. Specify -1 to enable unlimited swap. --memory-swappiness="" Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. diff --git a/docs/reference/run.md b/docs/reference/run.md index 9288dd21d5..e802f3e177 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -275,6 +275,8 @@ of the containers. '|': connect to a user-defined network --add-host="" : Add a line to /etc/hosts (host:IP) --mac-address="" : Sets the container's Ethernet device's MAC address + --ip="" : Sets the container's Ethernet device's IPv4 address + --ip6="" : Sets the container's Ethernet device's IPv6 address By default, all containers have networking enabled and they can make any outgoing connections. The operator can completely disable networking diff --git a/docs/userguide/networking/work-with-networks.md b/docs/userguide/networking/work-with-networks.md index 9572912589..5762559a19 100644 --- a/docs/userguide/networking/work-with-networks.md +++ b/docs/userguide/networking/work-with-networks.md @@ -115,8 +115,8 @@ $ docker run -itd --name=container2 busybox Then create an isolated, `bridge` network to test with. ```bash -$ docker network create -d bridge isolated_nw -f836c8deb6282ee614eade9d2f42d590e603d0b1efa0d99bd88b88c503e6ba7a +$ docker network create -d bridge --subnet 172.25.0.0/16 isolated_nw +06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8 ``` Connect `container2` to the network and then `inspect` the network to verify the connection: @@ -124,23 +124,26 @@ Connect `container2` to the network and then `inspect` the network to verify the ``` $ docker network connect isolated_nw container2 $ docker network inspect isolated_nw -[[ +[ { "Name": "isolated_nw", - "Id": "f836c8deb6282ee614eade9d2f42d590e603d0b1efa0d99bd88b88c503e6ba7a", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", "Scope": "local", "Driver": "bridge", "IPAM": { "Driver": "default", "Config": [ - {} + { + "Subnet": "172.25.0.0/16" + } ] }, "Containers": { - "498eaaaf328e1018042c04b2de04036fc04719a6e39a097a4f4866043a2c2152": { - "EndpointID": "0e24479cfaafb029104999b4e120858a07b19b1b6d956ae56811033e45d68ad9", - "MacAddress": "02:42:ac:15:00:02", - "IPv4Address": "172.21.0.2/16", + "90e1f3ec71caf82ae776a827e0712a68a110a3f175954e5bd4222fd142ac9428": { + "Name": "container2", + "EndpointID": "11cedac1810e864d6b1589d92da12af66203879ab89f4ccd8c8fdaa9b1c48b1d", + "MacAddress": "02:42:ac:19:00:02", + "IPv4Address": "172.25.0.2/16", "IPv6Address": "" } }, @@ -150,20 +153,28 @@ $ docker network inspect isolated_nw ``` You can see that the Engine automatically assigns an IP address to `container2`. -If you had specified a `--subnetwork` when creating your network, the network -would have used that addressing. Now, start a third container and connect it to +Given we specified a `--subnet` when creating the network, Engine picked +an address from that same subnet. Now, start a third container and connect it to the network on launch using the `docker run` command's `--net` option: ```bash -$ docker run --net=isolated_nw -itd --name=container3 busybox -c282ca437ee7e926a7303a64fc04109740208d2c20e442366139322211a6481c +$ docker run --net=isolated_nw --ip=172.25.3.3 -itd --name=container3 busybox +467a7863c3f0277ef8e661b38427737f28099b61fa55622d6c30fb288d88c551 ``` +As you can see you were able to specify the ip address for your container. +As long as the network to which the container is connecting was created with +a user specified subnet, you will be able to select the IPv4 and/or IPv6 address(es) +for your container when executing `docker run` and `docker network connect` commands. +The selected IP address is part of the container networking configuration and will be +preserved across container reload. The feature is only available on user defined networks, +because they guarantee their subnets configuration does not change across daemon reload. + Now, inspect the network resources used by `container3`. ```bash $ docker inspect --format='{{json .NetworkSettings.Networks}}' container3 -{"isolated_nw":{"EndpointID":"e5d077f9712a69c6929fdd890df5e7c1c649771a50df5b422f7e68f0ae61e847","Gateway":"172.21.0.1","IPAddress":"172.21.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:03"}} +{"isolated_nw":{"IPAMConfig":{"IPv4Address":"172.25.3.3"},"EndpointID":"dffc7ec2915af58cc827d995e6ebdc897342be0420123277103c40ae35579103","Gateway":"172.25.0.1","IPAddress":"172.25.3.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:19:03:03"}} ``` Repeat this command for `container2`. If you have Python installed, you can pretty print the output. @@ -171,24 +182,26 @@ Repeat this command for `container2`. If you have Python installed, you can pret $ docker inspect --format='{{json .NetworkSettings.Networks}}' container2 | python -m json.tool { "bridge": { - "EndpointID": "281b5ead415cf48a6a84fd1a6504342c76e9091fe09b4fdbcc4a01c30b0d3c5b", + "EndpointID": "0099f9efb5a3727f6a554f176b1e96fca34cae773da68b3b6a26d046c12cb365", "Gateway": "172.17.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, + "IPAMConfig": null, "IPAddress": "172.17.0.3", "IPPrefixLen": 16, "IPv6Gateway": "", "MacAddress": "02:42:ac:11:00:03" }, "isolated_nw": { - "EndpointID": "0e24479cfaafb029104999b4e120858a07b19b1b6d956ae56811033e45d68ad9", - "Gateway": "172.21.0.1", + "EndpointID": "11cedac1810e864d6b1589d92da12af66203879ab89f4ccd8c8fdaa9b1c48b1d", + "Gateway": "172.25.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, - "IPAddress": "172.21.0.2", + "IPAMConfig": null, + "IPAddress": "172.25.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", - "MacAddress": "02:42:ac:15:00:02" + "MacAddress": "02:42:ac:19:00:02" } } ``` @@ -223,8 +236,8 @@ eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03 RX bytes:648 (648.0 B) TX bytes:648 (648.0 B) eth1 Link encap:Ethernet HWaddr 02:42:AC:15:00:02 - inet addr:172.21.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 - inet6 addr: fe80::42:acff:fe15:2/64 Scope:Link + inet addr:172.25.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: fe80::42:acff:fe19:2/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:8 errors:0 dropped:0 overruns:0 frame:0 TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 @@ -252,19 +265,19 @@ fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters -172.21.0.3 container3 -172.21.0.3 container3.isolated_nw +172.21.3.3 container3 +172.21.3.3 container3.isolated_nw ``` On the `isolated_nw` which was user defined, the Docker network feature updated the `/etc/hosts` with the proper name resolution. Inside of `container2` it is possible to ping `container3` by name. ```bash / # ping -w 4 container3 -PING container3 (172.21.0.3): 56 data bytes -64 bytes from 172.21.0.3: seq=0 ttl=64 time=0.070 ms -64 bytes from 172.21.0.3: seq=1 ttl=64 time=0.080 ms -64 bytes from 172.21.0.3: seq=2 ttl=64 time=0.080 ms -64 bytes from 172.21.0.3: seq=3 ttl=64 time=0.097 ms +PING container3 (172.25.3.3): 56 data bytes +64 bytes from 172.25.3.3: seq=0 ttl=64 time=0.070 ms +64 bytes from 172.25.3.3: seq=1 ttl=64 time=0.080 ms +64 bytes from 172.25.3.3: seq=2 ttl=64 time=0.080 ms +64 bytes from 172.25.3.3: seq=3 ttl=64 time=0.097 ms --- container3 ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss @@ -342,23 +355,26 @@ docker inspect --format='{{json .NetworkSettings.Networks}}' container2 | pytho $ docker network inspect isolated_nw -[[ +[ { "Name": "isolated_nw", - "Id": "f836c8deb6282ee614eade9d2f42d590e603d0b1efa0d99bd88b88c503e6ba7a", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", "Scope": "local", "Driver": "bridge", "IPAM": { "Driver": "default", "Config": [ - {} + { + "Subnet": "172.25.0.0/16" + } ] }, "Containers": { - "c282ca437ee7e926a7303a64fc04109740208d2c20e442366139322211a6481c": { - "EndpointID": "e5d077f9712a69c6929fdd890df5e7c1c649771a50df5b422f7e68f0ae61e847", - "MacAddress": "02:42:ac:15:00:03", - "IPv4Address": "172.21.0.3/16", + "467a7863c3f0277ef8e661b38427737f28099b61fa55622d6c30fb288d88c551": { + "Name": "container3", + "EndpointID": "dffc7ec2915af58cc827d995e6ebdc897342be0420123277103c40ae35579103", + "MacAddress": "02:42:ac:19:03:03", + "IPv4Address": "172.25.3.3/16", "IPv6Address": "" } }, @@ -393,7 +409,7 @@ lo Link encap:Local Loopback RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) / # ping container3 -PING container3 (172.20.0.1): 56 data bytes +PING container3 (172.25.3.3): 56 data bytes ^C --- container3 ping statistics --- 2 packets transmitted, 0 packets received, 100% packet loss @@ -426,13 +442,15 @@ docker network inspect isolated_nw [ { "Name": "isolated_nw", - "Id": "f836c8deb6282ee614eade9d2f42d590e603d0b1efa0d99bd88b88c503e6ba7a", + "Id": "06a62f1c73c4e3107c0f555b7a5f163309827bfbbf999840166065a8f35455a8", "Scope": "local", "Driver": "bridge", "IPAM": { "Driver": "default", "Config": [ - {} + { + "Subnet": "172.25.0.0/16" + } ] }, "Containers": {}, diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go index 9615642214..967d44cc22 100644 --- a/integration-cli/docker_cli_network_unix_test.go +++ b/integration-cli/docker_cli_network_unix_test.go @@ -962,3 +962,71 @@ func (s *DockerNetworkSuite) TestDockerNetworkRestartWithMulipleNetworks(c *chec c.Assert(networks, checker.Contains, "bridge", check.Commentf("Should contain 'bridge' network")) c.Assert(networks, checker.Contains, "test", check.Commentf("Should contain 'test' netwokr")) } + +func (s *DockerNetworkSuite) TestDockerNetworkConnectPreferredIP(c *check.C) { + // create two networks + dockerCmd(c, "network", "create", "--subnet=172.28.0.0/16", "--subnet=2001:db8:1234::/64", "n0") + assertNwIsAvailable(c, "n0") + + dockerCmd(c, "network", "create", "--subnet=172.30.0.0/16", "--ip-range=172.30.5.0/24", "--subnet=2001:db8:abcd::/64", "--ip-range=2001:db8:abcd::/80", "n1") + assertNwIsAvailable(c, "n1") + + // run a container on first network specifying the ip addresses + dockerCmd(c, "run", "-d", "--name", "c0", "--net=n0", "--ip", "172.28.99.88", "--ip6", "2001:db8:1234::9988", "busybox", "top") + c.Assert(waitRun("c0"), check.IsNil) + verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + + // connect the container to the second network specifying the preferred ip addresses + dockerCmd(c, "network", "connect", "--ip", "172.30.55.44", "--ip6", "2001:db8:abcd::5544", "n1", "c0") + verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + + // Stop and restart the container + dockerCmd(c, "stop", "c0") + dockerCmd(c, "start", "c0") + + // verify preferred addresses are applied + verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988") + verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544") + + // Still it should fail to connect to the default network with a specified IP (whatever ip) + out, _, err := dockerCmdWithError("network", "connect", "--ip", "172.21.55.44", "bridge", "c0") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndIP.Error()) + +} + +func (s *DockerNetworkSuite) TestDockerNetworkUnsupportedPreferredIP(c *check.C) { + // preferred IP is not supported on predefined networks + for _, mode := range []string{"none", "host", "bridge"} { + checkUnsupportedNetworkAndIP(c, mode) + } + + // preferred IP is not supported on networks with no user defined subnets + dockerCmd(c, "network", "create", "n0") + assertNwIsAvailable(c, "n0") + + out, _, err := dockerCmdWithError("run", "-d", "--ip", "172.28.99.88", "--net", "n0", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkNoSubnetAndIP.Error()) + + out, _, err = dockerCmdWithError("run", "-d", "--ip6", "2001:db8:1234::9988", "--net", "n0", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkNoSubnetAndIP.Error()) + + dockerCmd(c, "network", "rm", "n0") + assertNwNotAvailable(c, "n0") +} + +func checkUnsupportedNetworkAndIP(c *check.C, nwMode string) { + out, _, err := dockerCmdWithError("run", "-d", "--net", nwMode, "--ip", "172.28.99.88", "--ip6", "2001:db8:1234::9988", "busybox", "top") + c.Assert(err, checker.NotNil, check.Commentf("out: %s", out)) + c.Assert(out, checker.Contains, runconfig.ErrUnsupportedNetworkAndIP.Error()) +} + +func verifyIPAddresses(c *check.C, cName, nwname, ipv4, ipv6 string) { + out, _ := dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.IPAddress }}'", nwname), cName) + c.Assert(strings.TrimSpace(out), check.Equals, ipv4) + + out, _ = dockerCmd(c, "inspect", fmt.Sprintf("--format='{{ .NetworkSettings.Networks.%s.GlobalIPv6Address }}'", nwname), cName) + c.Assert(strings.TrimSpace(out), check.Equals, ipv6) +} diff --git a/runconfig/config.go b/runconfig/config.go index f40da2275a..f62b471bce 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -7,18 +7,19 @@ import ( "github.com/docker/docker/volume" "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" ) // DecodeContainerConfig decodes a json encoded config into a ContainerConfigWrapper // struct and returns both a Config and an HostConfig struct // Be aware this function is not checking whether the resulted structs are nil, // it's your business to do so -func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostConfig, error) { +func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { var w ContainerConfigWrapper decoder := json.NewDecoder(src) if err := decoder.Decode(&w); err != nil { - return nil, nil, err + return nil, nil, nil, err } hc := w.getHostConfig() @@ -33,21 +34,21 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon // Now validate all the volumes and binds if err := validateVolumesAndBindSettings(w.Config, hc); err != nil { - return nil, nil, err + return nil, nil, nil, err } } // Certain parameters need daemon-side validation that cannot be done // on the client, as only the daemon knows what is valid for the platform. if err := ValidateNetMode(w.Config, hc); err != nil { - return nil, nil, err + return nil, nil, nil, err } // Validate the isolation level if err := ValidateIsolationLevel(hc); err != nil { - return nil, nil, err + return nil, nil, nil, err } - return w.Config, hc, nil + return w.Config, hc, w.NetworkingConfig, nil } // validateVolumesAndBindSettings validates each of the volumes and bind settings diff --git a/runconfig/config_test.go b/runconfig/config_test.go index 8c8cb15b00..b36d027bea 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" "github.com/docker/engine-api/types/strslice" ) @@ -45,7 +46,7 @@ func TestDecodeContainerConfig(t *testing.T) { t.Fatal(err) } - c, h, err := DecodeContainerConfig(bytes.NewReader(b)) + c, h, _, err := DecodeContainerConfig(bytes.NewReader(b)) if err != nil { t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err)) } @@ -70,29 +71,29 @@ func TestDecodeContainerConfig(t *testing.T) { func TestDecodeContainerConfigIsolation(t *testing.T) { // An invalid isolation level - if _, _, err := callDecodeContainerConfigIsolation("invalid"); err != nil { + if _, _, _, err := callDecodeContainerConfigIsolation("invalid"); err != nil { if !strings.Contains(err.Error(), `invalid --isolation: "invalid"`) { t.Fatal(err) } } // Blank isolation level (== default) - if _, _, err := callDecodeContainerConfigIsolation(""); err != nil { + if _, _, _, err := callDecodeContainerConfigIsolation(""); err != nil { t.Fatal("Blank isolation should have succeeded") } // Default isolation level - if _, _, err := callDecodeContainerConfigIsolation("default"); err != nil { + if _, _, _, err := callDecodeContainerConfigIsolation("default"); err != nil { t.Fatal("default isolation should have succeeded") } // Hyper-V Containers isolation level (Valid on Windows only) if runtime.GOOS == "windows" { - if _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { t.Fatal("hyperv isolation should have succeeded") } } else { - if _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { if !strings.Contains(err.Error(), `invalid --isolation: "hyperv"`) { t.Fatal(err) } @@ -102,7 +103,7 @@ func TestDecodeContainerConfigIsolation(t *testing.T) { // callDecodeContainerConfigIsolation is a utility function to call // DecodeContainerConfig for validating isolation levels -func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *container.HostConfig, error) { +func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { var ( b []byte err error @@ -114,7 +115,7 @@ func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *c Isolation: container.IsolationLevel(isolation)}, } if b, err = json.Marshal(w); err != nil { - return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + return nil, nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) } return DecodeContainerConfig(bytes.NewReader(b)) } diff --git a/runconfig/config_unix.go b/runconfig/config_unix.go index 7bff0a2783..16a8d94d4a 100644 --- a/runconfig/config_unix.go +++ b/runconfig/config_unix.go @@ -2,15 +2,19 @@ package runconfig -import "github.com/docker/engine-api/types/container" +import ( + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) // ContainerConfigWrapper is a Config wrapper that hold the container Config (portable) // and the corresponding HostConfig (non-portable). type ContainerConfigWrapper struct { *container.Config - InnerHostConfig *container.HostConfig `json:"HostConfig,omitempty"` - Cpuset string `json:",omitempty"` // Deprecated. Exported for backwards compatibility. - *container.HostConfig // Deprecated. Exported to read attributes from json that are not in the inner host config structure. + InnerHostConfig *container.HostConfig `json:"HostConfig,omitempty"` + Cpuset string `json:",omitempty"` // Deprecated. Exported for backwards compatibility. + NetworkingConfig *networktypes.NetworkingConfig `json:"NetworkingConfig,omitempty"` + *container.HostConfig // Deprecated. Exported to read attributes from json that are not in the inner host config structure. } // getHostConfig gets the HostConfig of the Config. diff --git a/runconfig/config_windows.go b/runconfig/config_windows.go index c8d387becd..08d9b023ec 100644 --- a/runconfig/config_windows.go +++ b/runconfig/config_windows.go @@ -1,12 +1,16 @@ package runconfig -import "github.com/docker/engine-api/types/container" +import ( + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" +) // ContainerConfigWrapper is a Config wrapper that hold the container Config (portable) // and the corresponding HostConfig (non-portable). type ContainerConfigWrapper struct { *container.Config - HostConfig *container.HostConfig `json:"HostConfig,omitempty"` + HostConfig *container.HostConfig `json:"HostConfig,omitempty"` + NetworkingConfig *networktypes.NetworkingConfig `json:"NetworkingConfig,omitempty"` } // getHostConfig gets the HostConfig of the Config. diff --git a/runconfig/errors.go b/runconfig/errors.go index 7dbdb9e1ce..34b8aecea5 100644 --- a/runconfig/errors.go +++ b/runconfig/errors.go @@ -29,4 +29,8 @@ var ( ErrConflictNetworkPublishPorts = fmt.Errorf("Conflicting options: port publishing and the container type network mode") // ErrConflictNetworkExposePorts conflict between the expose option and the network mode ErrConflictNetworkExposePorts = fmt.Errorf("Conflicting options: port exposing and the container type network mode") + // ErrUnsupportedNetworkAndIP conflict between network mode and preferred ip address + ErrUnsupportedNetworkAndIP = fmt.Errorf("User specified IP address is supported on user defined networks only") + // ErrUnsupportedNetworkNoSubnetAndIP conflict between network with no configured subnet and preferred ip address + ErrUnsupportedNetworkNoSubnetAndIP = fmt.Errorf("User specified IP address is supported only when connecting to networks with user configured subnets") ) diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index 1f8038bb7c..b27b9b5721 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/signal" "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" "github.com/docker/engine-api/types/strslice" "github.com/docker/go-connections/nat" "github.com/docker/go-units" @@ -19,7 +20,7 @@ import ( // Parse parses the specified args for the specified command and generates a Config, // a HostConfig and returns them with the specified command. // If the specified args are not valid, it will return an error. -func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.HostConfig, *flag.FlagSet, error) { +func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { var ( // FIXME: use utils.ListOpts for attach and volumes? flAttach = opts.NewListOpts(ValidateAttach) @@ -77,6 +78,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host flSwappiness = cmd.Int64([]string{"-memory-swappiness"}, -1, "Tune container memory swappiness (0 to 100)") flNetMode = cmd.String([]string{"-net"}, "default", "Connect a container to a network") flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") + flIPv4Address = cmd.String([]string{"-ip"}, "", "Container IPv4 address (e.g. 172.30.100.104)") + flIPv6Address = cmd.String([]string{"-ip6"}, "", "Container IPv6 address (e.g. 2001:db8::33)") flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use") flRestartPolicy = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits") flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") @@ -119,7 +122,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host cmd.Require(flag.Min, 1) if err := cmd.ParseFlags(args, true); err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } var ( @@ -131,7 +134,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host // Validate the input mac address if *flMacAddress != "" { if _, err := ValidateMACAddress(*flMacAddress); err != nil { - return nil, nil, cmd, fmt.Errorf("%s is not a valid mac address", *flMacAddress) + return nil, nil, nil, cmd, fmt.Errorf("%s is not a valid mac address", *flMacAddress) } } if *flStdin { @@ -149,7 +152,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host if *flMemoryString != "" { flMemory, err = units.RAMInBytes(*flMemoryString) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } } @@ -157,7 +160,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host if *flMemoryReservation != "" { MemoryReservation, err = units.RAMInBytes(*flMemoryReservation) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } } @@ -168,7 +171,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host } else { memorySwap, err = units.RAMInBytes(*flMemorySwap) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } } } @@ -177,20 +180,20 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host if *flKernelMemory != "" { KernelMemory, err = units.RAMInBytes(*flKernelMemory) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } } swappiness := *flSwappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, nil, cmd, fmt.Errorf("Invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + return nil, nil, nil, cmd, fmt.Errorf("Invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } var shmSize int64 if *flShmSize != "" { shmSize, err = units.RAMInBytes(*flShmSize) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } } @@ -210,7 +213,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host for _, t := range flTmpfs.GetAll() { if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { if _, _, err := mount.ParseTmpfsOptions(arr[1]); err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } tmpfs[arr[0]] = arr[1] } else { @@ -243,13 +246,13 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host ports, portBindings, err := nat.ParsePortSpecs(flPublish.GetAll()) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } // Merge in exposed ports to the map of published ports for _, e := range flExpose.GetAll() { if strings.Contains(e, ":") { - return nil, nil, cmd, fmt.Errorf("Invalid port format for --expose: %s", e) + return nil, nil, nil, cmd, fmt.Errorf("Invalid port format for --expose: %s", e) } //support two formats for expose, original format /[] or /[] proto, port := nat.SplitProtoPort(e) @@ -257,12 +260,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host //if expose a port, the start and end port are the same start, end, err := nat.ParsePortRange(port) if err != nil { - return nil, nil, cmd, fmt.Errorf("Invalid range format for --expose: %s, error: %s", e, err) + return nil, nil, nil, cmd, fmt.Errorf("Invalid range format for --expose: %s, error: %s", e, err) } for i := start; i <= end; i++ { p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } if _, exists := ports[p]; !exists { ports[p] = struct{}{} @@ -275,7 +278,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host for _, device := range flDevices.GetAll() { deviceMapping, err := ParseDevice(device) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } deviceMappings = append(deviceMappings, deviceMapping) } @@ -283,38 +286,38 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host // collect all the environment variables for the container envVariables, err := readKVStrings(flEnvFile.GetAll(), flEnv.GetAll()) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } // collect all the labels for the container labels, err := readKVStrings(flLabelsFile.GetAll(), flLabels.GetAll()) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } ipcMode := container.IpcMode(*flIpcMode) if !ipcMode.Valid() { - return nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode") + return nil, nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode") } pidMode := container.PidMode(*flPidMode) if !pidMode.Valid() { - return nil, nil, cmd, fmt.Errorf("--pid: invalid PID mode") + return nil, nil, nil, cmd, fmt.Errorf("--pid: invalid PID mode") } utsMode := container.UTSMode(*flUTSMode) if !utsMode.Valid() { - return nil, nil, cmd, fmt.Errorf("--uts: invalid UTS mode") + return nil, nil, nil, cmd, fmt.Errorf("--uts: invalid UTS mode") } restartPolicy, err := ParseRestartPolicy(*flRestartPolicy) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } loggingOpts, err := parseLoggingOpts(*flLoggingDriver, flLoggingOpts.GetAll()) if err != nil { - return nil, nil, cmd, err + return nil, nil, nil, cmd, err } resources := container.Resources{ @@ -405,7 +408,21 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host if config.OpenStdin && config.AttachStdin { config.StdinOnce = true } - return config, hostConfig, cmd, nil + + var networkingConfig *networktypes.NetworkingConfig + if *flIPv4Address != "" || *flIPv6Address != "" { + networkingConfig = &networktypes.NetworkingConfig{ + EndpointsConfig: make(map[string]*networktypes.EndpointSettings), + } + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = &networktypes.EndpointSettings{ + IPAMConfig: &networktypes.EndpointIPAMConfig{ + IPv4Address: *flIPv4Address, + IPv6Address: *flIPv6Address, + }, + } + } + + return config, hostConfig, networkingConfig, cmd, nil } // reads a file of line terminated key=value pairs and override that with override parameter diff --git a/runconfig/opts/parse_test.go b/runconfig/opts/parse_test.go index 58a853ac82..3ca9e32b32 100644 --- a/runconfig/opts/parse_test.go +++ b/runconfig/opts/parse_test.go @@ -13,10 +13,11 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/runconfig" "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" "github.com/docker/go-connections/nat" ) -func parseRun(args []string) (*container.Config, *container.HostConfig, *flag.FlagSet, error) { +func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { cmd := flag.NewFlagSet("run", flag.ContinueOnError) cmd.SetOutput(ioutil.Discard) cmd.Usage = nil @@ -24,7 +25,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *flag.Fl } func parse(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { - config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) + config, hostConfig, _, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) return config, hostConfig, err } @@ -304,7 +305,7 @@ func callDecodeContainerConfig(volumes []string, binds []string) (*container.Con if b, err = json.Marshal(w); err != nil { return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) } - c, h, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) + c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) if err != nil { return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) } @@ -349,7 +350,7 @@ func setupPlatformVolume(u []string, w []string) ([]string, string) { func TestParseWithMacAddress(t *testing.T) { invalidMacAddress := "--mac-address=invalidMacAddress" validMacAddress := "--mac-address=92:d0:c6:0a:29:33" - if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { + if _, _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) } if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { @@ -360,7 +361,7 @@ func TestParseWithMacAddress(t *testing.T) { func TestParseWithMemory(t *testing.T) { invalidMemory := "--memory=invalid" validMemory := "--memory=1G" - if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { + if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) } if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { @@ -372,7 +373,7 @@ func TestParseWithMemorySwap(t *testing.T) { invalidMemory := "--memory-swap=invalid" validMemory := "--memory-swap=1G" anotherValidMemory := "--memory-swap=-1" - if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { + if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) } if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { @@ -417,12 +418,12 @@ func TestParseWithExpose(t *testing.T) { "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, } for expose, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { + if _, _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) } } for expose, exposedPorts := range valids { - config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) + config, _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -436,7 +437,7 @@ func TestParseWithExpose(t *testing.T) { } } // Merge with actual published port - config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) + config, _, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -475,7 +476,7 @@ func TestParseDevice(t *testing.T) { }, } for device, deviceMapping := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) + _, hostconfig, _, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -491,11 +492,11 @@ func TestParseDevice(t *testing.T) { func TestParseModes(t *testing.T) { // ipc ko - if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { + if _, _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) } // ipc ok - _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) + _, hostconfig, _, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -503,11 +504,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) } // pid ko - if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { + if _, _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) } // pid ok - _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) + _, hostconfig, _, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -515,11 +516,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) } // uts ko - if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { + if _, _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) } // uts ok - _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) + _, hostconfig, _, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -527,11 +528,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) } // shm-size ko - if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { + if _, _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err) } // shm-size ok - _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) + _, hostconfig, _, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -560,12 +561,12 @@ func TestParseRestartPolicy(t *testing.T) { }, } for restart, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { + if _, _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) } } for restart, expected := range valids { - _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) + _, hostconfig, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -577,11 +578,11 @@ func TestParseRestartPolicy(t *testing.T) { func TestParseLoggingOpts(t *testing.T) { // logging opts ko - if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "Invalid logging opts for driver none" { + if _, _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "Invalid logging opts for driver none" { t.Fatalf("Expected an error with message 'Invalid logging opts for driver none', got %v", err) } // logging opts ok - _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) + _, hostconfig, _, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -596,18 +597,18 @@ func TestParseEnvfileVariables(t *testing.T) { e = "open nonexistent: The system cannot find the file specified." } // env ko - if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + if _, _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { t.Fatalf("Expected an error with message '%s', got %v", e, err) } // env ok - config, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"}) + config, _, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"}) if err != nil { t.Fatal(err) } if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { t.Fatalf("Expected a a config with [ENV1=value1], got %v", config.Env) } - config, _, _, err = parseRun([]string{"--env-file=fixtures/valid.env", "--env=ENV2=value2", "img", "cmd"}) + config, _, _, _, err = parseRun([]string{"--env-file=fixtures/valid.env", "--env=ENV2=value2", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -622,18 +623,18 @@ func TestParseLabelfileVariables(t *testing.T) { e = "open nonexistent: The system cannot find the file specified." } // label ko - if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + if _, _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { t.Fatalf("Expected an error with message '%s', got %v", e, err) } // label ok - config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) + config, _, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) if err != nil { t.Fatal(err) } if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { t.Fatalf("Expected a a config with [LABEL1:value1], got %v", config.Labels) } - config, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"}) + config, _, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -643,7 +644,7 @@ func TestParseLabelfileVariables(t *testing.T) { } func TestParseEntryPoint(t *testing.T) { - config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) + config, _, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) if err != nil { t.Fatal(err) }