From a023dc145b35b74f7166eaecd1a2f09ebc37a092 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:22 +0200 Subject: [PATCH 01/15] Extract get_net_args_from_network_mode() Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 60 +++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 09086317..c86f989e 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -877,38 +877,46 @@ async def assert_cnt_nets(compose, cnt): await compose.podman.output([], "network", ["exists", net_name]) +def get_net_args_from_network_mode(compose, cnt): + net_args = [] + net = cnt.get("network_mode") + + is_bridge = False + if net == "none": + net_args.append("--network=none") + elif net == "host": + net_args.append(f"--network={net}") + elif net.startswith("slirp4netns"): # Note: podman-specific network mode + net_args.append(f"--network={net}") + elif net == "private": # Note: podman-specific network mode + net_args.append("--network=private") + elif net.startswith("pasta"): # Note: podman-specific network mode + net_args.append(f"--network={net}") + elif net.startswith("ns:"): # Note: podman-specific network mode + net_args.append(f"--network={net}") + elif net.startswith("service:"): + other_srv = net.split(":", 1)[1].strip() + other_cnt = compose.container_names_by_service[other_srv][0] + net_args.append(f"--network=container:{other_cnt}") + elif net.startswith("container:"): + other_cnt = net.split(":", 1)[1].strip() + net_args.append(f"--network=container:{other_cnt}") + elif net.startswith("bridge"): + is_bridge = True + else: + log.fatal("unknown network_mode [%s]", net) + sys.exit(1) + + return net_args, is_bridge + + def get_net_args(compose, cnt): service_name = cnt["service_name"] net_args = [] - is_bridge = False mac_address = cnt.get("mac_address") net = cnt.get("network_mode") if net: - if net == "none": - is_bridge = False - net_args.append("--network=none") - elif net == "host": - net_args.append(f"--network={net}") - elif net.startswith("slirp4netns"): # Note: podman-specific network mode - net_args.append(f"--network={net}") - elif net == "private": # Note: podman-specific network mode - net_args.append("--network=private") - elif net.startswith("pasta"): # Note: podman-specific network mode - net_args.append(f"--network={net}") - elif net.startswith("ns:"): # Note: podman-specific network mode - net_args.append(f"--network={net}") - elif net.startswith("service:"): - other_srv = net.split(":", 1)[1].strip() - other_cnt = compose.container_names_by_service[other_srv][0] - net_args.append(f"--network=container:{other_cnt}") - elif net.startswith("container:"): - other_cnt = net.split(":", 1)[1].strip() - net_args.append(f"--network=container:{other_cnt}") - elif net.startswith("bridge"): - is_bridge = True - else: - log.fatal("unknown network_mode [%s]", net) - sys.exit(1) + net_args, is_bridge = get_net_args_from_network_mode(compose, cnt) else: is_bridge = True cnt_nets = cnt.get("networks") From 2891be01d75920503d70d55b659d594cc589c6c8 Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Wed, 18 Dec 2024 19:59:23 +0200 Subject: [PATCH 02/15] Ensure that network_mode and networks are not present at the same time See https://docs.docker.com/reference/compose-file/services/#network_mode Signed-off-by: Povilas Kanapickas --- newsfragments/network_mode_networks.bugfix | 2 ++ podman_compose.py | 6 ++++++ tests/unit/test_get_net_args.py | 9 +++++++++ 3 files changed, 17 insertions(+) create mode 100644 newsfragments/network_mode_networks.bugfix diff --git a/newsfragments/network_mode_networks.bugfix b/newsfragments/network_mode_networks.bugfix new file mode 100644 index 00000000..18ca32b2 --- /dev/null +++ b/newsfragments/network_mode_networks.bugfix @@ -0,0 +1,2 @@ +Improved error detection by rejecting service definitions that contain both "network_mode" and +"networks" keys, which is not allowed. diff --git a/podman_compose.py b/podman_compose.py index c86f989e..13674978 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -880,6 +880,12 @@ async def assert_cnt_nets(compose, cnt): def get_net_args_from_network_mode(compose, cnt): net_args = [] net = cnt.get("network_mode") + service_name = cnt["service_name"] + + if "networks" in cnt: + raise ValueError( + f"networks and network_mode must not be present in the same service [{service_name}]" + ) is_bridge = False if net == "none": diff --git a/tests/unit/test_get_net_args.py b/tests/unit/test_get_net_args.py index 586067f0..aec07423 100644 --- a/tests/unit/test_get_net_args.py +++ b/tests/unit/test_get_net_args.py @@ -59,6 +59,15 @@ def test_one_net(self): args = get_net_args(compose, container) self.assertListEqual(expected_args, args) + def test_network_mode_and_networks_unsupported(self): + compose = get_networked_compose() + container = get_minimal_container() + container["networks"] = {"net0": {}} + container["network_mode"] = "none" + + with self.assertRaises(ValueError): + get_net_args(compose, container) + def test_alias(self): compose = get_networked_compose() container = get_minimal_container() From de2c33d7ae6c147f37613345ac889aee5f48ad06 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:24 +0200 Subject: [PATCH 03/15] Simplify get_net_args_from_network_mode() This commit takes into account that network_mode won't have networks attribute set, accordingly get_net_args_from_network_mode() can calculate complete net_args easily. Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 13674978..07509ea2 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -887,7 +887,6 @@ def get_net_args_from_network_mode(compose, cnt): f"networks and network_mode must not be present in the same service [{service_name}]" ) - is_bridge = False if net == "none": net_args.append("--network=none") elif net == "host": @@ -908,12 +907,20 @@ def get_net_args_from_network_mode(compose, cnt): other_cnt = net.split(":", 1)[1].strip() net_args.append(f"--network=container:{other_cnt}") elif net.startswith("bridge"): - is_bridge = True + aliases_on_container = [service_name] + if cnt.get("_aliases"): + aliases_on_container.extend(cnt.get("_aliases")) + net_args.append("--network=bridge") + mac_address = cnt.get("mac_address") + if mac_address: + net_args.append(f"--mac-address={mac_address}") + for alias in aliases_on_container: + net_args.extend([f"--network-alias={alias}"]) else: log.fatal("unknown network_mode [%s]", net) sys.exit(1) - return net_args, is_bridge + return net_args def get_net_args(compose, cnt): @@ -922,7 +929,7 @@ def get_net_args(compose, cnt): mac_address = cnt.get("mac_address") net = cnt.get("network_mode") if net: - net_args, is_bridge = get_net_args_from_network_mode(compose, cnt) + return get_net_args_from_network_mode(compose, cnt) else: is_bridge = True cnt_nets = cnt.get("networks") From d6da65e6c978c662891d15eda821601d46c5db6b Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:25 +0200 Subject: [PATCH 04/15] Extract get_net_args_from_networks() Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 07509ea2..278e0b40 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -924,14 +924,17 @@ def get_net_args_from_network_mode(compose, cnt): def get_net_args(compose, cnt): - service_name = cnt["service_name"] - net_args = [] - mac_address = cnt.get("mac_address") net = cnt.get("network_mode") if net: return get_net_args_from_network_mode(compose, cnt) - else: - is_bridge = True + + return get_net_args_from_networks(compose, cnt) + + +def get_net_args_from_networks(compose, cnt): + net_args = [] + mac_address = cnt.get("mac_address") + service_name = cnt["service_name"] cnt_nets = cnt.get("networks") aliases = [service_name] @@ -1038,11 +1041,10 @@ def get_net_args(compose, cnt): else: net_args.append(f"--network={net_name}") else: - if is_bridge: - if net_names_str: - net_args.append(f"--network={net_names_str}") - else: - net_args.append("--network=bridge") + if net_names_str: + net_args.append(f"--network={net_names_str}") + else: + net_args.append("--network=bridge") if ip: net_args.append(f"--ip={ip}") if ip6: @@ -1050,9 +1052,8 @@ def get_net_args(compose, cnt): if mac_address: net_args.append(f"--mac-address={mac_address}") - if is_bridge: - for alias in aliases: - net_args.extend([f"--network-alias={alias}"]) + for alias in aliases: + net_args.extend([f"--network-alias={alias}"]) return net_args From a34d1d1a31821dbdf945cd4e71583a8810b72585 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:26 +0200 Subject: [PATCH 05/15] Remove unused ip_assignments in get_net_args_from_networks() Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 278e0b40..8df43aef 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -944,7 +944,6 @@ def get_net_args_from_networks(compose, cnt): # release. ip = None ip6 = None - ip_assignments = 0 if cnt.get("_aliases"): aliases.extend(cnt.get("_aliases")) if cnt_nets and isinstance(cnt_nets, dict): @@ -953,10 +952,6 @@ def get_net_args_from_networks(compose, cnt): for net_key, net_value in cnt_nets.items(): net_value = net_value or {} aliases.extend(norm_as_list(net_value.get("aliases"))) - if net_value.get("ipv4_address") is not None: - ip_assignments = ip_assignments + 1 - if net_value.get("ipv6_address") is not None: - ip_assignments = ip_assignments + 1 if not ip: ip = net_value.get("ipv4_address") From 2dfbb59097ab60d3487ea2dd543187f6f691d808 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:27 +0200 Subject: [PATCH 06/15] Extract multiple network handling in get_net_args_from_networks() Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 54 ++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 8df43aef..f42becf6 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -946,6 +946,34 @@ def get_net_args_from_networks(compose, cnt): ip6 = None if cnt.get("_aliases"): aliases.extend(cnt.get("_aliases")) + + # TODO: add support for per-interface aliases + # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases + # Even though podman accepts network-specific aliases (e.g., --network=bridge:alias=foo, + # podman currently ignores this if a per-container network-alias is set; as pdoman-compose + # always sets a network-alias to the container name, is currently doesn't make sense to + # implement this. + multiple_nets = cnt.get("networks", {}) + + # networks can be specified as a dict with config per network or as a plain list without + # config. Support both cases by converting the plain list to a dict with empty config. + if is_list(multiple_nets): + multiple_nets = {net: {} for net in multiple_nets} + else: + multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()} + + # if a mac_address was specified on the container level, we need to check that it is not + # specified on the network level as well + if mac_address is not None: + for net_config in multiple_nets.values(): + network_mac = net_config.get("x-podman.mac_address") + if network_mac is not None: + raise RuntimeError( + f"conflicting mac addresses {mac_address} and {network_mac}:" + "specifying mac_address on both container and network level " + "is not supported" + ) + if cnt_nets and isinstance(cnt_nets, dict): prioritized_cnt_nets = [] # cnt_nets is {net_key: net_value, ...} @@ -976,33 +1004,7 @@ def get_net_args_from_networks(compose, cnt): net_names.append(net_name) net_names_str = ",".join(net_names) - # TODO: add support for per-interface aliases - # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases - # Even though podman accepts network-specific aliases (e.g., --network=bridge:alias=foo, - # podman currently ignores this if a per-container network-alias is set; as pdoman-compose - # always sets a network-alias to the container name, is currently doesn't make sense to - # implement this. - multiple_nets = cnt.get("networks") if multiple_nets and len(multiple_nets) > 1: - # networks can be specified as a dict with config per network or as a plain list without - # config. Support both cases by converting the plain list to a dict with empty config. - if is_list(multiple_nets): - multiple_nets = {net: {} for net in multiple_nets} - else: - multiple_nets = {net: net_config or {} for net, net_config in multiple_nets.items()} - - # if a mac_address was specified on the container level, we need to check that it is not - # specified on the network level as well - if mac_address is not None: - for net_config_ in multiple_nets.values(): - network_mac = net_config_.get("x-podman.mac_address") - if network_mac is not None: - raise RuntimeError( - f"conflicting mac addresses {mac_address} and {network_mac}:" - "specifying mac_address on both container and network level " - "is not supported" - ) - for net_, net_config_ in multiple_nets.items(): net_desc = compose.networks[net_] or {} is_ext = net_desc.get("external") From 16196a1f6dcffdc5b86792bb16f79a6bc3c7083b Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Wed, 18 Dec 2024 19:59:28 +0200 Subject: [PATCH 07/15] Simplify ip address processing in get_net_args_from_networks() Signed-off-by: Povilas Kanapickas --- podman_compose.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index f42becf6..f98b836a 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -942,8 +942,6 @@ def get_net_args_from_networks(compose, cnt): # NOTE: A container will only have access to aliases on the first network # that it joins. This is a limitation that will be removed in a later # release. - ip = None - ip6 = None if cnt.get("_aliases"): aliases.extend(cnt.get("_aliases")) @@ -981,10 +979,6 @@ def get_net_args_from_networks(compose, cnt): net_value = net_value or {} aliases.extend(norm_as_list(net_value.get("aliases"))) - if not ip: - ip = net_value.get("ipv4_address") - if not ip6: - ip6 = net_value.get("ipv6_address") net_priority = net_value.get("priority", 0) prioritized_cnt_nets.append(( net_priority, @@ -1042,10 +1036,16 @@ def get_net_args_from_networks(compose, cnt): net_args.append(f"--network={net_names_str}") else: net_args.append("--network=bridge") - if ip: - net_args.append(f"--ip={ip}") - if ip6: - net_args.append(f"--ip6={ip6}") + ipv4 = None + ipv6 = None + if multiple_nets: + net_config = list(multiple_nets.values())[0] + ipv4 = net_config.get("ipv4_address") + ipv6 = net_config.get("ipv6_address") + if ipv4: + net_args.append(f"--ip={ipv4}") + if ipv6: + net_args.append(f"--ip6={ipv6}") if mac_address: net_args.append(f"--mac-address={mac_address}") From 590c371db2f5cb8f424da7234cdc431ca6bbd28a Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:29 +0200 Subject: [PATCH 08/15] Move network alias processing to common location Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/podman_compose.py b/podman_compose.py index f98b836a..87c3a008 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -977,7 +977,6 @@ def get_net_args_from_networks(compose, cnt): # cnt_nets is {net_key: net_value, ...} for net_key, net_value in cnt_nets.items(): net_value = net_value or {} - aliases.extend(norm_as_list(net_value.get("aliases"))) net_priority = net_value.get("priority", 0) prioritized_cnt_nets.append(( @@ -1010,6 +1009,7 @@ def get_net_args_from_networks(compose, cnt): ipv6 = net_config_.get("ipv6_address") # custom extension; not supported by docker-compose v3 mac = net_config_.get("x-podman.mac_address") + aliases.extend(norm_as_list(net_config_.get("aliases"))) # if a mac_address was specified on the container level, apply it to the first network # This works for Python > 3.6, because dict insert ordering is preserved, so we are @@ -1042,6 +1042,7 @@ def get_net_args_from_networks(compose, cnt): net_config = list(multiple_nets.values())[0] ipv4 = net_config.get("ipv4_address") ipv6 = net_config.get("ipv6_address") + aliases.extend(norm_as_list(net_config.get("aliases"))) if ipv4: net_args.append(f"--ip={ipv4}") if ipv6: From b324029f2563fe29d3819326f252945d5aefd534 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:30 +0200 Subject: [PATCH 09/15] Simplify network name processing when there is single network Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 87c3a008..5664ff99 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -935,7 +935,6 @@ def get_net_args_from_networks(compose, cnt): net_args = [] mac_address = cnt.get("mac_address") service_name = cnt["service_name"] - cnt_nets = cnt.get("networks") aliases = [service_name] # NOTE: from podman manpage: @@ -972,31 +971,6 @@ def get_net_args_from_networks(compose, cnt): "is not supported" ) - if cnt_nets and isinstance(cnt_nets, dict): - prioritized_cnt_nets = [] - # cnt_nets is {net_key: net_value, ...} - for net_key, net_value in cnt_nets.items(): - net_value = net_value or {} - - net_priority = net_value.get("priority", 0) - prioritized_cnt_nets.append(( - net_priority, - net_key, - )) - # sort dict by priority - prioritized_cnt_nets.sort(reverse=True) - cnt_nets = [net_key for _, net_key in prioritized_cnt_nets] - cnt_nets = norm_as_list(cnt_nets or compose.default_net) - net_names = [] - for net in cnt_nets: - net_desc = compose.networks[net] or {} - is_ext = net_desc.get("external") - ext_desc = is_ext if isinstance(is_ext, str) else {} - default_net_name = default_network_name_for_project(compose, net, is_ext) - net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name - net_names.append(net_name) - net_names_str = ",".join(net_names) - if multiple_nets and len(multiple_nets) > 1: for net_, net_config_ in multiple_nets.items(): net_desc = compose.networks[net_] or {} @@ -1032,10 +1006,17 @@ def get_net_args_from_networks(compose, cnt): else: net_args.append(f"--network={net_name}") else: - if net_names_str: - net_args.append(f"--network={net_names_str}") + if multiple_nets or compose.default_net: + net = list(multiple_nets.keys())[0] if multiple_nets else compose.default_net + net_desc = compose.networks[net] or {} + is_ext = net_desc.get("external") + ext_desc = is_ext if isinstance(is_ext, str) else {} + default_net_name = default_network_name_for_project(compose, net, is_ext) + net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name + net_args.append(f"--network={net_name}") else: net_args.append("--network=bridge") + ipv4 = None ipv6 = None if multiple_nets: From fb0bbd6fe15dafb1e3f3d61878b23be789e812d7 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:31 +0200 Subject: [PATCH 10/15] Cleanup network alias processing Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 5664ff99..d156ca8b 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -936,13 +936,13 @@ def get_net_args_from_networks(compose, cnt): mac_address = cnt.get("mac_address") service_name = cnt["service_name"] - aliases = [service_name] + aliases_on_container = [service_name] # NOTE: from podman manpage: # NOTE: A container will only have access to aliases on the first network # that it joins. This is a limitation that will be removed in a later # release. - if cnt.get("_aliases"): - aliases.extend(cnt.get("_aliases")) + aliases_on_container.extend(cnt.get("_aliases", [])) + aliases_on_net = [] # TODO: add support for per-interface aliases # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases @@ -983,7 +983,7 @@ def get_net_args_from_networks(compose, cnt): ipv6 = net_config_.get("ipv6_address") # custom extension; not supported by docker-compose v3 mac = net_config_.get("x-podman.mac_address") - aliases.extend(norm_as_list(net_config_.get("aliases"))) + aliases_on_net = norm_as_list(net_config_.get("aliases", [])) # if a mac_address was specified on the container level, apply it to the first network # This works for Python > 3.6, because dict insert ordering is preserved, so we are @@ -1023,7 +1023,7 @@ def get_net_args_from_networks(compose, cnt): net_config = list(multiple_nets.values())[0] ipv4 = net_config.get("ipv4_address") ipv6 = net_config.get("ipv6_address") - aliases.extend(norm_as_list(net_config.get("aliases"))) + aliases_on_net = norm_as_list(net_config.get("aliases")) if ipv4: net_args.append(f"--ip={ipv4}") if ipv6: @@ -1031,8 +1031,8 @@ def get_net_args_from_networks(compose, cnt): if mac_address: net_args.append(f"--mac-address={mac_address}") - for alias in aliases: - net_args.extend([f"--network-alias={alias}"]) + net_args.extend([f"--network-alias={alias}" for alias in aliases_on_container]) + net_args.extend([f"--network-alias={alias}" for alias in aliases_on_net]) return net_args From aa8c6fd59837ba5dc3a5083113cd8fdf8959a501 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:32 +0200 Subject: [PATCH 11/15] Simplify network handling in get_net_args_from_networks() Note that multiple_nets is now guaranteed to be not empty in processing stage. Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index d156ca8b..6e5531f1 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -951,6 +951,18 @@ def get_net_args_from_networks(compose, cnt): # always sets a network-alias to the container name, is currently doesn't make sense to # implement this. multiple_nets = cnt.get("networks", {}) + if not multiple_nets: + if not compose.default_net: + # The bridge mode in podman is using the `podman` network. + # It seems weird, but we should keep this behavior to avoid + # breaking changes. + net_args.append("--network=bridge") + if mac_address: + net_args.append(f"--mac-address={mac_address}") + net_args.extend([f"--network-alias={alias}" for alias in aliases_on_container]) + return net_args + + multiple_nets = {compose.default_net: {}} # networks can be specified as a dict with config per network or as a plain list without # config. Support both cases by converting the plain list to a dict with empty config. @@ -971,7 +983,7 @@ def get_net_args_from_networks(compose, cnt): "is not supported" ) - if multiple_nets and len(multiple_nets) > 1: + if len(multiple_nets) > 1: for net_, net_config_ in multiple_nets.items(): net_desc = compose.networks[net_] or {} is_ext = net_desc.get("external") @@ -1006,24 +1018,20 @@ def get_net_args_from_networks(compose, cnt): else: net_args.append(f"--network={net_name}") else: - if multiple_nets or compose.default_net: - net = list(multiple_nets.keys())[0] if multiple_nets else compose.default_net - net_desc = compose.networks[net] or {} - is_ext = net_desc.get("external") - ext_desc = is_ext if isinstance(is_ext, str) else {} - default_net_name = default_network_name_for_project(compose, net, is_ext) - net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name - net_args.append(f"--network={net_name}") - else: - net_args.append("--network=bridge") + net = list(multiple_nets.keys())[0] + net_config = list(multiple_nets.values())[0] + + net_desc = compose.networks[net] or {} + is_ext = net_desc.get("external") + ext_desc = is_ext if isinstance(is_ext, str) else {} + default_net_name = default_network_name_for_project(compose, net, is_ext) + net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name + net_args.append(f"--network={net_name}") + + ipv4 = net_config.get("ipv4_address") + ipv6 = net_config.get("ipv6_address") + aliases_on_net = norm_as_list(net_config.get("aliases")) - ipv4 = None - ipv6 = None - if multiple_nets: - net_config = list(multiple_nets.values())[0] - ipv4 = net_config.get("ipv4_address") - ipv6 = net_config.get("ipv6_address") - aliases_on_net = norm_as_list(net_config.get("aliases")) if ipv4: net_args.append(f"--ip={ipv4}") if ipv6: From 4a232f5e32a2e119d78f34a5aa5ceb19777440dc Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:33 +0200 Subject: [PATCH 12/15] Merge single and multi network handling in get_net_args_from_networks() The behavior has not changed, single-network has a special case. Modified-by: Povilas Kanapickas Signed-off-by: Songmin Li --- podman_compose.py | 71 +++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 6e5531f1..025135a0 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -983,28 +983,36 @@ def get_net_args_from_networks(compose, cnt): "is not supported" ) - if len(multiple_nets) > 1: - for net_, net_config_ in multiple_nets.items(): - net_desc = compose.networks[net_] or {} - is_ext = net_desc.get("external") - ext_desc = is_ext if isinstance(is_ext, str) else {} - default_net_name = default_network_name_for_project(compose, net_, is_ext) - net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name - - ipv4 = net_config_.get("ipv4_address") - ipv6 = net_config_.get("ipv6_address") - # custom extension; not supported by docker-compose v3 - mac = net_config_.get("x-podman.mac_address") - aliases_on_net = norm_as_list(net_config_.get("aliases", [])) - - # if a mac_address was specified on the container level, apply it to the first network - # This works for Python > 3.6, because dict insert ordering is preserved, so we are - # sure that the first network we encounter here is also the first one specified by - # the user - if mac is None and mac_address is not None: - mac = mac_address - mac_address = None + for net_, net_config_ in multiple_nets.items(): + net_desc = compose.networks[net_] or {} + is_ext = net_desc.get("external") + ext_desc = is_ext if isinstance(is_ext, str) else {} + default_net_name = default_network_name_for_project(compose, net_, is_ext) + net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name + ipv4 = net_config_.get("ipv4_address") + ipv6 = net_config_.get("ipv6_address") + # custom extension; not supported by docker-compose v3 + mac = net_config_.get("x-podman.mac_address") + aliases_on_net = norm_as_list(net_config_.get("aliases", [])) + + # if a mac_address was specified on the container level, apply it to the first network + # This works for Python > 3.6, because dict insert ordering is preserved, so we are + # sure that the first network we encounter here is also the first one specified by + # the user + if mac is None and mac_address is not None: + mac = mac_address + mac_address = None + + if len(multiple_nets) == 1: + net_args.append(f"--network={net_name}") + if ipv4: + net_args.append(f"--ip={ipv4}") + if ipv6: + net_args.append(f"--ip6={ipv6}") + if mac: + net_args.append(f"--mac-address={mac}") + else: net_options = [] if ipv4: net_options.append(f"ip={ipv4}") @@ -1017,27 +1025,6 @@ def get_net_args_from_networks(compose, cnt): net_args.append(f"--network={net_name}:" + ",".join(net_options)) else: net_args.append(f"--network={net_name}") - else: - net = list(multiple_nets.keys())[0] - net_config = list(multiple_nets.values())[0] - - net_desc = compose.networks[net] or {} - is_ext = net_desc.get("external") - ext_desc = is_ext if isinstance(is_ext, str) else {} - default_net_name = default_network_name_for_project(compose, net, is_ext) - net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name - net_args.append(f"--network={net_name}") - - ipv4 = net_config.get("ipv4_address") - ipv6 = net_config.get("ipv6_address") - aliases_on_net = norm_as_list(net_config.get("aliases")) - - if ipv4: - net_args.append(f"--ip={ipv4}") - if ipv6: - net_args.append(f"--ip6={ipv6}") - if mac_address: - net_args.append(f"--mac-address={mac_address}") net_args.extend([f"--network-alias={alias}" for alias in aliases_on_container]) net_args.extend([f"--network-alias={alias}" for alias in aliases_on_net]) From 978a1381bcab924af191e21450cdbab52338cda7 Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:34 +0200 Subject: [PATCH 13/15] Support network scoped service aliases Signed-off-by: Songmin Li --- newsfragments/network-scoped-aliases.feature | 1 + podman_compose.py | 77 +++++------- tests/unit/test_container_to_args.py | 66 ++++------- tests/unit/test_container_to_args_secrets.py | 24 ++-- tests/unit/test_get_net_args.py | 118 +++++++++++-------- 5 files changed, 132 insertions(+), 154 deletions(-) create mode 100644 newsfragments/network-scoped-aliases.feature diff --git a/newsfragments/network-scoped-aliases.feature b/newsfragments/network-scoped-aliases.feature new file mode 100644 index 00000000..49058fe0 --- /dev/null +++ b/newsfragments/network-scoped-aliases.feature @@ -0,0 +1 @@ +Support network scoped service aliases. diff --git a/podman_compose.py b/podman_compose.py index 025135a0..cbf0f3f2 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -857,6 +857,7 @@ async def assert_cnt_nets(compose, cnt): net = cnt.get("network_mode") if net and not net.startswith("bridge"): return + cnt_nets = cnt.get("networks") if cnt_nets and isinstance(cnt_nets, dict): cnt_nets = list(cnt_nets.keys()) @@ -910,12 +911,13 @@ def get_net_args_from_network_mode(compose, cnt): aliases_on_container = [service_name] if cnt.get("_aliases"): aliases_on_container.extend(cnt.get("_aliases")) - net_args.append("--network=bridge") + net_options = [f"alias={alias}" for alias in aliases_on_container] mac_address = cnt.get("mac_address") if mac_address: - net_args.append(f"--mac-address={mac_address}") - for alias in aliases_on_container: - net_args.extend([f"--network-alias={alias}"]) + net_options.append(f"mac={mac_address}") + + net = f"{net}," if ":" in net else f"{net}:" + net_args.append(f"--network={net}{','.join(net_options)}") else: log.fatal("unknown network_mode [%s]", net) sys.exit(1) @@ -937,29 +939,18 @@ def get_net_args_from_networks(compose, cnt): service_name = cnt["service_name"] aliases_on_container = [service_name] - # NOTE: from podman manpage: - # NOTE: A container will only have access to aliases on the first network - # that it joins. This is a limitation that will be removed in a later - # release. aliases_on_container.extend(cnt.get("_aliases", [])) - aliases_on_net = [] - - # TODO: add support for per-interface aliases - # See https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases - # Even though podman accepts network-specific aliases (e.g., --network=bridge:alias=foo, - # podman currently ignores this if a per-container network-alias is set; as pdoman-compose - # always sets a network-alias to the container name, is currently doesn't make sense to - # implement this. + multiple_nets = cnt.get("networks", {}) if not multiple_nets: if not compose.default_net: # The bridge mode in podman is using the `podman` network. # It seems weird, but we should keep this behavior to avoid # breaking changes. - net_args.append("--network=bridge") + net_options = [f"alias={alias}" for alias in aliases_on_container] if mac_address: - net_args.append(f"--mac-address={mac_address}") - net_args.extend([f"--network-alias={alias}" for alias in aliases_on_container]) + net_options.append(f"mac={mac_address}") + net_args.append(f"--network=bridge:{','.join(net_options)}") return net_args multiple_nets = {compose.default_net: {}} @@ -984,7 +975,7 @@ def get_net_args_from_networks(compose, cnt): ) for net_, net_config_ in multiple_nets.items(): - net_desc = compose.networks[net_] or {} + net_desc = compose.networks.get(net_) or {} is_ext = net_desc.get("external") ext_desc = is_ext if isinstance(is_ext, str) else {} default_net_name = default_network_name_for_project(compose, net_, is_ext) @@ -992,9 +983,9 @@ def get_net_args_from_networks(compose, cnt): ipv4 = net_config_.get("ipv4_address") ipv6 = net_config_.get("ipv6_address") - # custom extension; not supported by docker-compose v3 + mac = net_config_.get("x-podman.mac_address") - aliases_on_net = norm_as_list(net_config_.get("aliases", [])) + aliases_on_net = net_config_.get("aliases") # if a mac_address was specified on the container level, apply it to the first network # This works for Python > 3.6, because dict insert ordering is preserved, so we are @@ -1004,30 +995,24 @@ def get_net_args_from_networks(compose, cnt): mac = mac_address mac_address = None - if len(multiple_nets) == 1: - net_args.append(f"--network={net_name}") - if ipv4: - net_args.append(f"--ip={ipv4}") - if ipv6: - net_args.append(f"--ip6={ipv6}") - if mac: - net_args.append(f"--mac-address={mac}") + net_options = [] + if ipv4: + net_options.append(f"ip={ipv4}") + if ipv6: + net_options.append(f"ip6={ipv6}") + if mac: + net_options.append(f"mac={mac}") + + # Container level service aliases + net_options.extend([f"alias={alias}" for alias in aliases_on_container]) + # network level service aliases + if aliases_on_net: + net_options.extend([f"alias={alias}" for alias in aliases_on_net]) + + if net_options: + net_args.append(f"--network={net_name}:" + ",".join(net_options)) else: - net_options = [] - if ipv4: - net_options.append(f"ip={ipv4}") - if ipv6: - net_options.append(f"ip={ipv6}") - if mac: - net_options.append(f"mac={mac}") - - if net_options: - net_args.append(f"--network={net_name}:" + ",".join(net_options)) - else: - net_args.append(f"--network={net_name}") - - net_args.extend([f"--network-alias={alias}" for alias in aliases_on_container]) - net_args.extend([f"--network-alias={alias}" for alias in aliases_on_net]) + net_args.append(f"--network={net_name}") return net_args @@ -3487,7 +3472,7 @@ def compose_logs_parse(parser): parser.add_argument("-t", "--timestamps", action="store_true", help="Show timestamps.") parser.add_argument( "--tail", - help="Number of lines to show from the end of the logs for each " "container.", + help="Number of lines to show from the end of the logs for each container.", type=str, default="all", ) diff --git a/tests/unit/test_container_to_args.py b/tests/unit/test_container_to_args.py index b556b237..beb21cad 100644 --- a/tests/unit/test_container_to_args.py +++ b/tests/unit/test_container_to_args.py @@ -51,8 +51,7 @@ async def test_minimal(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -69,8 +68,7 @@ async def test_runtime(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--runtime", "runsc", "busybox", @@ -92,8 +90,7 @@ async def test_sysctl_list(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--sysctl", "net.core.somaxconn=1024", "--sysctl", @@ -117,8 +114,7 @@ async def test_sysctl_map(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--sysctl", "net.core.somaxconn=1024", "--sysctl", @@ -149,8 +145,7 @@ async def test_pid(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--pid", "host", "busybox", @@ -170,8 +165,7 @@ async def test_http_proxy(self): "--name=project_name_service_name1", "-d", "--http-proxy=false", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -197,8 +191,7 @@ async def test_uidmaps_extension(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", '--uidmap', '1000:1000:1', '--uidmap', @@ -219,8 +212,7 @@ async def test_gidmaps_extension(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", '--gidmap', '1000:1000:1', '--gidmap', @@ -242,8 +234,7 @@ async def test_rootfs_extension(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--rootfs", "/path/to/rootfs", ], @@ -261,8 +252,7 @@ async def test_no_hosts_extension(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--no-hosts", "busybox", ], @@ -287,8 +277,7 @@ async def test_env_file_str(self): "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -321,8 +310,7 @@ async def test_env_file_str_array_one_path(self): "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -351,8 +339,7 @@ async def test_env_file_str_array_two_paths(self): "ZZVAR1=podman-rocks-223", "-e", "ZZVAR2=podman-rocks-224", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -376,8 +363,7 @@ async def test_env_file_obj_required(self): "ZZVAR2=podman-rocks-124", "-e", "ZZVAR3=podman-rocks-125", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -403,8 +389,7 @@ async def test_env_file_obj_optional(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -428,8 +413,7 @@ async def test_gpu_count_all(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--device", "nvidia.com/gpu=all", "--security-opt=label=disable", @@ -463,8 +447,7 @@ async def test_gpu_count_specific(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--device", "nvidia.com/gpu=0", "--device", @@ -500,8 +483,7 @@ async def test_gpu_device_ids_all(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--device", "nvidia.com/gpu=all", "--security-opt=label=disable", @@ -535,8 +517,7 @@ async def test_gpu_device_ids_specific(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--device", "nvidia.com/gpu=1", "--device", @@ -581,8 +562,7 @@ async def test_selinux_volume(self, prefer_volume, selinux_type, expected_additi "--name=project_name_service_name1", "-d", *expected_additional_args, - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) @@ -606,8 +586,7 @@ async def test_network_default_name(self, name, is_compat, project_name, expecte [ "--name=project_name_service_name1", "-d", - f"--network={expected_network_name}", - "--network-alias=service_name", + f"--network={expected_network_name}:alias=service_name", "busybox", ], ) @@ -629,8 +608,7 @@ async def test_device(self): "/dev/ttyS0", "--device-cgroup-rule", "c 100:200 rwm", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "busybox", ], ) diff --git a/tests/unit/test_container_to_args_secrets.py b/tests/unit/test_container_to_args_secrets.py index 86540a73..084839b0 100644 --- a/tests/unit/test_container_to_args_secrets.py +++ b/tests/unit/test_container_to_args_secrets.py @@ -36,8 +36,7 @@ async def test_pass_secret_as_env_variable(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret,type=env,target=ENV_SECRET", "busybox", @@ -68,8 +67,7 @@ async def test_secret_as_env_external_true_has_no_name(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret,type=env,target=ENV_SECRET", "busybox", @@ -152,8 +150,7 @@ async def test_secret_string_no_external_name_in_declared_secrets(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret_name", "busybox", @@ -191,8 +188,7 @@ async def test_secret_string_options_external_name_in_declared_secrets(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret_name,uid=103,gid=103,mode=400", "busybox", @@ -257,8 +253,7 @@ async def test_secret_target_does_not_match_secret_name_secret_type_env(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret_name,type=env,target=does_not_equal_secret_name", "busybox", @@ -289,8 +284,7 @@ async def test_secret_target_matches_secret_name_secret_type_not_env(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--secret", "my_secret_name,type=does_not_equal_env", "busybox", @@ -361,8 +355,7 @@ async def test_file_secret( [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--volume", expected_volume_ref, "busybox", @@ -398,8 +391,7 @@ async def test_file_secret_unused_params_warning(self): [ "--name=project_name_service_name1", "-d", - "--network=bridge", - "--network-alias=service_name", + "--network=bridge:alias=service_name", "--volume", repo_root() + "/test_dirname/my_secret:/run/secrets/unused_params_warning:ro,rprivate,rbind", diff --git a/tests/unit/test_get_net_args.py b/tests/unit/test_get_net_args.py index aec07423..3b544135 100644 --- a/tests/unit/test_get_net_args.py +++ b/tests/unit/test_get_net_args.py @@ -24,6 +24,9 @@ def get_networked_compose(num_networks=1): "enable_ipv6": True, } + if num_networks == 1: + compose.default_net = "net0" + return compose @@ -41,32 +44,36 @@ def test_minimal(self): container = get_minimal_container() expected_args = [ - "--network=bridge", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) - def test_one_net(self): + def test_default_net_is_None(self): compose = get_networked_compose() container = get_minimal_container() - container["networks"] = {"net0": {}} + + mac_address = "11:22:33:44:55:66" + container["mac_address"] = mac_address + + compose.default_net = None expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--network-alias={SERVICE_NAME}", + f"--network=bridge:alias={SERVICE_NAME},mac={mac_address}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) - def test_network_mode_and_networks_unsupported(self): + def test_one_net(self): compose = get_networked_compose() container = get_minimal_container() container["networks"] = {"net0": {}} - container["network_mode"] = "none" - with self.assertRaises(ValueError): - get_net_args(compose, container) + expected_args = [ + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME}", + ] + args = get_net_args(compose, container) + self.assertListEqual(expected_args, args) def test_alias(self): compose = get_networked_compose() @@ -75,10 +82,18 @@ def test_alias(self): container["_aliases"] = ["alias1", "alias2"] expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--network-alias={SERVICE_NAME}", - "--network-alias=alias1", - "--network-alias=alias2", + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME},alias=alias1,alias=alias2", + ] + args = get_net_args(compose, container) + self.assertListEqual(expected_args, args) + + def test_aliases_on_network_scope(self): + compose = get_networked_compose() + container = get_minimal_container() + container["networks"] = {"net0": {"aliases": ["alias1"]}} + + expected_args = [ + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME},alias=alias1", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -90,9 +105,7 @@ def test_one_ipv4(self): container["networks"] = {"net0": {"ipv4_address": ip}} expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--ip={ip}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:ip={ip},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertEqual(expected_args, args) @@ -104,9 +117,7 @@ def test_one_ipv6(self): container["networks"] = {"net0": {"ipv6_address": ipv6_address}} expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--ip6={ipv6_address}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:ip6={ipv6_address},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -119,9 +130,7 @@ def test_one_mac(self): container["mac_address"] = mac expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--mac-address={mac}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:mac={mac},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -134,9 +143,20 @@ def test_one_mac_two_nets(self): container["mac_address"] = mac expected_args = [ - f"--network={PROJECT_NAME}_net0:mac={mac}", - f"--network={PROJECT_NAME}_net1", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:mac={mac},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:alias={SERVICE_NAME}", + ] + args = get_net_args(compose, container) + self.assertListEqual(expected_args, args) + + def test_mac_on_network(self): + mac = "00:11:22:33:44:55" + compose = get_networked_compose() + container = get_minimal_container() + container["networks"] = {"net0": {"x-podman.mac_address": mac}} + + expected_args = [ + f"--network={PROJECT_NAME}_net0:mac={mac},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -147,9 +167,8 @@ def test_two_nets_as_dict(self): container["networks"] = {"net0": {}, "net1": {}} expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--network={PROJECT_NAME}_net1", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -160,9 +179,8 @@ def test_two_nets_as_list(self): container["networks"] = ["net0", "net1"] expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--network={PROJECT_NAME}_net1", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -175,9 +193,8 @@ def test_two_ipv4(self): container["networks"] = {"net0": {"ipv4_address": ip0}, "net1": {"ipv4_address": ip1}} expected_args = [ - f"--network={PROJECT_NAME}_net0:ip={ip0}", - f"--network={PROJECT_NAME}_net1:ip={ip1}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:ip={ip0},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:ip={ip1},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -190,9 +207,8 @@ def test_two_ipv6(self): container["networks"] = {"net0": {"ipv6_address": ip0}, "net1": {"ipv6_address": ip1}} expected_args = [ - f"--network={PROJECT_NAME}_net0:ip={ip0}", - f"--network={PROJECT_NAME}_net1:ip={ip1}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:ip6={ip0},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:ip6={ip1},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -209,9 +225,8 @@ def test_two_mac(self): } expected_args = [ - f"--network={PROJECT_NAME}_net0:mac={mac0}", - f"--network={PROJECT_NAME}_net1:mac={mac1}", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:mac={mac0},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:mac={mac1},alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @@ -233,7 +248,7 @@ def test_mixed_mac(self): container["mac_address"] = mac_1 expected_exception = ( - r"specifying mac_address on both container and network level " r"is not supported" + r"specifying mac_address on both container and network level is not supported" ) self.assertRaisesRegex(RuntimeError, expected_exception, get_net_args, compose, container) @@ -254,17 +269,20 @@ def test_mixed_config(self): container["mac_address"] = mac expected_args = [ - f"--network={PROJECT_NAME}_net0:ip={ip4_0},ip={ip6_0},mac={mac}", - f"--network={PROJECT_NAME}_net1:ip={ip4_1}", - f"--network={PROJECT_NAME}_net2:ip={ip6_2}", - f"--network={PROJECT_NAME}_net3", - f"--network-alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net0:ip={ip4_0},ip6={ip6_0},mac={mac},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net1:ip={ip4_1},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net2:ip6={ip6_2},alias={SERVICE_NAME}", + f"--network={PROJECT_NAME}_net3:alias={SERVICE_NAME}", ] args = get_net_args(compose, container) self.assertListEqual(expected_args, args) @parameterized.expand([ - ("bridge", ["--network=bridge", f"--network-alias={SERVICE_NAME}"]), + ("bridge", [f"--network=bridge:alias={SERVICE_NAME},mac=11:22:33:44:55:66"]), + ( + "bridge:ip=10.88.0.3", + [f"--network=bridge:ip=10.88.0.3,alias={SERVICE_NAME},mac=11:22:33:44:55:66"], + ), ("host", ["--network=host"]), ("none", ["--network=none"]), ("slirp4netns", ["--network=slirp4netns"]), @@ -280,6 +298,10 @@ def test_network_modes(self, network_mode, expected_args): container = get_minimal_container() container["network_mode"] = network_mode + mac_address = "11:22:33:44:55:66" + container["network_mode"] = network_mode + container["mac_address"] = mac_address + args = get_net_args(compose, container) self.assertListEqual(expected_args, args) From 55642247e3a1b5d3e0f0f8cb498324b2484f852d Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Wed, 18 Dec 2024 19:59:35 +0200 Subject: [PATCH 14/15] Add integration test for network scoped aliases Signed-off-by: Songmin Li --- podman_compose.py | 4 +- .../docker-compose.yaml | 33 ++++++++ ...t_podman_compose_network_scoped_aliases.py | 84 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 tests/integration/network_scoped_aliases/docker-compose.yaml create mode 100644 tests/integration/test_podman_compose_network_scoped_aliases.py diff --git a/podman_compose.py b/podman_compose.py index cbf0f3f2..25ccfdba 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -983,9 +983,9 @@ def get_net_args_from_networks(compose, cnt): ipv4 = net_config_.get("ipv4_address") ipv6 = net_config_.get("ipv6_address") - + # custom extension; not supported by docker-compose v3 mac = net_config_.get("x-podman.mac_address") - aliases_on_net = net_config_.get("aliases") + aliases_on_net = norm_as_list(net_config_.get("aliases", [])) # if a mac_address was specified on the container level, apply it to the first network # This works for Python > 3.6, because dict insert ordering is preserved, so we are diff --git a/tests/integration/network_scoped_aliases/docker-compose.yaml b/tests/integration/network_scoped_aliases/docker-compose.yaml new file mode 100644 index 00000000..1a5a630a --- /dev/null +++ b/tests/integration/network_scoped_aliases/docker-compose.yaml @@ -0,0 +1,33 @@ +--- +networks: + net0: + ipam: + config: + - subnet: "172.19.3.0/24" + net1: + ipam: + config: + - subnet: "172.19.4.0/24" +services: + web1: + image: busybox + command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] + networks: + net0: + ipv4_address: "172.19.3.11" + aliases: + - secure-web + net1: + ipv4_address: "172.19.4.11" + aliases: + - insecure-web + utils-net0: + image: busybox + command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] + networks: + - net0 + utils-net1: + image: busybox + command: ["/bin/busybox", "httpd", "-f", "-h", "/tmp", "-p", "8001"] + networks: + - net1 diff --git a/tests/integration/test_podman_compose_network_scoped_aliases.py b/tests/integration/test_podman_compose_network_scoped_aliases.py new file mode 100644 index 00000000..8e478fad --- /dev/null +++ b/tests/integration/test_podman_compose_network_scoped_aliases.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: GPL-2.0 + +# pylint: disable=redefined-outer-name +import os +import unittest + +from tests.integration.test_podman_compose import podman_compose_path +from tests.integration.test_podman_compose import test_path +from tests.integration.test_utils import RunSubprocessMixin + + +class TestPodmanComposeNetworkScopedAliases(RunSubprocessMixin, unittest.TestCase): + @staticmethod + def compose_file(): + """Returns the path to the compose file used for this test module""" + return os.path.join(test_path(), "network_scoped_aliases", "docker-compose.yaml") + + def test_network_scoped_aliases(self): + try: + self.up() + self.verify() + finally: + self.down() + + def up(self): + up_cmd = [ + "coverage", + "run", + podman_compose_path(), + "-f", + self.compose_file(), + "up", + "-d", + "--force-recreate", + ] + + self.run_subprocess_assert_returncode(up_cmd) + + def down(self): + down_cmd = [ + "coverage", + "run", + podman_compose_path(), + "-f", + self.compose_file(), + "kill", + "-a", + ] + self.run_subprocess(down_cmd) + + def verify(self): + expected_results = [ + ("utils-net0", "web1", ["172.19.3.11"]), + ("utils-net0", "secure-web", ["172.19.3.11"]), + ("utils-net0", "insecure-web", []), + ("utils-net1", "web1", ["172.19.4.11"]), + ("utils-net1", "secure-web", []), + ("utils-net1", "insecure-web", ["172.19.4.11"]), + ] + + for utils, service, expected_result in expected_results: + cmd = [ + podman_compose_path(), + "-f", + self.compose_file(), + "exec", + utils, + "nslookup", + service, + ] + out, _, _ = self.run_subprocess(cmd) + addresses = self.parse_dnslookup(out.decode()) + self.assertEqual(addresses, expected_result) + + def parse_dnslookup(self, output): + lines = output.splitlines() + addresses = [] + for line in lines: + if line.startswith("Address"): + addr = line.split(":", 1)[1].strip() + if ":" not in addr: + addresses.append(addr) + + return list(sorted(set(addresses))) From 346f7a57f0349d3c074bb0b99d99c66aeffad48c Mon Sep 17 00:00:00 2001 From: Songmin Li Date: Thu, 19 Dec 2024 10:44:29 +0800 Subject: [PATCH 15/15] Ignore creating networks on network_mode=bridge. When using bridge mode, podman-compose will use the default `podman` network. We do not need to create this network, so just return as other network_mode. Signed-off-by: Songmin Li --- podman_compose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/podman_compose.py b/podman_compose.py index 25ccfdba..81132e17 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -855,7 +855,7 @@ async def assert_cnt_nets(compose, cnt): create missing networks """ net = cnt.get("network_mode") - if net and not net.startswith("bridge"): + if net: return cnt_nets = cnt.get("networks")