mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
When the IPAM config of a compose project's network changes (#999 added `ip_range`), compose recreates the network on the next `up`. With a foreign endpoint attached, as every worktree stack is on the primary's network, compose first stops the project's own containers, then fails with `error while removing network: ... has active endpoints`, and a repeated `up` or `up --force-recreate` does not recover. On 2026-10-19 this took the shared registry/package cache and coredns down for every local session. `make compose-network-recreate` (alias `i8conere`) runs the sequence that recovered it: detach every running container of another project from the network, `docker compose down --remove-orphans` without `-v` so the caches keep their volumes, the normal `up`, then attach the detached containers again, which draw addresses from the dynamic pool. If `up` fails, the still-detached containers are listed instead of reattached. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
75 lines
2.1 KiB
Python
75 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
|
|
from .common import make_compose
|
|
from .up import handler as up_handler
|
|
|
|
|
|
def foreign_endpoints(rows: str, project: str) -> list[str]:
|
|
"""Return the running containers of other projects on the network.
|
|
|
|
Args:
|
|
rows: ``docker ps`` lines of ``<name>\\t<compose project>``.
|
|
project: compose project that owns the network.
|
|
"""
|
|
names = []
|
|
for row in rows.splitlines():
|
|
name, _, owner = row.partition("\t")
|
|
if name and owner != project:
|
|
names.append(name)
|
|
return names
|
|
|
|
|
|
def _docker(*args: str, capture: bool = False) -> str:
|
|
result = subprocess.run(
|
|
["docker", *args], check=True, capture_output=capture, text=True
|
|
)
|
|
return result.stdout if capture else ""
|
|
|
|
|
|
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
p = sub.add_parser(
|
|
"recreate",
|
|
help="Recreate the default network, keeping volumes and foreign stacks.",
|
|
)
|
|
p.set_defaults(_handler=handler)
|
|
|
|
|
|
def handler(args: argparse.Namespace) -> int:
|
|
compose = make_compose()
|
|
rendered = compose.run(["config", "--format", "json"], capture=True).stdout
|
|
config = json.loads(rendered)
|
|
project = config["name"]
|
|
network = config["networks"]["default"]["name"]
|
|
|
|
rows = _docker(
|
|
"ps",
|
|
"--filter",
|
|
f"network={network}",
|
|
"--format",
|
|
'{{.Names}}\t{{.Label "com.docker.compose.project"}}',
|
|
capture=True,
|
|
)
|
|
detached = foreign_endpoints(rows, project)
|
|
for name in detached:
|
|
print(f">>> Detaching {name} from {network}")
|
|
_docker("network", "disconnect", network, name)
|
|
|
|
rc = 1
|
|
try:
|
|
compose.run(["down", "--remove-orphans"])
|
|
rc = up_handler(argparse.Namespace(skip_entry_init=False, when_down=False))
|
|
finally:
|
|
if rc != 0 and detached:
|
|
print(f">>> Still detached from {network}: {' '.join(detached)}")
|
|
if rc != 0:
|
|
return rc
|
|
|
|
for name in detached:
|
|
print(f">>> Attaching {name} to {network}")
|
|
_docker("network", "connect", network, name)
|
|
return 0
|