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..7b4fe12b 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,9 @@ 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") - 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 = [f"alias={alias}" for alias in aliases_on_container] + 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 +935,16 @@ 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") - 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 = [f"alias={alias}" for alias in aliases_on_container] + net_args.append(f"--network=bridge:{'.'.join(net_options)}") return net_args multiple_nets = {compose.default_net: {}} @@ -984,7 +969,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 +977,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 +989,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 +3466,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..7084a30b 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,33 @@ 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": {}} + + compose.default_net = None expected_args = [ - f"--network={PROJECT_NAME}_net0", - f"--network-alias={SERVICE_NAME}", + f"--network=bridge:alias={SERVICE_NAME}", ] 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 +79,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 +102,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 +114,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 +127,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 +140,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 +164,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 +176,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 +190,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 +204,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 +222,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 +245,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 +266,17 @@ 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}"]), + ("bridge:ip=10.88.0.3", [f"--network=bridge:ip=10.88.0.3,alias={SERVICE_NAME}"]), ("host", ["--network=host"]), ("none", ["--network=none"]), ("slirp4netns", ["--network=slirp4netns"]),