From d6906536ce3dde647a42537e13ef8afc5da54d03 Mon Sep 17 00:00:00 2001 From: Stavros Kois <47820033+stavros-k@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:59:38 +0200 Subject: [PATCH] lib-v2: handle validation of ports for both v6 and v4 (#1332) * better handling of ipv6 and wildcard addresses * update version * cleanup tests * greatly improve readability * update hashes * better check * flake * regen hashes --- library/2.1.8/ports.py | 68 ------ library/2.1.8/tests/test_ports.py | 110 --------- library/{2.1.8 => 2.1.9}/__init__.py | 0 library/{2.1.8 => 2.1.9}/configs.py | 0 library/{2.1.8 => 2.1.9}/container.py | 7 +- library/{2.1.8 => 2.1.9}/depends.py | 0 library/{2.1.8 => 2.1.9}/deploy.py | 0 library/{2.1.8 => 2.1.9}/deps.py | 0 library/{2.1.8 => 2.1.9}/deps_mariadb.py | 0 library/{2.1.8 => 2.1.9}/deps_perms.py | 0 library/{2.1.8 => 2.1.9}/deps_postgres.py | 0 library/{2.1.8 => 2.1.9}/deps_redis.py | 0 library/{2.1.8 => 2.1.9}/device.py | 0 library/{2.1.8 => 2.1.9}/devices.py | 0 library/{2.1.8 => 2.1.9}/dns.py | 0 library/{2.1.8 => 2.1.9}/environment.py | 0 library/{2.1.8 => 2.1.9}/error.py | 0 library/{2.1.8 => 2.1.9}/expose.py | 0 library/{2.1.8 => 2.1.9}/formatter.py | 0 library/{2.1.8 => 2.1.9}/functions.py | 0 library/{2.1.8 => 2.1.9}/healthcheck.py | 0 library/{2.1.8 => 2.1.9}/labels.py | 0 library/{2.1.8 => 2.1.9}/notes.py | 0 library/{2.1.8 => 2.1.9}/portal.py | 0 library/{2.1.8 => 2.1.9}/portals.py | 0 library/2.1.9/ports.py | 153 +++++++++++++ library/{2.1.8 => 2.1.9}/render.py | 0 library/{2.1.8 => 2.1.9}/resources.py | 0 library/{2.1.8 => 2.1.9}/restart.py | 0 library/{2.1.8 => 2.1.9}/storage.py | 0 library/{2.1.8 => 2.1.9}/sysctls.py | 0 library/{2.1.8 => 2.1.9}/tests/__init__.py | 0 .../tests/test_build_image.py | 0 .../{2.1.8 => 2.1.9}/tests/test_configs.py | 0 .../{2.1.8 => 2.1.9}/tests/test_container.py | 14 +- .../{2.1.8 => 2.1.9}/tests/test_depends.py | 0 library/{2.1.8 => 2.1.9}/tests/test_deps.py | 0 library/{2.1.8 => 2.1.9}/tests/test_device.py | 0 library/{2.1.8 => 2.1.9}/tests/test_dns.py | 0 .../tests/test_environment.py | 0 library/{2.1.8 => 2.1.9}/tests/test_expose.py | 0 .../{2.1.8 => 2.1.9}/tests/test_formatter.py | 0 .../{2.1.8 => 2.1.9}/tests/test_functions.py | 0 .../tests/test_healthcheck.py | 0 library/{2.1.8 => 2.1.9}/tests/test_labels.py | 0 library/{2.1.8 => 2.1.9}/tests/test_notes.py | 0 library/{2.1.8 => 2.1.9}/tests/test_portal.py | 0 library/2.1.9/tests/test_ports.py | 209 ++++++++++++++++++ library/{2.1.8 => 2.1.9}/tests/test_render.py | 0 .../{2.1.8 => 2.1.9}/tests/test_resources.py | 0 .../{2.1.8 => 2.1.9}/tests/test_restart.py | 0 .../{2.1.8 => 2.1.9}/tests/test_sysctls.py | 0 .../tests/test_validations.py | 0 .../{2.1.8 => 2.1.9}/tests/test_volumes.py | 0 library/{2.1.8 => 2.1.9}/validations.py | 0 library/{2.1.8 => 2.1.9}/volume_mount.py | 0 .../{2.1.8 => 2.1.9}/volume_mount_types.py | 0 library/{2.1.8 => 2.1.9}/volume_sources.py | 0 library/{2.1.8 => 2.1.9}/volume_types.py | 0 library/{2.1.8 => 2.1.9}/volumes.py | 0 library/hashes.yaml | 2 +- 61 files changed, 379 insertions(+), 184 deletions(-) delete mode 100644 library/2.1.8/ports.py delete mode 100644 library/2.1.8/tests/test_ports.py rename library/{2.1.8 => 2.1.9}/__init__.py (100%) rename library/{2.1.8 => 2.1.9}/configs.py (100%) rename library/{2.1.8 => 2.1.9}/container.py (97%) rename library/{2.1.8 => 2.1.9}/depends.py (100%) rename library/{2.1.8 => 2.1.9}/deploy.py (100%) rename library/{2.1.8 => 2.1.9}/deps.py (100%) rename library/{2.1.8 => 2.1.9}/deps_mariadb.py (100%) rename library/{2.1.8 => 2.1.9}/deps_perms.py (100%) rename library/{2.1.8 => 2.1.9}/deps_postgres.py (100%) rename library/{2.1.8 => 2.1.9}/deps_redis.py (100%) rename library/{2.1.8 => 2.1.9}/device.py (100%) rename library/{2.1.8 => 2.1.9}/devices.py (100%) rename library/{2.1.8 => 2.1.9}/dns.py (100%) rename library/{2.1.8 => 2.1.9}/environment.py (100%) rename library/{2.1.8 => 2.1.9}/error.py (100%) rename library/{2.1.8 => 2.1.9}/expose.py (100%) rename library/{2.1.8 => 2.1.9}/formatter.py (100%) rename library/{2.1.8 => 2.1.9}/functions.py (100%) rename library/{2.1.8 => 2.1.9}/healthcheck.py (100%) rename library/{2.1.8 => 2.1.9}/labels.py (100%) rename library/{2.1.8 => 2.1.9}/notes.py (100%) rename library/{2.1.8 => 2.1.9}/portal.py (100%) rename library/{2.1.8 => 2.1.9}/portals.py (100%) create mode 100644 library/2.1.9/ports.py rename library/{2.1.8 => 2.1.9}/render.py (100%) rename library/{2.1.8 => 2.1.9}/resources.py (100%) rename library/{2.1.8 => 2.1.9}/restart.py (100%) rename library/{2.1.8 => 2.1.9}/storage.py (100%) rename library/{2.1.8 => 2.1.9}/sysctls.py (100%) rename library/{2.1.8 => 2.1.9}/tests/__init__.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_build_image.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_configs.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_container.py (96%) rename library/{2.1.8 => 2.1.9}/tests/test_depends.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_deps.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_device.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_dns.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_environment.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_expose.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_formatter.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_functions.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_healthcheck.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_labels.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_notes.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_portal.py (100%) create mode 100644 library/2.1.9/tests/test_ports.py rename library/{2.1.8 => 2.1.9}/tests/test_render.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_resources.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_restart.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_sysctls.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_validations.py (100%) rename library/{2.1.8 => 2.1.9}/tests/test_volumes.py (100%) rename library/{2.1.8 => 2.1.9}/validations.py (100%) rename library/{2.1.8 => 2.1.9}/volume_mount.py (100%) rename library/{2.1.8 => 2.1.9}/volume_mount_types.py (100%) rename library/{2.1.8 => 2.1.9}/volume_sources.py (100%) rename library/{2.1.8 => 2.1.9}/volume_types.py (100%) rename library/{2.1.8 => 2.1.9}/volumes.py (100%) diff --git a/library/2.1.8/ports.py b/library/2.1.8/ports.py deleted file mode 100644 index 0512108116..0000000000 --- a/library/2.1.8/ports.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from render import Render - -try: - from .error import RenderError - from .validations import ( - valid_ip_or_raise, - valid_port_mode_or_raise, - valid_port_or_raise, - valid_port_protocol_or_raise, - ) -except ImportError: - from error import RenderError - from validations import ( - valid_ip_or_raise, - valid_port_mode_or_raise, - valid_port_or_raise, - valid_port_protocol_or_raise, - ) - - -class Ports: - def __init__(self, render_instance: "Render"): - self._render_instance = render_instance - self._ports: dict[str, dict] = {} - - def add_port(self, host_port: int, container_port: int, config: dict | None = None): - config = config or {} - host_port = valid_port_or_raise(host_port) - container_port = valid_port_or_raise(container_port) - proto = valid_port_protocol_or_raise(config.get("protocol", "tcp")) - mode = valid_port_mode_or_raise(config.get("mode", "ingress")) - host_ip = valid_ip_or_raise(config.get("host_ip", "0.0.0.0")) - - key = f"{host_port}_{host_ip}_{proto}" - if key in self._ports.keys(): - raise RenderError(f"Port [{host_port}/{proto}] already added for [{host_ip}]") - - if host_ip != "0.0.0.0": - # If the port we are adding is not going to use 0.0.0.0 - # Make sure that we don't have already added that port/proto to 0.0.0.0 - search_key = f"{host_port}_0.0.0.0_{proto}" - if search_key in self._ports.keys(): - raise RenderError(f"Cannot bind port [{host_port}/{proto}] to [{host_ip}], already bound to [0.0.0.0]") - elif host_ip == "0.0.0.0": - # If the port we are adding is going to use 0.0.0.0 - # Make sure that we don't have already added that port/proto to a specific ip - for p in self._ports.values(): - if p["published"] == host_port and p["protocol"] == proto: - raise RenderError( - f"Cannot bind port [{host_port}/{proto}] to [{host_ip}], already bound to [{p['host_ip']}]" - ) - - self._ports[key] = { - "published": host_port, - "target": container_port, - "protocol": proto, - "mode": mode, - "host_ip": host_ip, - } - - def has_ports(self): - return len(self._ports) > 0 - - def render(self): - return [config for _, config in sorted(self._ports.items())] diff --git a/library/2.1.8/tests/test_ports.py b/library/2.1.8/tests/test_ports.py deleted file mode 100644 index a4c923ca1d..0000000000 --- a/library/2.1.8/tests/test_ports.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest - - -from render import Render - - -@pytest.fixture -def mock_values(): - return { - "images": { - "test_image": { - "repository": "nginx", - "tag": "latest", - } - }, - } - - -def test_add_ports(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - c1.ports.add_port(8081, 8080) - c1.ports.add_port(8082, 8080, {"protocol": "udp"}) - output = render.render() - assert output["services"]["test_container"]["ports"] == [ - {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "0.0.0.0"}, - {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "0.0.0.0"}, - ] - - -def test_add_duplicate_ports(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - c1.ports.add_port(8081, 8080) - c1.ports.add_port(8081, 8080, {"protocol": "udp"}) # This should not raise - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080) - - -def test_add_duplicate_ports_with_different_host_ip(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - c1.ports.add_port(8081, 8080, {"host_ip": "192.168.1.10"}) - c1.ports.add_port(8081, 8080, {"host_ip": "192.168.1.11"}) - output = render.render() - assert output["services"]["test_container"]["ports"] == [ - {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"}, - {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"}, - ] - - -def test_add_duplicate_ports_to_specific_host_ip_binds_to_0_0_0_0(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - c1.ports.add_port(8081, 8080, {"host_ip": "192.168.1.10"}) - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080, {"host_ip": "0.0.0.0"}) - - -def test_add_duplicate_ports_to_0_0_0_0_binds_to_specific_host_ip(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - c1.ports.add_port(8081, 8080, {"host_ip": "0.0.0.0"}) - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080, {"host_ip": "192.168.1.10"}) - - -def test_add_ports_with_invalid_protocol(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080, {"protocol": "invalid_protocol"}) - - -def test_add_ports_with_invalid_mode(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080, {"mode": "invalid_mode"}) - - -def test_add_ports_with_invalid_ip(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - with pytest.raises(Exception): - c1.ports.add_port(8081, 8080, {"host_ip": "invalid_ip"}) - - -def test_add_ports_with_invalid_host_port(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - with pytest.raises(Exception): - c1.ports.add_port(-1, 8080) - - -def test_add_ports_with_invalid_container_port(mock_values): - render = Render(mock_values) - c1 = render.add_container("test_container", "test_image") - c1.healthcheck.disable() - with pytest.raises(Exception): - c1.ports.add_port(8081, -1) diff --git a/library/2.1.8/__init__.py b/library/2.1.9/__init__.py similarity index 100% rename from library/2.1.8/__init__.py rename to library/2.1.9/__init__.py diff --git a/library/2.1.8/configs.py b/library/2.1.9/configs.py similarity index 100% rename from library/2.1.8/configs.py rename to library/2.1.9/configs.py diff --git a/library/2.1.8/container.py b/library/2.1.9/container.py similarity index 97% rename from library/2.1.8/container.py rename to library/2.1.9/container.py index 3e787abc5f..d0abd6edc7 100644 --- a/library/2.1.8/container.py +++ b/library/2.1.9/container.py @@ -235,10 +235,13 @@ def add_port(self, port_config: dict | None = None, dev_config: dict | None = No host_port = config.get("port_number", 0) container_port = config.get("container_port", 0) or host_port protocol = config.get("protocol", "tcp") - host_ip = config.get("host_ip", "0.0.0.0") + host_ips = config.get("host_ips", ["0.0.0.0", "::"]) + if not isinstance(host_ips, list): + raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]") if bind_mode == "published": - self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip}) + for host_ip in host_ips: + self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip}) elif bind_mode == "exposed": self.expose.add_port(container_port, protocol) diff --git a/library/2.1.8/depends.py b/library/2.1.9/depends.py similarity index 100% rename from library/2.1.8/depends.py rename to library/2.1.9/depends.py diff --git a/library/2.1.8/deploy.py b/library/2.1.9/deploy.py similarity index 100% rename from library/2.1.8/deploy.py rename to library/2.1.9/deploy.py diff --git a/library/2.1.8/deps.py b/library/2.1.9/deps.py similarity index 100% rename from library/2.1.8/deps.py rename to library/2.1.9/deps.py diff --git a/library/2.1.8/deps_mariadb.py b/library/2.1.9/deps_mariadb.py similarity index 100% rename from library/2.1.8/deps_mariadb.py rename to library/2.1.9/deps_mariadb.py diff --git a/library/2.1.8/deps_perms.py b/library/2.1.9/deps_perms.py similarity index 100% rename from library/2.1.8/deps_perms.py rename to library/2.1.9/deps_perms.py diff --git a/library/2.1.8/deps_postgres.py b/library/2.1.9/deps_postgres.py similarity index 100% rename from library/2.1.8/deps_postgres.py rename to library/2.1.9/deps_postgres.py diff --git a/library/2.1.8/deps_redis.py b/library/2.1.9/deps_redis.py similarity index 100% rename from library/2.1.8/deps_redis.py rename to library/2.1.9/deps_redis.py diff --git a/library/2.1.8/device.py b/library/2.1.9/device.py similarity index 100% rename from library/2.1.8/device.py rename to library/2.1.9/device.py diff --git a/library/2.1.8/devices.py b/library/2.1.9/devices.py similarity index 100% rename from library/2.1.8/devices.py rename to library/2.1.9/devices.py diff --git a/library/2.1.8/dns.py b/library/2.1.9/dns.py similarity index 100% rename from library/2.1.8/dns.py rename to library/2.1.9/dns.py diff --git a/library/2.1.8/environment.py b/library/2.1.9/environment.py similarity index 100% rename from library/2.1.8/environment.py rename to library/2.1.9/environment.py diff --git a/library/2.1.8/error.py b/library/2.1.9/error.py similarity index 100% rename from library/2.1.8/error.py rename to library/2.1.9/error.py diff --git a/library/2.1.8/expose.py b/library/2.1.9/expose.py similarity index 100% rename from library/2.1.8/expose.py rename to library/2.1.9/expose.py diff --git a/library/2.1.8/formatter.py b/library/2.1.9/formatter.py similarity index 100% rename from library/2.1.8/formatter.py rename to library/2.1.9/formatter.py diff --git a/library/2.1.8/functions.py b/library/2.1.9/functions.py similarity index 100% rename from library/2.1.8/functions.py rename to library/2.1.9/functions.py diff --git a/library/2.1.8/healthcheck.py b/library/2.1.9/healthcheck.py similarity index 100% rename from library/2.1.8/healthcheck.py rename to library/2.1.9/healthcheck.py diff --git a/library/2.1.8/labels.py b/library/2.1.9/labels.py similarity index 100% rename from library/2.1.8/labels.py rename to library/2.1.9/labels.py diff --git a/library/2.1.8/notes.py b/library/2.1.9/notes.py similarity index 100% rename from library/2.1.8/notes.py rename to library/2.1.9/notes.py diff --git a/library/2.1.8/portal.py b/library/2.1.9/portal.py similarity index 100% rename from library/2.1.8/portal.py rename to library/2.1.9/portal.py diff --git a/library/2.1.8/portals.py b/library/2.1.9/portals.py similarity index 100% rename from library/2.1.8/portals.py rename to library/2.1.9/portals.py diff --git a/library/2.1.9/ports.py b/library/2.1.9/ports.py new file mode 100644 index 0000000000..e571232ac9 --- /dev/null +++ b/library/2.1.9/ports.py @@ -0,0 +1,153 @@ +import ipaddress +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from render import Render + +try: + from .error import RenderError + from .validations import ( + valid_ip_or_raise, + valid_port_mode_or_raise, + valid_port_or_raise, + valid_port_protocol_or_raise, + ) +except ImportError: + from error import RenderError + from validations import ( + valid_ip_or_raise, + valid_port_mode_or_raise, + valid_port_or_raise, + valid_port_protocol_or_raise, + ) + + +class Ports: + def __init__(self, render_instance: "Render"): + self._render_instance = render_instance + self._ports: dict[str, dict] = {} + + def _gen_port_key(self, host_port: int, host_ip: str, proto: str, ip_family: int) -> str: + return f"{host_port}_{host_ip}_{proto}_{ip_family}" + + def _is_wildcard_ip(self, ip: str) -> bool: + return ip in ["0.0.0.0", "::"] + + def _get_opposite_wildcard(self, ip: str) -> str: + return "0.0.0.0" if ip == "::" else "::" + + def _get_sort_key(self, p: dict) -> str: + return f"{p['published']}_{p['target']}_{p['protocol']}_{p.get('host_ip', '_')}" + + def _is_ports_same(self, port1: dict, port2: dict) -> bool: + return ( + port1["published"] == port2["published"] + and port1["target"] == port2["target"] + and port1["protocol"] == port2["protocol"] + and port1.get("host_ip", "_") == port2.get("host_ip", "_") + ) + + def _has_opposite_family_port(self, port_config: dict, wildcard_ports: dict) -> bool: + comparison_port = port_config.copy() + comparison_port["host_ip"] = self._get_opposite_wildcard(port_config["host_ip"]) + for p in wildcard_ports.values(): + if self._is_ports_same(comparison_port, p): + return True + return False + + def _check_port_conflicts(self, port_config: dict, ip_family: int) -> None: + host_port = port_config["published"] + host_ip = port_config["host_ip"] + proto = port_config["protocol"] + + key = self._gen_port_key(host_port, host_ip, proto, ip_family) + + if key in self._ports.keys(): + raise RenderError(f"Port [{host_port}/{proto}/ipv{ip_family}] already added for [{host_ip}]") + + wildcard_ip = "0.0.0.0" if ip_family == 4 else "::" + if host_ip != wildcard_ip: + # Check if there is a port with same details but with wildcard IP of the same family + wildcard_key = self._gen_port_key(host_port, wildcard_ip, proto, ip_family) + if wildcard_key in self._ports.keys(): + raise RenderError( + f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], " + f"already bound to [{wildcard_ip}]" + ) + else: + # We are adding a port with wildcard IP + # Check if there is a port with same details but with specific IP of the same family + for p in self._ports.values(): + # Skip if the port is not for the same family + if ip_family != ipaddress.ip_address(p["host_ip"]).version: + continue + + # Make a copy of the port config + search_port = p.copy() + # Replace the host IP with wildcard IP + search_port["host_ip"] = wildcard_ip + # If the ports match, means that a port for specific IP is already added + # and we are trying to add it again with wildcard IP. Raise an error + if self._is_ports_same(search_port, port_config): + raise RenderError( + f"Cannot bind port [{host_port}/{proto}/ipv{ip_family}] to [{host_ip}], " + f"already bound to [{p['host_ip']}]" + ) + + def add_port(self, host_port: int, container_port: int, config: dict | None = None): + config = config or {} + host_port = valid_port_or_raise(host_port) + container_port = valid_port_or_raise(container_port) + proto = valid_port_protocol_or_raise(config.get("protocol", "tcp")) + mode = valid_port_mode_or_raise(config.get("mode", "ingress")) + + # TODO: Once all apps stop using this function directly, (ie using the container.add_port function) + # Remove this, and let container.add_port call this for each host_ip + host_ip = config.get("host_ip", None) + if host_ip is None: + self.add_port(host_port, container_port, config | {"host_ip": "0.0.0.0"}) + self.add_port(host_port, container_port, config | {"host_ip": "::"}) + return + + host_ip = valid_ip_or_raise(config.get("host_ip", None)) + ip = ipaddress.ip_address(host_ip) + + port_config = { + "published": host_port, + "target": container_port, + "protocol": proto, + "mode": mode, + "host_ip": host_ip, + } + self._check_port_conflicts(port_config, ip.version) + + key = self._gen_port_key(host_port, host_ip, proto, ip.version) + self._ports[key] = port_config + + def has_ports(self): + return len(self._ports) > 0 + + def render(self): + specific_ports = [] + wildcard_ports = {} + + for port_config in self._ports.values(): + if self._is_wildcard_ip(port_config["host_ip"]): + wildcard_ports[id(port_config)] = port_config.copy() + else: + specific_ports.append(port_config.copy()) + + processed_ports = specific_ports.copy() + for wild_port in wildcard_ports.values(): + processed_port = wild_port.copy() + + # Check if there's a matching wildcard port for the opposite IP family + has_opposite_family = self._has_opposite_family_port(wild_port, wildcard_ports) + + if has_opposite_family: + processed_port.pop("host_ip") + + if processed_port not in processed_ports: + processed_ports.append(processed_port) + + return sorted(processed_ports, key=self._get_sort_key) diff --git a/library/2.1.8/render.py b/library/2.1.9/render.py similarity index 100% rename from library/2.1.8/render.py rename to library/2.1.9/render.py diff --git a/library/2.1.8/resources.py b/library/2.1.9/resources.py similarity index 100% rename from library/2.1.8/resources.py rename to library/2.1.9/resources.py diff --git a/library/2.1.8/restart.py b/library/2.1.9/restart.py similarity index 100% rename from library/2.1.8/restart.py rename to library/2.1.9/restart.py diff --git a/library/2.1.8/storage.py b/library/2.1.9/storage.py similarity index 100% rename from library/2.1.8/storage.py rename to library/2.1.9/storage.py diff --git a/library/2.1.8/sysctls.py b/library/2.1.9/sysctls.py similarity index 100% rename from library/2.1.8/sysctls.py rename to library/2.1.9/sysctls.py diff --git a/library/2.1.8/tests/__init__.py b/library/2.1.9/tests/__init__.py similarity index 100% rename from library/2.1.8/tests/__init__.py rename to library/2.1.9/tests/__init__.py diff --git a/library/2.1.8/tests/test_build_image.py b/library/2.1.9/tests/test_build_image.py similarity index 100% rename from library/2.1.8/tests/test_build_image.py rename to library/2.1.9/tests/test_build_image.py diff --git a/library/2.1.8/tests/test_configs.py b/library/2.1.9/tests/test_configs.py similarity index 100% rename from library/2.1.8/tests/test_configs.py rename to library/2.1.9/tests/test_configs.py diff --git a/library/2.1.8/tests/test_container.py b/library/2.1.9/tests/test_container.py similarity index 96% rename from library/2.1.8/tests/test_container.py rename to library/2.1.9/tests/test_container.py index 1e3c5c81c3..bb3d98dffc 100644 --- a/library/2.1.8/tests/test_container.py +++ b/library/2.1.9/tests/test_container.py @@ -354,8 +354,16 @@ def test_add_ports(mock_values): ) output = render.render() assert output["services"]["test_container"]["ports"] == [ - {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "0.0.0.0"}, - {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "0.0.0.0"}, - {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress", "host_ip": "0.0.0.0"}, + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}, + {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"}, + {"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"}, ] assert output["services"]["test_container"]["expose"] == ["8080/tcp"] + + +def test_add_ports_with_invalid_host_ips(mock_values): + render = Render(mock_values) + c1 = render.add_container("test_container", "test_image") + c1.healthcheck.disable() + with pytest.raises(Exception): + c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"}) diff --git a/library/2.1.8/tests/test_depends.py b/library/2.1.9/tests/test_depends.py similarity index 100% rename from library/2.1.8/tests/test_depends.py rename to library/2.1.9/tests/test_depends.py diff --git a/library/2.1.8/tests/test_deps.py b/library/2.1.9/tests/test_deps.py similarity index 100% rename from library/2.1.8/tests/test_deps.py rename to library/2.1.9/tests/test_deps.py diff --git a/library/2.1.8/tests/test_device.py b/library/2.1.9/tests/test_device.py similarity index 100% rename from library/2.1.8/tests/test_device.py rename to library/2.1.9/tests/test_device.py diff --git a/library/2.1.8/tests/test_dns.py b/library/2.1.9/tests/test_dns.py similarity index 100% rename from library/2.1.8/tests/test_dns.py rename to library/2.1.9/tests/test_dns.py diff --git a/library/2.1.8/tests/test_environment.py b/library/2.1.9/tests/test_environment.py similarity index 100% rename from library/2.1.8/tests/test_environment.py rename to library/2.1.9/tests/test_environment.py diff --git a/library/2.1.8/tests/test_expose.py b/library/2.1.9/tests/test_expose.py similarity index 100% rename from library/2.1.8/tests/test_expose.py rename to library/2.1.9/tests/test_expose.py diff --git a/library/2.1.8/tests/test_formatter.py b/library/2.1.9/tests/test_formatter.py similarity index 100% rename from library/2.1.8/tests/test_formatter.py rename to library/2.1.9/tests/test_formatter.py diff --git a/library/2.1.8/tests/test_functions.py b/library/2.1.9/tests/test_functions.py similarity index 100% rename from library/2.1.8/tests/test_functions.py rename to library/2.1.9/tests/test_functions.py diff --git a/library/2.1.8/tests/test_healthcheck.py b/library/2.1.9/tests/test_healthcheck.py similarity index 100% rename from library/2.1.8/tests/test_healthcheck.py rename to library/2.1.9/tests/test_healthcheck.py diff --git a/library/2.1.8/tests/test_labels.py b/library/2.1.9/tests/test_labels.py similarity index 100% rename from library/2.1.8/tests/test_labels.py rename to library/2.1.9/tests/test_labels.py diff --git a/library/2.1.8/tests/test_notes.py b/library/2.1.9/tests/test_notes.py similarity index 100% rename from library/2.1.8/tests/test_notes.py rename to library/2.1.9/tests/test_notes.py diff --git a/library/2.1.8/tests/test_portal.py b/library/2.1.9/tests/test_portal.py similarity index 100% rename from library/2.1.8/tests/test_portal.py rename to library/2.1.9/tests/test_portal.py diff --git a/library/2.1.9/tests/test_ports.py b/library/2.1.9/tests/test_ports.py new file mode 100644 index 0000000000..7b37a03600 --- /dev/null +++ b/library/2.1.9/tests/test_ports.py @@ -0,0 +1,209 @@ +import pytest + + +from render import Render + + +@pytest.fixture +def mock_values(): + return { + "images": { + "test_image": { + "repository": "nginx", + "tag": "latest", + } + }, + } + + +tests = [ + { + "name": "add_ports_should_work", + "inputs": [ + {"values": (8081, 8080), "expect_error": False}, + {"values": (8082, 8080, {"protocol": "udp"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}, + {"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"}, + ], + }, + { + "name": "add_duplicate_ports_should_fail", + "inputs": [ + {"values": (8081, 8080), "expect_error": False}, + {"values": (8081, 8080), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_port_different_protocol_should_work", + "inputs": [ + {"values": (8081, 8080), "expect_error": False}, + {"values": (8081, 8080, {"protocol": "udp"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}, + {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress"}, + ], + }, + { + "name": "adding_same_port_for_both_wildcard_families_should_work", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}, + ], + }, + { + "name": "adding_duplicate_port_for_v4_ip_and_v4_wildcard_should_fail", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_port_for_v4_wildcard_and_v4_ip_should_fail", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "0.0.0.0"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_port_for_v4_wildcard_and_v6_ip_should_work", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"}, + { + "published": 8081, + "target": 8080, + "protocol": "tcp", + "mode": "ingress", + "host_ip": "fd00:1234:5678:abcd::10", + }, + ], + }, + { + "name": "adding_duplicate_port_for_v6_wildcard_and_v4_ip_should_work", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"}, + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "::"}, + ], + }, + { + "name": "adding_duplicate_port_for_v6_wildcard_and_v6_ip_should_fail", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_port_for_v6_ip_and_v6_wildcard_should_fail", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "::"}), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_port_with_different_v4_ip_should_work", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False}, + ], + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"}, + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"}, + ], + }, + { + "name": "adding_port_with_invalid_protocol_should_fail", + "inputs": [ + {"values": (8081, 8080, {"protocol": "invalid_protocol"}), "expect_error": True}, + ], + }, + { + "name": "adding_port_with_invalid_mode_should_fail", + "inputs": [ + {"values": (8081, 8080, {"mode": "invalid_mode"}), "expect_error": True}, + ], + }, + { + "name": "adding_port_with_invalid_ip_should_fail", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "invalid_ip"}), "expect_error": True}, + ], + }, + { + "name": "adding_port_with_invalid_host_port_should_fail", + "inputs": [ + {"values": (-1, 8080), "expect_error": True}, + ], + }, + { + "name": "adding_port_with_invalid_container_port_should_fail", + "inputs": [ + {"values": (8081, -1), "expect_error": True}, + ], + }, + { + "name": "adding_duplicate_ports_with_different_host_ip_should_work", + "inputs": [ + {"values": (8081, 8080, {"host_ip": "192.168.1.10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.10", "protocol": "udp"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.11"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "192.168.1.11", "protocol": "udp"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::10"}), "expect_error": False}, + {"values": (8081, 8080, {"host_ip": "fd00:1234:5678:abcd::11"}), "expect_error": False}, + ], + # fmt: off + "expected": [ + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.10"}, + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "192.168.1.11"}, + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::10"}, # noqa + {"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress", "host_ip": "fd00:1234:5678:abcd::11"}, # noqa + {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.10"}, + {"published": 8081, "target": 8080, "protocol": "udp", "mode": "ingress", "host_ip": "192.168.1.11"}, + ], + # fmt: on + }, +] + + +@pytest.mark.parametrize("test", tests) +def test_ports(test): + mock_values = { + "images": { + "test_image": { + "repository": "nginx", + "tag": "latest", + } + }, + } + + render = Render(mock_values) + c1 = render.add_container("test_container", "test_image") + c1.healthcheck.disable() + + errored = False + for input in test["inputs"]: + if input["expect_error"]: + with pytest.raises(Exception): + c1.ports.add_port(*input["values"]) + errored = True + else: + c1.ports.add_port(*input["values"]) + + errored = True if [i["expect_error"] for i in test["inputs"]].count(True) > 0 else False + if errored: + return + + output = render.render() + assert output["services"]["test_container"]["ports"] == test["expected"] diff --git a/library/2.1.8/tests/test_render.py b/library/2.1.9/tests/test_render.py similarity index 100% rename from library/2.1.8/tests/test_render.py rename to library/2.1.9/tests/test_render.py diff --git a/library/2.1.8/tests/test_resources.py b/library/2.1.9/tests/test_resources.py similarity index 100% rename from library/2.1.8/tests/test_resources.py rename to library/2.1.9/tests/test_resources.py diff --git a/library/2.1.8/tests/test_restart.py b/library/2.1.9/tests/test_restart.py similarity index 100% rename from library/2.1.8/tests/test_restart.py rename to library/2.1.9/tests/test_restart.py diff --git a/library/2.1.8/tests/test_sysctls.py b/library/2.1.9/tests/test_sysctls.py similarity index 100% rename from library/2.1.8/tests/test_sysctls.py rename to library/2.1.9/tests/test_sysctls.py diff --git a/library/2.1.8/tests/test_validations.py b/library/2.1.9/tests/test_validations.py similarity index 100% rename from library/2.1.8/tests/test_validations.py rename to library/2.1.9/tests/test_validations.py diff --git a/library/2.1.8/tests/test_volumes.py b/library/2.1.9/tests/test_volumes.py similarity index 100% rename from library/2.1.8/tests/test_volumes.py rename to library/2.1.9/tests/test_volumes.py diff --git a/library/2.1.8/validations.py b/library/2.1.9/validations.py similarity index 100% rename from library/2.1.8/validations.py rename to library/2.1.9/validations.py diff --git a/library/2.1.8/volume_mount.py b/library/2.1.9/volume_mount.py similarity index 100% rename from library/2.1.8/volume_mount.py rename to library/2.1.9/volume_mount.py diff --git a/library/2.1.8/volume_mount_types.py b/library/2.1.9/volume_mount_types.py similarity index 100% rename from library/2.1.8/volume_mount_types.py rename to library/2.1.9/volume_mount_types.py diff --git a/library/2.1.8/volume_sources.py b/library/2.1.9/volume_sources.py similarity index 100% rename from library/2.1.8/volume_sources.py rename to library/2.1.9/volume_sources.py diff --git a/library/2.1.8/volume_types.py b/library/2.1.9/volume_types.py similarity index 100% rename from library/2.1.8/volume_types.py rename to library/2.1.9/volume_types.py diff --git a/library/2.1.8/volumes.py b/library/2.1.9/volumes.py similarity index 100% rename from library/2.1.8/volumes.py rename to library/2.1.9/volumes.py diff --git a/library/hashes.yaml b/library/hashes.yaml index 6df9a74c97..feef728ab5 100644 --- a/library/hashes.yaml +++ b/library/hashes.yaml @@ -1,2 +1,2 @@ 0.0.1: f074617a82a86d2a6cc78a4c8a4296fc9d168e456f12713e50c696557b302133 -2.1.8: a3251fe8d434248fc48ce7e1a29de015dab56de3687a93b925e79c986db2be44 +2.1.9: 6bd4d433db7dce2d4b8cc456a0f7874b45d52a6e2b145d5a1f48f327654a7125