2025-12-26 18:13:26 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
fix(backup,restore): harden the branch fixes and prove them with tests
Backup: a container that vanishes between the docker ps listing and the
swarm-task inspect (--rm one-shots, task-history GC) no longer aborts the
whole backup run; it counts as not stoppable and is skipped.
Restore: the postgres replay streams the dump through a spooled temp file
instead of buffering it three times in memory (multi-GB dumps OOMed the
restore mid-replay), and the superuser-only line filter is COPY-aware: data
rows inside COPY ... FROM stdin blocks pass through untouched, so a row
that happens to start with COMMENT ON EXTENSION or ALTER DEFAULT PRIVILEGES
is no longer silently dropped.
The e2e runner talks to the DinD daemon through docker exec instead of a
host-published tcp://127.0.0.1:2375: port publishing is unreachable from
sandboxed runners and from hosts with broken loopback publishing, and the
unencrypted root API port disappears from the host. The debug tmp dump
shrinks to tar plus docker cp against the DinD container itself.
New coverage: an e2e reproducing the swarm flake end to end (service task
on the volume, nothing whitelisted: the backup must succeed, the very same
task container must keep running, and the service must never replace a
task), unit tests for the COPY-aware filter, the swarm-task probe including
the vanished-container path, filter_stoppable ordering, and the one-session
FOREIGN_KEY_CHECKS drop assembly. Full suite: 35 unit, 9 integration,
30 e2e green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:18:40 +02:00
|
|
|
from .shell import BackupException, execute_shell_command
|
2025-12-26 18:13:26 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_image_info(container: str) -> str:
|
|
|
|
|
return execute_shell_command(
|
|
|
|
|
f"docker inspect --format '{{{{.Config.Image}}}}' {container}"
|
|
|
|
|
)[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def has_image(container: str, pattern: str) -> bool:
|
|
|
|
|
"""Return True if container's image contains the pattern."""
|
|
|
|
|
return pattern in get_image_info(container)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def docker_volume_names() -> list[str]:
|
|
|
|
|
return execute_shell_command("docker volume ls --format '{{.Name}}'")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def containers_using_volume(volume_name: str) -> list[str]:
|
|
|
|
|
return execute_shell_command(
|
|
|
|
|
f"docker ps --filter volume=\"{volume_name}\" --format '{{{{.Names}}}}'"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 08:23:11 +02:00
|
|
|
def is_swarm_task(container: str) -> bool:
|
|
|
|
|
"""Swarm-managed task containers must never be stopped or started
|
|
|
|
|
manually: the orchestrator replaces the stopped task and a later
|
fix(backup,restore): harden the branch fixes and prove them with tests
Backup: a container that vanishes between the docker ps listing and the
swarm-task inspect (--rm one-shots, task-history GC) no longer aborts the
whole backup run; it counts as not stoppable and is skipped.
Restore: the postgres replay streams the dump through a spooled temp file
instead of buffering it three times in memory (multi-GB dumps OOMed the
restore mid-replay), and the superuser-only line filter is COPY-aware: data
rows inside COPY ... FROM stdin blocks pass through untouched, so a row
that happens to start with COMMENT ON EXTENSION or ALTER DEFAULT PRIVILEGES
is no longer silently dropped.
The e2e runner talks to the DinD daemon through docker exec instead of a
host-published tcp://127.0.0.1:2375: port publishing is unreachable from
sandboxed runners and from hosts with broken loopback publishing, and the
unencrypted root API port disappears from the host. The debug tmp dump
shrinks to tar plus docker cp against the DinD container itself.
New coverage: an e2e reproducing the swarm flake end to end (service task
on the volume, nothing whitelisted: the backup must succeed, the very same
task container must keep running, and the service must never replace a
task), unit tests for the COPY-aware filter, the swarm-task probe including
the vanished-container path, filter_stoppable ordering, and the one-session
FOREIGN_KEY_CHECKS drop assembly. Full suite: 35 unit, 9 integration,
30 e2e green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:18:40 +02:00
|
|
|
`docker start` fails on the detached overlay network. A container that
|
|
|
|
|
vanished between listing and inspect (--rm one-shots, task-history GC)
|
2026-07-11 09:28:21 +02:00
|
|
|
counts as not stoppable instead of aborting the whole backup run; if the
|
|
|
|
|
container still exists the inspect failure re-raises, so a broken daemon
|
|
|
|
|
keeps failing the run loudly instead of silently skipping the stop."""
|
fix(backup,restore): harden the branch fixes and prove them with tests
Backup: a container that vanishes between the docker ps listing and the
swarm-task inspect (--rm one-shots, task-history GC) no longer aborts the
whole backup run; it counts as not stoppable and is skipped.
Restore: the postgres replay streams the dump through a spooled temp file
instead of buffering it three times in memory (multi-GB dumps OOMed the
restore mid-replay), and the superuser-only line filter is COPY-aware: data
rows inside COPY ... FROM stdin blocks pass through untouched, so a row
that happens to start with COMMENT ON EXTENSION or ALTER DEFAULT PRIVILEGES
is no longer silently dropped.
The e2e runner talks to the DinD daemon through docker exec instead of a
host-published tcp://127.0.0.1:2375: port publishing is unreachable from
sandboxed runners and from hosts with broken loopback publishing, and the
unencrypted root API port disappears from the host. The debug tmp dump
shrinks to tar plus docker cp against the DinD container itself.
New coverage: an e2e reproducing the swarm flake end to end (service task
on the volume, nothing whitelisted: the backup must succeed, the very same
task container must keep running, and the service must never replace a
task), unit tests for the COPY-aware filter, the swarm-task probe including
the vanished-container path, filter_stoppable ordering, and the one-session
FOREIGN_KEY_CHECKS drop assembly. Full suite: 35 unit, 9 integration,
30 e2e green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:18:40 +02:00
|
|
|
try:
|
|
|
|
|
out = execute_shell_command(
|
|
|
|
|
"docker inspect --format "
|
|
|
|
|
f"'{{{{index .Config.Labels \"com.docker.swarm.task.id\"}}}}' {container}"
|
|
|
|
|
)
|
|
|
|
|
except BackupException:
|
2026-07-11 09:28:21 +02:00
|
|
|
still_listed = execute_shell_command(
|
|
|
|
|
f"docker ps -a --filter name=^{container}$ --format '{{{{.Names}}}}'"
|
|
|
|
|
)
|
|
|
|
|
if still_listed and still_listed[0].strip():
|
|
|
|
|
raise
|
fix(backup,restore): harden the branch fixes and prove them with tests
Backup: a container that vanishes between the docker ps listing and the
swarm-task inspect (--rm one-shots, task-history GC) no longer aborts the
whole backup run; it counts as not stoppable and is skipped.
Restore: the postgres replay streams the dump through a spooled temp file
instead of buffering it three times in memory (multi-GB dumps OOMed the
restore mid-replay), and the superuser-only line filter is COPY-aware: data
rows inside COPY ... FROM stdin blocks pass through untouched, so a row
that happens to start with COMMENT ON EXTENSION or ALTER DEFAULT PRIVILEGES
is no longer silently dropped.
The e2e runner talks to the DinD daemon through docker exec instead of a
host-published tcp://127.0.0.1:2375: port publishing is unreachable from
sandboxed runners and from hosts with broken loopback publishing, and the
unencrypted root API port disappears from the host. The debug tmp dump
shrinks to tar plus docker cp against the DinD container itself.
New coverage: an e2e reproducing the swarm flake end to end (service task
on the volume, nothing whitelisted: the backup must succeed, the very same
task container must keep running, and the service must never replace a
task), unit tests for the COPY-aware filter, the swarm-task probe including
the vanished-container path, filter_stoppable ordering, and the one-session
FOREIGN_KEY_CHECKS drop assembly. Full suite: 35 unit, 9 integration,
30 e2e green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:18:40 +02:00
|
|
|
return True
|
2026-07-11 08:23:11 +02:00
|
|
|
return bool(out and out[0].strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_stoppable(containers: list[str]) -> list[str]:
|
|
|
|
|
"""Containers baudolo may stop/start itself (everything but swarm tasks)."""
|
|
|
|
|
stoppable = []
|
|
|
|
|
for container in containers:
|
|
|
|
|
if is_swarm_task(container):
|
|
|
|
|
print(
|
|
|
|
|
f"Skipping stop/start for swarm task container '{container}'.",
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
stoppable.append(container)
|
|
|
|
|
return stoppable
|
|
|
|
|
|
|
|
|
|
|
2025-12-26 18:13:26 +01:00
|
|
|
def change_containers_status(containers: list[str], status: str) -> None:
|
|
|
|
|
"""Stop or start a list of containers."""
|
|
|
|
|
if not containers:
|
|
|
|
|
print(f"No containers to {status}.", flush=True)
|
|
|
|
|
return
|
|
|
|
|
names = " ".join(containers)
|
|
|
|
|
print(f"{status.capitalize()} containers: {names}...", flush=True)
|
|
|
|
|
execute_shell_command(f"docker {status} {names}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def docker_volume_exists(volume: str) -> bool:
|
|
|
|
|
# Avoid throwing exceptions for exists checks.
|
|
|
|
|
try:
|
2025-12-28 22:12:31 +01:00
|
|
|
execute_shell_command(
|
|
|
|
|
f"docker volume inspect {volume} >/dev/null 2>&1 && echo OK"
|
|
|
|
|
)
|
2025-12-26 18:13:26 +01:00
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|