Implement volume and network management methods in DockerComposeManager and enhance service creation with additional parameters

This commit is contained in:
2025-06-18 04:14:51 +02:00
parent be43c468a4
commit f0ee0bcac6
2 changed files with 176 additions and 2 deletions

View File

@ -20,6 +20,52 @@ if TYPE_CHECKING:
class DockerComposeManager:
def add_volume(self, name: str, config: dict[str, Any] | None = None) -> DockerComposeManager:
"""Add a top-level volume definition.
Returns:
DockerComposeManager: self (for chaining)
"""
if "volumes" not in self._data or not isinstance(self._data["volumes"], dict):
self._data["volumes"] = {}
self._data["volumes"][name] = config or {}
self._dirty = True
return self
def remove_volume(self, name: str) -> DockerComposeManager:
"""Remove a top-level volume definition.
Returns:
DockerComposeManager: self (for chaining)
"""
if "volumes" in self._data and name in self._data["volumes"]:
del self._data["volumes"][name]
self._dirty = True
return self
def add_network(self, name: str, config: dict[str, Any] | None = None) -> DockerComposeManager:
"""Add a top-level network definition.
Returns:
DockerComposeManager: self (for chaining)
"""
if "networks" not in self._data or not isinstance(self._data["networks"], dict):
self._data["networks"] = {}
self._data["networks"][name] = config or {}
self._dirty = True
return self
def remove_network(self, name: str) -> DockerComposeManager:
"""Remove a top-level network definition.
Returns:
DockerComposeManager: self (for chaining)
"""
if "networks" in self._data and name in self._data["networks"]:
del self._data["networks"][name]
self._dirty = True
return self
"""A class to create and modify Docker Compose YAML files programmatically.
Supports context manager usage for auto-saving.
@ -47,9 +93,22 @@ class DockerComposeManager:
def create_service(
self,
name: str,
image: str,
image: str = "",
ports: list[str] | None = None,
environment: dict[str, str] | None = None,
volumes: list[str] | None = None,
networks: list[str] | None = None,
command: str | list[str] | None = None,
entrypoint: str | list[str] | None = None,
build: dict[str, Any] | str | None = None,
healthcheck: dict[str, Any] | None = None,
restart: str | None = None,
labels: dict[str, str] | list[str] | None = None,
depends_on: list[str] | None = None,
configs: list[dict[str, Any]] | None = None,
secrets: list[dict[str, Any]] | None = None,
deploy: dict[str, Any] | None = None,
resources: dict[str, Any] | None = None,
**kwargs: object,
) -> DockerComposeManager:
"""Create a new service in the compose file.
@ -59,11 +118,39 @@ class DockerComposeManager:
"""
services = self._data["services"]
service: dict[str, Any] = {"image": image}
service: dict[str, Any] = {}
if image:
service["image"] = image
if ports is not None:
service["ports"] = ports
if environment is not None:
service["environment"] = environment
if volumes is not None:
service["volumes"] = volumes
if networks is not None:
service["networks"] = networks
if command is not None:
service["command"] = command
if entrypoint is not None:
service["entrypoint"] = entrypoint
if build is not None:
service["build"] = build
if healthcheck is not None:
service["healthcheck"] = healthcheck
if restart is not None:
service["restart"] = restart
if labels is not None:
service["labels"] = labels
if depends_on is not None:
service["depends_on"] = depends_on
if configs is not None:
service["configs"] = configs
if secrets is not None:
service["secrets"] = secrets
if deploy is not None:
service["deploy"] = deploy
if resources is not None:
service["resources"] = resources
service.update(kwargs)
services[name] = service
self._dirty = True

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
@ -65,3 +66,89 @@ def test_modify_nonexistent_service(tmp_path: Path) -> None:
manager = DockerComposeManager(str(compose_file))
with pytest.raises(KeyError):
manager.modify_service("notfound", image="nginx:latest")
def test_remove_nonexistent_service(tmp_path: Path) -> None:
compose_file: Path = tmp_path / "docker-compose.yml"
manager = DockerComposeManager(str(compose_file))
# Should not raise
manager.remove_service("notfound").save()
with compose_file.open() as f:
data = yaml.safe_load(f)
assert "services" in data
assert data["services"] == {}
def test_create_service_with_extra_kwargs(tmp_path: Path) -> None:
compose_file: Path = tmp_path / "docker-compose.yml"
manager = DockerComposeManager(str(compose_file))
manager.create_service(
name="db",
image="postgres:latest",
environment={"POSTGRES_PASSWORD": "example"},
volumes=["db_data:/var/lib/postgresql/data"],
depends_on=["web"],
).save()
with compose_file.open() as f:
data = yaml.safe_load(f)
assert "db" in data["services"]
assert data["services"]["db"]["volumes"] == ["db_data:/var/lib/postgresql/data"]
assert data["services"]["db"]["depends_on"] == ["web"]
def test_create_service_minimal(tmp_path: Path) -> None:
compose_file: Path = tmp_path / "docker-compose.yml"
manager = DockerComposeManager(str(compose_file))
manager.create_service(name="worker", image="busybox").save()
with compose_file.open() as f:
data = yaml.safe_load(f)
assert "worker" in data["services"]
assert data["services"]["worker"]["image"] == "busybox"
assert "ports" not in data["services"]["worker"]
assert "environment" not in data["services"]["worker"]
def test_create_service_all_fields(tmp_path: Path) -> None:
compose_file: Path = tmp_path / "docker-compose.yml"
manager = DockerComposeManager(str(compose_file))
manager.create_service(
name="full",
image="alpine:latest",
ports=["1234:1234"],
environment={"FOO": "bar"},
volumes=["data:/data"],
networks=["default", "custom"],
command=["echo", "hello"],
entrypoint=["/bin/sh", "-c"],
build={"context": ".", "dockerfile": "Dockerfile"},
healthcheck={"test": ["CMD", "true"], "interval": "1m"},
restart="always",
labels={"com.example": "label"},
depends_on=["db"],
configs=[{"source": "my_config", "target": "/etc/config"}],
secrets=[{"source": "my_secret", "target": "/run/secret"}],
deploy={"replicas": 2},
resources={"limits": {"cpus": "0.5", "memory": "50M"}},
extra_field="extra_value",
).save()
with compose_file.open() as f:
data = yaml.safe_load(f)
svc = data["services"]["full"]
assert svc["image"] == "alpine:latest"
assert svc["ports"] == ["1234:1234"]
assert svc["environment"] == {"FOO": "bar"}
assert svc["volumes"] == ["data:/data"]
assert svc["networks"] == ["default", "custom"]
assert svc["command"] == ["echo", "hello"]
assert svc["entrypoint"] == ["/bin/sh", "-c"]
assert svc["build"] == {"context": ".", "dockerfile": "Dockerfile"}
assert svc["healthcheck"] == {"test": ["CMD", "true"], "interval": "1m"}
assert svc["restart"] == "always"
assert svc["labels"] == {"com.example": "label"}
assert svc["depends_on"] == ["db"]
assert svc["configs"] == [{"source": "my_config", "target": "/etc/config"}]
assert svc["secrets"] == [{"source": "my_secret", "target": "/run/secret"}]
assert svc["deploy"] == {"replicas": 2}
assert svc["resources"] == {"limits": {"cpus": "0.5", "memory": "50M"}}
assert svc["extra_field"] == "extra_value"