Skip to content

Commit

Permalink
Support network scoped service aliases
Browse files Browse the repository at this point in the history
Signed-off-by: Songmin Li <[email protected]>
  • Loading branch information
lisongmin committed Dec 17, 2024
1 parent 376f081 commit a955bc8
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 250 deletions.
8 changes: 7 additions & 1 deletion docs/Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Podman-compose in addition supports the specification of MAC addresses on a per-
is done by adding a `x-podman.mac_address` key to the network configuration in the container. The
value of the `x-podman.mac_address` key is the MAC address to be used for the network interface.

Note that the [compose spec](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#mac_address)
now supports `mac_address` on the network level, so we recommend using
the standard `mac_address` key for setting the MAC address. The
`x-podman.mac_address` is still supported for backwards compatibility.


Specifying a MAC address for the container and for individual networks at the same time is not
supported.

Expand Down Expand Up @@ -69,7 +75,7 @@ services:
x-podman.mac_address: "02:aa:aa:aa:aa:aa"
net1:
ipv4_address: "192.168.1.10"
x-podman.mac_address: "02:bb:bb:bb:bb:bb"
mac_address: "02:bb:bb:bb:bb:bb" # mac_address is supported
```

## Podman-specific network modes
Expand Down
1 change: 1 addition & 0 deletions newsfragments/network-scoped-aliases.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support network scoped service aliases.
268 changes: 121 additions & 147 deletions podman_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,9 @@ 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")
if cnt_nets and isinstance(cnt_nets, dict):
cnt_nets = list(cnt_nets.keys())
Expand All @@ -878,160 +879,133 @@ async def assert_cnt_nets(compose, cnt):


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 and cnt.get("networks"):
# https://github.com/compose-spec/compose-spec/blob/main/05-services.md#network_mode
# When network_mode is set, the networks attribute is not allowed and
# Compose rejects any Compose file containing both attributes.
raise RuntimeError("The network_mode and networks options are mutually exclusive")

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)
return get_net_args_from_network_mode(compose, cnt)

return get_net_args_from_networks(compose, cnt)


def get_net_args_from_network_mode(compose, cnt):
net_args = []
net = cnt.get("network_mode")

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"):
net_args.append(f"--network={net}")
else:
is_bridge = True
cnt_nets = cnt.get("networks")
log.fatal("unknown network_mode [%s]", net)
sys.exit(1)

return net_args


aliases = [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.
ip = None
ip6 = None
ip_assignments = 0
def get_net_args_from_networks(compose, cnt):
net_args = []
mac_address = cnt.get("mac_address")

service_name = cnt["service_name"]
aliases_on_container = [service_name]
if cnt.get("_aliases"):
aliases.extend(cnt.get("_aliases"))
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 {}
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")
if not ip6:
ip6 = net_value.get("ipv6_address")
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 {}
aliases_on_container.extend(cnt.get("_aliases"))

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_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: {}}

# 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("mac_address", 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.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)
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)

# 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")
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")

# 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

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}")
else:
if is_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:
net_args.append(f"--ip6={ip6}")
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}"])
ipv4 = net_config_.get("ipv4_address")
ipv6 = net_config_.get("ipv6_address")

# Note: mac_address is supported by compose spec now, and x-podman.mac_address
# is only for backward compatibility
# https://github.com/compose-spec/compose-spec/blob/main/05-services.md#mac_address
mac = net_config_.get("mac_address", net_config_.get("x-podman.mac_address"))
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
# 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

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_args.append(f"--network={net_name}")

return net_args

Expand Down Expand Up @@ -3481,7 +3455,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",
)
Expand Down
Loading

0 comments on commit a955bc8

Please sign in to comment.