Skip to content
Merged
13 changes: 13 additions & 0 deletions python_on_whales/components/compose/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ class ComposeServiceVolume(BaseModel):
type: Optional[str] = None


class ComposeServiceNetwork(BaseModel):
aliases: Optional[List[str]] = None
ipv4_address: Optional[str] = None
ipv6_address: Optional[str] = None
link_local_ips: Optional[List[str]] = None
mac_address: Optional[str] = None
driver_opts: Optional[Dict[str, Any]] = None
gw_priority: Optional[int] = None
priority: Optional[int] = None


class ComposeConfigService(BaseModel):
deploy: Optional[ServiceDeployConfig] = None
blkio_config: Optional[Any] = None
Expand All @@ -77,10 +88,12 @@ class ComposeConfigService(BaseModel):
environment: Union[Dict[str, Union[str, int, None]], List[str], None] = None
expose: Annotated[Union[List[int], List[str]], Field(default_factory=list)]
entrypoint: Union[List[str], str, None] = None
hostname: Optional[str] = None
image: Optional[str] = None
labels: Annotated[Optional[Dict[str, str]], Field(default_factory=dict)]
ports: Optional[List[ComposeServicePort]] = None
volumes: Optional[List[ComposeServiceVolume]] = None
networks: Optional[Dict[str, Optional[ComposeServiceNetwork]]] = None


class ComposeConfigNetwork(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion scripts/download-docker-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -e

mkdir -p ~/.docker/cli-plugins/
wget -q https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-linux-x86_64 -O ~/.docker/cli-plugins/docker-compose
wget -q https://github.com/docker/compose/releases/download/v2.35.0/docker-compose-linux-x86_64 -O ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
wget -q https://github.com/docker/buildx/releases/download/v0.13.0/buildx-v0.13.0.linux-amd64 -O ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
99 changes: 99 additions & 0 deletions tests/python_on_whales/components/complex-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
version: "3.7"

services:
my_service:
build:
context: my_service_build
image: some_random_image
hostname: my_service_hostname
command: ping -c 2 www.google.com
ports:
- "5000:5000"
volumes:
- /tmp:/tmp
- dodo:/dodo
environment:
- DATADOG_HOST=something
networks:
- some-network
- other-network
deploy:
placement:
constraints:
- node.labels.hello-world == yes
resources:
reservations:
cpus: '1'
memory: 20M
limits:
cpus: '2'
memory: 40M
replicas: 4

network_service:
image: some_random_image
hostname: network_service_hostname
command: ping -c 2 www.google.com
networks:
app_net_1:
aliases:
- app-1-service
- app-1-server
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10
priority: 1000
app_net_2:
aliases:
- app-2-service
driver_opts:
foo: "bar"
baz: 1
gw_priority: 1
app_net_3:
link_local_ips:
- 169.254.22.11
- 169.254.22.13
mac_address: "02:42:ac:11:00:02"
priority: 100
app_net_4:
app_net_5:

networks:
some-network:
driver: bridge
attachable: true
labels:
type: "frontend"
purpose: "web-traffic"
other-network:
driver: bridge
internal: true
driver_opts:
com.docker.network.bridge.name: "other-bridge"
app_net_1:
driver: bridge
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"
- subnet: "2001:3984:3989::/64"
app_net_2:
driver: bridge
driver_opts:
com.docker.network.bridge.enable_icc: "true"
com.docker.network.bridge.host_binding_ipv4: "0.0.0.0"
app_net_3:
driver: bridge
enable_ipv6: true
labels:
env: "development"
network_type: "link-local"
app_net_4:
driver: bridge
external: false
app_net_5:
driver: bridge
attachable: false

volumes:
dodo: {}
32 changes: 0 additions & 32 deletions tests/python_on_whales/components/complexe-compose.yml

This file was deleted.

96 changes: 94 additions & 2 deletions tests/python_on_whales/components/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,11 +660,12 @@ def test_entrypoint_loaded_in_config():
def test_config_complexe_compose():
"""Checking that the pydantic model does its job"""
compose_file = (
PROJECT_ROOT / "tests/python_on_whales/components/complexe-compose.yml"
PROJECT_ROOT / "tests/python_on_whales/components/complex-compose.yml"
)
docker = DockerClient(compose_files=[compose_file], compose_compatibility=True)
config = docker.compose.config()

# Test my_service
assert config.services["my_service"].build.context == (
PROJECT_ROOT / "tests/python_on_whales/components/my_service_build"
)
Expand All @@ -675,6 +676,7 @@ def test_config_complexe_compose():
"2",
"www.google.com",
]
assert config.services["my_service"].hostname == "my_service_hostname"

assert config.services["my_service"].ports[0].published == 5000
assert config.services["my_service"].ports[0].target == 5000
Expand All @@ -685,6 +687,13 @@ def test_config_complexe_compose():
assert config.services["my_service"].volumes[1].target == "/dodo"

assert config.services["my_service"].environment == {"DATADOG_HOST": "something"}
# Test networks for my_service (Docker Compose normalizes list to dict)
assert isinstance(config.services["my_service"].networks, dict)
assert "some-network" in config.services["my_service"].networks
assert "other-network" in config.services["my_service"].networks
assert config.services["my_service"].networks["some-network"] is None
assert config.services["my_service"].networks["other-network"] is None

assert config.services["my_service"].deploy.placement.constraints == [
"node.labels.hello-world == yes"
]
Expand All @@ -698,12 +707,95 @@ def test_config_complexe_compose():

assert config.services["my_service"].deploy.replicas == 4

# Test network_service with complex network configurations
assert config.services["network_service"].image == "some_random_image"
assert config.services["network_service"].command == [
"ping",
"-c",
"2",
"www.google.com",
]
assert config.services["network_service"].hostname == "network_service_hostname"

# Test network configurations for network_service
app_net_1_config = config.services["network_service"].networks["app_net_1"]
assert app_net_1_config.aliases == ["app-1-service", "app-1-server"]
assert app_net_1_config.ipv4_address == "172.16.238.10"
assert app_net_1_config.ipv6_address == "2001:3984:3989::10"
assert app_net_1_config.priority == 1000

app_net_2_config = config.services["network_service"].networks["app_net_2"]
assert app_net_2_config.aliases == ["app-2-service"]
assert app_net_2_config.driver_opts == {
"foo": "bar",
"baz": "1",
} # Docker normalizes to strings
assert app_net_2_config.gw_priority == 1

app_net_3_config = config.services["network_service"].networks["app_net_3"]
assert app_net_3_config.link_local_ips == ["169.254.22.11", "169.254.22.13"]
assert app_net_3_config.mac_address == "02:42:ac:11:00:02"
assert app_net_3_config.priority == 100

app_net_4_config = config.services["network_service"].networks["app_net_4"]
assert app_net_4_config is None

app_net_5_config = config.services["network_service"].networks["app_net_5"]
assert app_net_5_config is None

# Test network definitions
assert config.networks["some-network"].driver == "bridge"
assert config.networks["some-network"].attachable
assert config.networks["some-network"].labels["type"] == "frontend"

assert config.networks["other-network"].internal
assert config.networks["app_net_1"].ipam["config"][0]["subnet"] == "172.16.238.0/24"
assert config.networks["app_net_3"].enable_ipv6

assert not config.volumes["dodo"].external


def test_compose_down_networks():
compose_file = (
PROJECT_ROOT / "tests/python_on_whales/components/complex-compose.yml"
)
docker = DockerClient(compose_files=[compose_file], compose_compatibility=True)

# Start services to create networks
docker.compose.up(
["my_service", "network_service"],
detach=True,
scales=dict(my_service=1),
build=True,
)

# Check that networks are created
networks = docker.network.list()
network_names = [network.name for network in networks]
assert "components_some-network" in network_names
assert "components_other-network" in network_names
assert "components_app_net_1" in network_names
assert "components_app_net_2" in network_names
assert "components_app_net_3" in network_names
assert "components_app_net_4" in network_names
assert "components_app_net_5" in network_names

# Stop services - Docker Compose automatically removes unused networks
docker.compose.down()

# Networks should be removed when no containers are using them (normal Docker Compose behavior)
networks_after_down = docker.network.list()
network_names_after = [network.name for network in networks_after_down]

# Verify the networks have been cleaned up as expected
assert "components_some-network" not in network_names_after
assert "components_other-network" not in network_names_after
assert "components_app_net_1" not in network_names_after


def test_compose_down_volumes():
compose_file = (
PROJECT_ROOT / "tests/python_on_whales/components/complexe-compose.yml"
PROJECT_ROOT / "tests/python_on_whales/components/complex-compose.yml"
)
docker = DockerClient(compose_files=[compose_file], compose_compatibility=True)
docker.compose.up(
Expand Down
Loading