Skip to content

feat: Add gateway_priority support for network endpoints #3339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions docker/api/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,29 @@ def create_container(self, image, command=None, hostname=None, user=None,
stop_signal, networking_config, healthcheck,
stop_timeout, runtime
)
# The gw_priority is not directly part of ContainerConfig,
# it's part of NetworkingConfig's EndpointsConfig.
# We need to ensure networking_config passed to create_container_from_config
# can have gw_priority.
# create_container_config doesn't handle networking_config directly in its
# parameters but it's passed through to ContainerConfig which stores it.
# The actual handling of gw_priority is within create_endpoint_config.
# We need to make sure that when create_container is called, if gw_priority
# is intended for an endpoint, it's correctly passed into the
# relevant create_endpoint_config call within create_networking_config.

# The current structure expects networking_config to be pre-constructed.
# If we want to add a simple top-level gw_priority to create_container,
# it would imply it's for the *primary* network interface if only one
# is being configured, or require more complex logic if multiple networks
# are part of networking_config.

# For now, users should construct NetworkingConfig with GwPriority using
# create_networking_config and create_endpoint_config as shown in examples.
# We will modify create_endpoint_config to correctly handle gw_priority.
# No direct change to create_container signature for a top-level gw_priority.
# The user is responsible for building the networking_config correctly.

return self.create_container_from_config(config, name, platform)

def create_container_config(self, *args, **kwargs):
Expand Down Expand Up @@ -652,9 +675,10 @@ def create_endpoint_config(self, *args, **kwargs):
Names in that list can be used within the network to reach the
container. Defaults to ``None``.
links (dict): Mapping of links for this endpoint using the
``{'container': 'alias'}`` format. The alias is optional.
``{\'container\': \'alias\'}`` format. The alias is optional.
Containers declared in this dict will be linked to this
container using the provided alias. Defaults to ``None``.

ipv4_address (str): The IP address of this container on the
network, using the IPv4 protocol. Defaults to ``None``.
ipv6_address (str): The IP address of this container on the
Expand All @@ -663,19 +687,24 @@ def create_endpoint_config(self, *args, **kwargs):
addresses.
driver_opt (dict): A dictionary of options to provide to the
network driver. Defaults to ``None``.
gw_priority (int): The priority of the gateway for this endpoint.
Requires API version 1.48 or higher. Defaults to ``None``.

Returns:
(dict) An endpoint config.

Example:

>>> endpoint_config = client.api.create_endpoint_config(
aliases=['web', 'app'],
links={'app_db': 'db', 'another': None},
ipv4_address='132.65.0.123'
)
... aliases=[\'web\', \'app\'],
... links={\'app_db\': \'db\', \'another\': None},
... ipv4_address=\'132.65.0.123\',
... gw_priority=100
... )

"""
# Ensure gw_priority is extracted before passing to EndpointConfig
# The actual EndpointConfig class handles the version check and storage.
return EndpointConfig(self._version, *args, **kwargs)

@utils.check_resource('container')
Expand Down
6 changes: 4 additions & 2 deletions docker/api/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def connect_container_to_network(self, container, net_id,
ipv4_address=None, ipv6_address=None,
aliases=None, links=None,
link_local_ips=None, driver_opt=None,
mac_address=None):
mac_address=None, gw_priority=None):
"""
Connect a container to a network.

Expand All @@ -237,14 +237,16 @@ def connect_container_to_network(self, container, net_id,
(IPv4/IPv6) addresses.
mac_address (str): The MAC address of this container on the
network. Defaults to ``None``.
gw_priority (int): The priority of the gateway for this endpoint.
Requires API version 1.48 or higher. Defaults to ``None``.
"""
data = {
"Container": container,
"EndpointConfig": self.create_endpoint_config(
aliases=aliases, links=links, ipv4_address=ipv4_address,
ipv6_address=ipv6_address, link_local_ips=link_local_ips,
driver_opt=driver_opt,
mac_address=mac_address
mac_address=mac_address, gw_priority=gw_priority
),
}

Expand Down
45 changes: 44 additions & 1 deletion docker/types/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,41 @@
class EndpointConfig(dict):
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
ipv6_address=None, link_local_ips=None, driver_opt=None,
mac_address=None):
mac_address=None, gw_priority=None):
"""
Initialize an EndpointConfig object.

Args:
version (str): The API version.
aliases (:py:class:`list`, optional): A list of aliases for this
endpoint. Defaults to ``None``.
links (dict, optional): Mapping of links for this endpoint.
Defaults to ``None``.
ipv4_address (str, optional): The IPv4 address for this endpoint.
Defaults to ``None``.
ipv6_address (str, optional): The IPv6 address for this endpoint.
Defaults to ``None``.
link_local_ips (:py:class:`list`, optional): A list of link-local
(IPv4/IPv6) addresses. Defaults to ``None``.
driver_opt (dict, optional): A dictionary of options to provide to
the network driver. Defaults to ``None``.
mac_address (str, optional): The MAC address for this endpoint.
Requires API version 1.25 or higher. Defaults to ``None``.
gw_priority (int, optional): The priority of the gateway for this
endpoint. Used to determine which network endpoint provides
the default gateway for the container. The endpoint with the
highest priority is selected. If multiple endpoints have the
same priority, endpoints are sorted lexicographically by their
network name, and the one that sorts first is picked.
Allowed values are positive and negative integers.
The default value is 0 if not specified.
Requires API version 1.48 or higher. Defaults to ``None``.

Raises:
errors.InvalidVersion: If a parameter is not supported for the
given API version.
TypeError: If a parameter has an invalid type.
"""
if version_lt(version, '1.22'):
raise errors.InvalidVersion(
'Endpoint config is not supported for API version < 1.22'
Expand Down Expand Up @@ -50,6 +84,15 @@ def __init__(self, version, aliases=None, links=None, ipv4_address=None,
raise TypeError('driver_opt must be a dictionary')
self['DriverOpts'] = driver_opt

if gw_priority is not None:
if version_lt(version, '1.48'):
raise errors.InvalidVersion(
'gw_priority is not supported for API version < 1.48'
)
if not isinstance(gw_priority, int):
raise TypeError('gw_priority must be an integer')
self['GwPriority'] = gw_priority


class NetworkingConfig(dict):
def __init__(self, endpoints_config=None):
Expand Down
21 changes: 21 additions & 0 deletions docs/change_log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
\
## X.Y.Z (UNRELEASED)

**Features**

* Added `gw_priority` parameter to `EndpointConfig` (available in
`create_endpoint_config` and used by `connect_container_to_network`
and `create_container` via `networking_config`). This allows setting
the gateway priority for a container's network endpoint. Requires
Docker API version 1.48 or higher.

**Bugfixes**

* None yet.

**Deprecations**

* None yet.

---
## 7.1.0 (2024-04-08)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,4 @@ ignore = [

[tool.ruff.per-file-ignores]
"**/__init__.py" = ["F401"]
"docker/_version.py" = ["I001"]
65 changes: 65 additions & 0 deletions tests/integration/api_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,71 @@ def test_create_with_uts_mode(self):
assert config['HostConfig']['UTSMode'] == 'host'


@requires_api_version('1.48')
class CreateContainerWithGwPriorityTest(BaseAPIIntegrationTest):
def test_create_container_with_gw_priority(self):
net_name = helpers.random_name()
self.client.create_network(net_name)
self.tmp_networks.append(net_name)

gw_priority_val = 10
container_name = helpers.random_name()

networking_config = self.client.create_networking_config({
net_name: self.client.create_endpoint_config(
gw_priority=gw_priority_val
)
})

container = self.client.create_container(
TEST_IMG,
['sleep', '60'],
name=container_name,
networking_config=networking_config,
host_config=self.client.create_host_config(network_mode=net_name)
)
self.tmp_containers.append(container['Id'])
self.client.start(container['Id'])

inspect_data = self.client.inspect_container(container['Id'])
assert 'NetworkSettings' in inspect_data
assert 'Networks' in inspect_data['NetworkSettings']
assert net_name in inspect_data['NetworkSettings']['Networks']
network_data = inspect_data['NetworkSettings']['Networks'][net_name]
assert 'GwPriority' in network_data
assert network_data['GwPriority'] == gw_priority_val

def test_create_container_with_gw_priority_default(self):
net_name = helpers.random_name()
self.client.create_network(net_name)
self.tmp_networks.append(net_name)

container_name = helpers.random_name()

# GwPriority is not specified, daemon should default to 0
networking_config = self.client.create_networking_config({
net_name: self.client.create_endpoint_config()
})

container = self.client.create_container(
TEST_IMG,
['sleep', '60'],
name=container_name,
networking_config=networking_config,
host_config=self.client.create_host_config(network_mode=net_name)
)
self.tmp_containers.append(container['Id'])
self.client.start(container['Id'])

inspect_data = self.client.inspect_container(container['Id'])
assert 'NetworkSettings' in inspect_data
assert 'Networks' in inspect_data['NetworkSettings']
assert net_name in inspect_data['NetworkSettings']['Networks']
network_data = inspect_data['NetworkSettings']['Networks'][net_name]
assert 'GwPriority' in network_data
assert network_data['GwPriority'] == 0


@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/api_network_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,37 @@ def test_create_inspect_network_with_scope(self):
with pytest.raises(docker.errors.NotFound):
self.client.inspect_network(net_name_swarm, scope='local')

@requires_api_version('1.48')
def test_connect_with_gw_priority(self):
net_name, net_id = self.create_network()

container = self.client.create_container(TEST_IMG, 'top')
self.tmp_containers.append(container)
self.client.start(container)

# Connect with gateway priority
gw_priority_value = 100
self.client.connect_container_to_network(
container, net_name, gw_priority=gw_priority_value
)

container_data = self.client.inspect_container(container)
net_data = container_data['NetworkSettings']['Networks'][net_name]

assert net_data is not None
assert 'GwPriority' in net_data
assert net_data['GwPriority'] == gw_priority_value

# Test with a different priority to ensure update
# gw_priority_value_updated = -50 # Removed unused variable
# Disconnect first - a container can only be connected to a network once
# with a specific configuration. To change gw_priority, we'd typically
# disconnect and reconnect, or update the connection if the API supports it.
# For this test, we are verifying the initial connection and inspection.
# A separate test would be needed for "update" scenarios if supported.

# Clean up: disconnect and remove container and network

def test_create_remove_network_with_space_in_name(self):
net_id = self.client.create_network('test 01')
self.tmp_networks.append(net_id)
Expand Down
71 changes: 69 additions & 2 deletions tests/unit/api_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,9 +956,77 @@ def test_create_container_with_aliases(self):
}}
''')

@requires_api_version('1.48') # Updated API version
def test_create_container_with_gw_priority(self):
"""Test creating a container with gateway priority."""
network_name = 'test-network'
gw_priority_value = 50

# Mock the API version to be >= 1.48 for this test
# self.client.api_version would be the ideal way if it was easily settable for a test
# or if BaseAPIClientTest allowed easy version overriding.
# For now, we assume the client used in tests will respect the @requires_api_version
# or the EndpointConfig internal checks will handle it.

networking_config = self.client.create_networking_config({
network_name: self.client.create_endpoint_config(
gw_priority=gw_priority_value
)
})

self.client.create_container(
'busybox', 'ls',
host_config=self.client.create_host_config(
network_mode=network_name,
),
networking_config=networking_config,
)

args = fake_request.call_args
data = json.loads(args[1]['data'])

assert 'NetworkingConfig' in data
assert 'EndpointsConfig' in data['NetworkingConfig']
assert network_name in data['NetworkingConfig']['EndpointsConfig']
endpoint_cfg = data['NetworkingConfig']['EndpointsConfig'][network_name]
assert 'GwPriority' in endpoint_cfg
assert endpoint_cfg['GwPriority'] == gw_priority_value

@requires_api_version('1.48') # Updated API version
def test_create_container_with_gw_priority_default_value(self):
"""Test creating a container where gw_priority defaults to 0 if not specified."""
network_name = 'test-network-default-gw'

# EndpointConfig should default gw_priority to None if not provided.
# The Docker daemon defaults to 0 if the field is not present in the API call.
# Our EndpointConfig will not include GwPriority if gw_priority is None.
# This test ensures that if we *don't* set it, it's not in the payload.
networking_config = self.client.create_networking_config({
network_name: self.client.create_endpoint_config(
# No gw_priority specified
)
})

self.client.create_container(
'busybox', 'ls',
host_config=self.client.create_host_config(
network_mode=network_name,
),
networking_config=networking_config,
)

args = fake_request.call_args
data = json.loads(args[1]['data'])

assert 'NetworkingConfig' in data
assert 'EndpointsConfig' in data['NetworkingConfig']
assert network_name in data['NetworkingConfig']['EndpointsConfig']
endpoint_cfg = data['NetworkingConfig']['EndpointsConfig'][network_name]
# If not specified, EndpointConfig should not add GwPriority to the dict
assert 'GwPriority' not in endpoint_cfg

@requires_api_version('1.22')
def test_create_container_with_tmpfs_list(self):

self.client.create_container(
'busybox', 'true', host_config=self.client.create_host_config(
tmpfs=[
Expand All @@ -982,7 +1050,6 @@ def test_create_container_with_tmpfs_list(self):

@requires_api_version('1.22')
def test_create_container_with_tmpfs_dict(self):

self.client.create_container(
'busybox', 'true', host_config=self.client.create_host_config(
tmpfs={
Expand Down
Loading