Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix multiple networks with separately specified ip and mac #867

Merged
merged 10 commits into from
Apr 8, 2024
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
test-compose.yaml
test-compose-?.yaml

# Translations
*.mo
Expand Down
67 changes: 67 additions & 0 deletions docs/Extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Podman specific extensions to the docker-compose format

Podman-compose supports the following extension to the docker-compose format.

## Per-network MAC-addresses

Generic docker-compose files support specification of the MAC address on the container level. If the
container has multiple network interfaces, the specified MAC address is applied to the first
specified network.

Podman-compose in addition supports the specification of MAC addresses on a per-network basis. This
is done by adding a `podman.mac_address` key to the network configuration in the container. The
value of the `podman.mac_address` key is the MAC address to be used for the network interface.

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

Example:

```yaml
---
version: "3"

networks:
net0:
driver: "bridge"
ipam:
config:
- subnet: "192.168.0.0/24"
net1:
driver: "bridge"
ipam:
config:
- subnet: "192.168.1.0/24"

services:
webserver
image: "busybox"
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc", "-p", "8001"]
networks:
net0:
ipv4_address: "192.168.0.10"
podman.mac_address: "02:aa:aa:aa:aa:aa"
net1:
ipv4_address: "192.168.1.10"
podman.mac_address: "02:bb:bb:bb:bb:bb"
```

## Podman-specific network modes

Generic docker-compose supports the following values for `network-mode` for a container:

- `bridge`
- `host`
- `none`
- `service`
- `container`

In addition, podman-compose supports the following podman-specific values for `network-mode`:

- `slirp4netns[:<options>,...]`
- `ns:<options>`
- `pasta[:<options>,...]`
- `private`

The options to the network modes are passed to the `--network` option of the `podman create` command
as-is.
99 changes: 76 additions & 23 deletions podman_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,27 +779,29 @@ async def assert_cnt_nets(compose, cnt):
def get_net_args(compose, cnt):
service_name = cnt["service_name"]
net_args = []
mac_address = cnt.get("mac_address", None)
if mac_address:
net_args.extend(["--mac-address", mac_address])
is_bridge = False
mac_address = cnt.get("mac_address", None)
net = cnt.get("network_mode", None)
if net:
if net == "none":
is_bridge = False
elif net == "host":
net_args.extend(["--network", net])
elif net.startswith("slirp4netns:"):
net_args.extend(["--network", net])
elif net.startswith("ns:"):
net_args.extend(["--network", net])
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.extend(["--network", f"container:{other_cnt}"])
net_args.append(f"--network=container:{other_cnt}")
elif net.startswith("container:"):
other_cnt = net.split(":", 1)[1].strip()
net_args.extend(["--network", f"container:{other_cnt}"])
net_args.append(f"--network=container:{other_cnt}")
elif net.startswith("bridge"):
is_bridge = True
else:
Expand All @@ -811,6 +813,7 @@ def get_net_args(compose, cnt):
default_net = compose.default_net
nets = compose.networks
cnt_nets = cnt.get("networks", None)

aliases = [service_name]
# NOTE: from podman manpage:
# NOTE: A container will only have access to aliases on the first network
Expand Down Expand Up @@ -855,32 +858,82 @@ def get_net_args(compose, cnt):
net_names.append(net_name)
net_names_str = ",".join(net_names)

if ip_assignments > 1:
multiple_nets = cnt.get("networks", None)
multiple_net_names = multiple_nets.keys()
# 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", None)
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("podman.mac_address", None)
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_ in multiple_net_names:
for net_, net_config_ in multiple_nets.items():
net_desc = nets[net_] or {}
is_ext = net_desc.get("external", None)
ext_desc = is_ext if is_dict(is_ext) else {}
default_net_name = net_ if is_ext else f"{proj_name}_{net_}"
net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name

ipv4 = multiple_nets[net_].get("ipv4_address", None)
ipv6 = multiple_nets[net_].get("ipv6_address", None)
if ipv4 is not None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4},ip={ipv6}"])
elif ipv4 is None and ipv6 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv6}"])
elif ipv6 is None and ipv4 is not None:
net_args.extend(["--network", f"{net_name}:ip={ipv4}"])
ipv4 = net_config_.get("ipv4_address", None)
ipv6 = net_config_.get("ipv6_address", None)
# custom extension; not supported by docker-compose v3
mac = net_config_.get("podman.mac_address", None)

# 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:
net_args.extend(["--net", net_names_str, "--network-alias", ",".join(aliases)])
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}"])

return net_args


Expand Down
40 changes: 14 additions & 26 deletions pytests/test_container_to_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from podman_compose import container_to_args


def create_compose_mock():
def create_compose_mock(project_name="test_project_name"):
compose = mock.Mock()
compose.project_name = "test_project_name"
compose.project_name = project_name
compose.dirname = "test_dirname"
compose.container_names_by_service.get = mock.Mock(return_value=None)
compose.prefer_volume_over_mount = False
Expand Down Expand Up @@ -37,10 +37,8 @@ async def test_minimal(self):
[
"--name=project_name_service_name1",
"-d",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"busybox",
],
)
Expand All @@ -57,10 +55,8 @@ async def test_runtime(self):
[
"--name=project_name_service_name1",
"-d",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"--runtime",
"runsc",
"busybox",
Expand All @@ -82,10 +78,8 @@ async def test_sysctl_list(self):
[
"--name=project_name_service_name1",
"-d",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"--sysctl",
"net.core.somaxconn=1024",
"--sysctl",
Expand All @@ -109,10 +103,8 @@ async def test_sysctl_map(self):
[
"--name=project_name_service_name1",
"-d",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"--sysctl",
"net.core.somaxconn=1024",
"--sysctl",
Expand Down Expand Up @@ -143,10 +135,8 @@ async def test_pid(self):
[
"--name=project_name_service_name1",
"-d",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"--pid",
"host",
"busybox",
Expand All @@ -166,10 +156,8 @@ async def test_http_proxy(self):
"--name=project_name_service_name1",
"-d",
"--http-proxy=false",
"--net",
"",
"--network-alias",
"service_name",
"--network=bridge",
"--network-alias=service_name",
"busybox",
],
)
Loading