diff --git a/.github/workflows/pr-verify.yaml b/.github/workflows/pr-verify.yaml index 4f7c7e0..2bb817a 100644 --- a/.github/workflows/pr-verify.yaml +++ b/.github/workflows/pr-verify.yaml @@ -43,6 +43,31 @@ jobs: uv sync --locked --directory src/ uv run --directory src/ pytest -m "integration" -v + integration-tests-podman: + runs-on: ubuntu-latest + needs: unit-tests + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Install Podman + run: | + sudo apt-get update + sudo apt-get -y install podman podman-compose podman-docker + + - name: Configure Podman socket + run: | + systemctl --user enable --now podman.socket + sudo rm -rf /var/run/docker.sock + sudo ln -s /run/user/$(id -u)/podman/podman.sock /var/run/docker.sock + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + + - name: Run integration tests with Podman + run: | + uv sync --locked --directory src/ + uv run --directory src/ pytest -m "integration" -v + code-quality: runs-on: ubuntu-latest steps: diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 01cb5e2..080f0b8 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -6,6 +6,7 @@ services: ALLOW_STOP: 1 CONTAINERS: 1 EXEC: 1 + NETWORKS: 1 POST: 1 VERSION: 1 read_only: true diff --git a/src/Dockerfile b/src/Dockerfile index 8725bc9..60a7fa5 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/astral-sh/uv:0.9.10 AS uv-builder -FROM restic/restic:0.18.1 +FROM docker.io/restic/restic:0.18.1 RUN apk update && apk add dcron diff --git a/src/restic_compose_backup/backup_runner.py b/src/restic_compose_backup/backup_runner.py index f80d64b..dd6640d 100644 --- a/src/restic_compose_backup/backup_runner.py +++ b/src/restic_compose_backup/backup_runner.py @@ -12,23 +12,28 @@ def run( volumes: dict = None, environment: dict = None, labels: dict = None, - source_container_id: str = None, + network_names: set[str] = set(), ): logger.info("Starting backup container") client = utils.docker_client() - container = client.containers.run( + container = client.containers.create( image, command, labels=labels, detach=True, environment=environment + ["BACKUP_PROCESS_CONTAINER=true"], volumes=volumes, - network_mode=f"container:{source_container_id}", # reuse original container network for optional access to docker proxy working_dir=os.getcwd(), tty=True, ) + for network_name in network_names: + network = client.networks.get(network_name) + network.connect(container) + + container.start() + logger.info("Backup process container: %s", container.name) log_generator = container.logs(stdout=True, stderr=True, stream=True, follow=True) diff --git a/src/restic_compose_backup/cli.py b/src/restic_compose_backup/cli.py index e395e02..59fd437 100644 --- a/src/restic_compose_backup/cli.py +++ b/src/restic_compose_backup/cli.py @@ -173,7 +173,7 @@ def backup(config, containers: RunningContainers): command="rcb start-backup-process", volumes=volumes, environment=containers.this_container.environment, - source_container_id=containers.this_container.id, + network_names=containers.this_container.network_names, labels={ containers.backup_process_label: "True", "com.docker.compose.project": containers.project_name, diff --git a/src/restic_compose_backup/containers.py b/src/restic_compose_backup/containers.py index 55ee256..99a9794 100644 --- a/src/restic_compose_backup/containers.py +++ b/src/restic_compose_backup/containers.py @@ -35,6 +35,11 @@ def __init__(self, data: dict): self._include = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_INCLUDE)) self._exclude = self._parse_pattern(self.get_label(enums.LABEL_VOLUMES_EXCLUDE)) + # Parse network information + network_settings: dict = data.get("NetworkSettings", {}) + networks: dict = network_settings.get("Networks", {}) + self._networks = networks + @property def instance(self) -> "Container": """Container: Get a service specific subclass instance""" @@ -73,6 +78,11 @@ def service_name(self) -> str: "com.docker.compose.service", default="" ) or self.get_label("com.docker.swarm.service.name", default="") + @property + def network_names(self) -> set[str]: + """set[str]: Set of network names the container is connected to""" + return set(self._networks.keys()) + @property def backup_process_label(self) -> str: """str: The unique backup process label for this project""" diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index 7af4b44..616f561 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -487,18 +487,41 @@ def backup_container_with_multi_project( # Remove the old container backup_cont.remove() + # Parse volumes from Binds format to volumes dict format + # Binds format: ["/host/path:/container/path:ro"] + # Volumes format: {"/host/path": {"bind": "/container/path", "mode": "ro"}} + volumes_dict = {} + for bind in host_config.get("Binds", []): + parts = bind.split(":") + if len(parts) >= 2: + host_path = parts[0] + container_path = parts[1] + mode = parts[2] if len(parts) > 2 else "rw" + volumes_dict[host_path] = {"bind": container_path, "mode": mode} + + # Get network names + networks = list(container_info["NetworkSettings"]["Networks"].keys()) + # Create a new container with the updated environment + # which is needed for the backup container to identify itself + # Note: We don't set hostname - Docker/Podman will set it to the container ID new_container = docker_client.containers.create( config["Image"], + command=config.get("Cmd"), environment=env_list, - volumes=host_config.get("Binds", []), - network=list(container_info["NetworkSettings"]["Networks"].keys())[0] - if container_info["NetworkSettings"]["Networks"] - else None, + volumes=volumes_dict, name=container_info["Name"].strip("/"), labels=config.get("Labels", {}), + detach=True, + working_dir=config.get("WorkingDir"), + entrypoint=config.get("Entrypoint"), ) + # Connect to networks after creation (more compatible with Podman) + for network_name in networks: + network = docker_client.networks.get(network_name) + network.connect(new_container) + # Start the new container new_container.start() diff --git a/src/tests/unit/fixtures.py b/src/tests/unit/fixtures.py index b3dd0ce..41a3bef 100644 --- a/src/tests/unit/fixtures.py +++ b/src/tests/unit/fixtures.py @@ -53,6 +53,14 @@ def wrapper(*args, **kwargs): "Status": "running", "Running": True, }, + "NetworkSettings": { + "Networks": { + f"{project}_default": { + "NetworkID": "network-id", + "IPAddress": "10.0.0.1", + } + } + }, } for container in containers ]