feat: Add gateway_priority support for network connections

Signed-off-by: Adithya1331 <adithyakokkirala@gmail.com>
This commit is contained in:
Adithya1331 2025-05-23 16:46:14 +00:00
parent 526a9db743
commit b29f7461b7
10 changed files with 474 additions and 10 deletions

View File

@ -437,6 +437,29 @@ class ContainerApiMixin:
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):
@ -652,9 +675,10 @@ class ContainerApiMixin:
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
@ -663,6 +687,8 @@ class ContainerApiMixin:
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.
@ -670,12 +696,15 @@ class ContainerApiMixin:
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')

View File

@ -216,7 +216,7 @@ class NetworkApiMixin:
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.
@ -237,6 +237,8 @@ class NetworkApiMixin:
(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,
@ -244,7 +246,7 @@ class NetworkApiMixin:
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
),
}

View File

@ -5,7 +5,41 @@ from ..utils import normalize_links, version_lt
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'
@ -50,6 +84,15 @@ class EndpointConfig(dict):
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):

21
docs/change_log.md Normal file
View File

@ -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)

View File

@ -100,3 +100,4 @@ ignore = [
[tool.ruff.per-file-ignores]
"**/__init__.py" = ["F401"]
"docker/_version.py" = ["I001"]

View File

@ -495,6 +495,71 @@ class CreateContainerTest(BaseAPIIntegrationTest):
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'
)

View File

@ -503,6 +503,37 @@ class TestNetworks(BaseAPIIntegrationTest):
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)

View File

@ -956,9 +956,77 @@ class CreateContainerTest(BaseAPIClientTest):
}}
''')
@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=[
@ -982,7 +1050,6 @@ class CreateContainerTest(BaseAPIClientTest):
@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={

View File

@ -0,0 +1,176 @@
import json
import unittest
from unittest import mock
import pytest
from docker.errors import InvalidVersion
from docker.types import EndpointConfig
from .api_test import BaseAPIClientTest
class NetworkGatewayPriorityTest(BaseAPIClientTest):
"""Tests for the gw-priority feature in network operations."""
def test_connect_container_to_network_with_gw_priority(self):
"""Test connecting a container to a network with gateway priority."""
network_id = 'abc12345'
container_id = 'def45678'
gw_priority = 100
# Create a mock response object
fake_resp = mock.Mock()
fake_resp.status_code = 201
# If the response is expected to have JSON content, mock the json() method
# fake_resp.json = mock.Mock(return_value={}) # Example if JSON is needed
post = mock.Mock(return_value=fake_resp)
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
with mock.patch('docker.api.client.APIClient.post', post):
self.client.connect_container_to_network(
container={'Id': container_id},
net_id=network_id,
gw_priority=gw_priority
)
# Verify the API call was made correctly
# The version in the URL will be based on the client's _version at the time of _url() call
# which happens inside connect_container_to_network.
# Since we patched _version to '1.48', the URL should reflect that.
assert post.call_args[0][0] == f"http+docker://localhost/v1.48/networks/{network_id}/connect"
data = json.loads(post.call_args[1]['data'])
assert data['Container'] == container_id
assert data['EndpointConfig']['GwPriority'] == gw_priority
def test_connect_container_to_network_with_gw_priority_and_other_params(self):
"""Test connecting with gw_priority alongside other parameters."""
network_id = 'abc12345'
container_id = 'def45678'
gw_priority = 200
# Create a mock response object
fake_resp = mock.Mock()
fake_resp.status_code = 201
# If the response is expected to have JSON content, mock the json() method
# fake_resp.json = mock.Mock(return_value={}) # Example if JSON is needed
post = mock.Mock(return_value=fake_resp)
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
with mock.patch('docker.api.client.APIClient.post', post):
self.client.connect_container_to_network(
container={'Id': container_id},
net_id=network_id,
aliases=['web', 'app'],
ipv4_address='192.168.1.100',
gw_priority=gw_priority
)
data = json.loads(post.call_args[1]['data'])
endpoint_config = data['EndpointConfig']
assert endpoint_config['GwPriority'] == gw_priority
assert endpoint_config['Aliases'] == ['web', 'app']
assert endpoint_config['IPAMConfig']['IPv4Address'] == '192.168.1.100'
def test_create_endpoint_config_with_gw_priority(self):
"""Test creating endpoint config with gateway priority."""
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
config = self.client.create_endpoint_config(
gw_priority=150
)
assert config['GwPriority'] == 150
def test_gw_priority_validation_type_error(self):
"""Test that gw_priority must be an integer."""
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
with pytest.raises(TypeError, match='gw_priority must be an integer'):
self.client.create_endpoint_config(gw_priority="100")
def test_gw_priority_valid_values(self):
"""Test that various integer values for gw_priority work correctly."""
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
# Test a positive value
config_positive = self.client.create_endpoint_config(gw_priority=100)
assert config_positive['GwPriority'] == 100
# Test zero
config_zero = self.client.create_endpoint_config(gw_priority=0)
assert config_zero['GwPriority'] == 0
# Test a negative value
config_negative = self.client.create_endpoint_config(gw_priority=-50)
assert config_negative['GwPriority'] == -50
# Test a large positive value
config_large_positive = self.client.create_endpoint_config(gw_priority=70000)
assert config_large_positive['GwPriority'] == 70000
# Test a large negative value
config_large_negative = self.client.create_endpoint_config(gw_priority=-70000)
assert config_large_negative['GwPriority'] == -70000
class EndpointConfigGatewayPriorityTest(unittest.TestCase):
"""Test EndpointConfig class with gateway priority."""
def test_endpoint_config_with_gw_priority_supported_version(self):
"""Test EndpointConfig with gw_priority on supported API version."""
config = EndpointConfig(
version='1.48', # Updated API version
gw_priority=300
)
assert config['GwPriority'] == 300
def test_endpoint_config_with_gw_priority_unsupported_version(self):
"""Test that gw_priority raises error on unsupported API version."""
with pytest.raises(InvalidVersion, match='gw_priority is not supported for API version < 1.48'): # Updated API version
EndpointConfig(
version='1.47', # Updated API version
gw_priority=300
)
def test_endpoint_config_without_gw_priority(self):
"""Test that EndpointConfig works normally without gw_priority."""
config = EndpointConfig(
version='1.48', # Updated API version
aliases=['test'],
ipv4_address='192.168.1.100'
)
assert 'GwPriority' not in config
assert config['Aliases'] == ['test']
assert config['IPAMConfig']['IPv4Address'] == '192.168.1.100'
def test_endpoint_config_gw_priority_type_validation(self):
"""Test type validation for gw_priority in EndpointConfig."""
with pytest.raises(TypeError, match='gw_priority must be an integer'):
EndpointConfig(version='1.48', gw_priority='not_an_int') # Updated API version
def test_endpoint_config_gw_priority_valid_values(self):
"""Test that various integer values for gw_priority work correctly in EndpointConfig."""
# Test a positive value
config_positive = EndpointConfig(version='1.48', gw_priority=100)
assert config_positive['GwPriority'] == 100
# Test zero
config_zero = EndpointConfig(version='1.48', gw_priority=0)
assert config_zero['GwPriority'] == 0
# Test a negative value
config_negative = EndpointConfig(version='1.48', gw_priority=-50)
assert config_negative['GwPriority'] == -50
# Test a large positive value
config_large_positive = EndpointConfig(version='1.48', gw_priority=70000)
assert config_large_positive['GwPriority'] == 70000
# Test a large negative value
config_large_negative = EndpointConfig(version='1.48', gw_priority=-70000)
assert config_large_negative['GwPriority'] == -70000

View File

@ -148,6 +148,35 @@ class NetworkTest(BaseAPIClientTest):
},
}
def test_connect_container_to_network_with_gw_priority(self):
network_id = 'abc12345'
container_id = 'def45678'
post = mock.Mock(return_value=response(status_code=201))
# Mock the API version to be >= 1.48 for this test
with mock.patch.object(self.client, '_version', '1.48'):
with mock.patch('docker.api.client.APIClient.post', post):
self.client.connect_container_to_network(
container={'Id': container_id},
net_id=network_id,
gw_priority=100,
)
# The version in the URL will be based on the client's _version at the time of _url() call
# which happens inside connect_container_to_network.
# Since we patched _version to '1.48', the URL should reflect that.
assert post.call_args[0][0] == (
f"http+docker://localhost/v1.48/networks/{network_id}/connect"
)
assert json.loads(post.call_args[1]['data']) == {
'Container': container_id,
'EndpointConfig': {
'GwPriority': 100,
},
}
def test_disconnect_container_from_network(self):
network_id = 'abc12345'
container_id = 'def45678'