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: 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. """A class to create and modify Docker Compose YAML files programmatically.
Supports context manager usage for auto-saving. Supports context manager usage for auto-saving.
@ -47,9 +93,22 @@ class DockerComposeManager:
def create_service( def create_service(
self, self,
name: str, name: str,
image: str, image: str = "",
ports: list[str] | None = None, ports: list[str] | None = None,
environment: dict[str, 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, **kwargs: object,
) -> DockerComposeManager: ) -> DockerComposeManager:
"""Create a new service in the compose file. """Create a new service in the compose file.
@ -59,11 +118,39 @@ class DockerComposeManager:
""" """
services = self._data["services"] services = self._data["services"]
service: dict[str, Any] = {"image": image} service: dict[str, Any] = {}
if image:
service["image"] = image
if ports is not None: if ports is not None:
service["ports"] = ports service["ports"] = ports
if environment is not None: if environment is not None:
service["environment"] = environment 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) service.update(kwargs)
services[name] = service services[name] = service
self._dirty = True self._dirty = True

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
@ -65,3 +66,89 @@ def test_modify_nonexistent_service(tmp_path: Path) -> None:
manager = DockerComposeManager(str(compose_file)) manager = DockerComposeManager(str(compose_file))
with pytest.raises(KeyError): with pytest.raises(KeyError):
manager.modify_service("notfound", image="nginx:latest") 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"