Files
computer-playbook/utils/env/handlers/infinito/ip_range.py
kevinveenbirkenbachandClaude Opus 5.5 d2b33a3b2a fix(compose): keep dynamic addresses off the stack's static IPs
compose/cache.shared.override.yml attaches every worktree stack's
infinito service to the primary's network (infinito-nexus-core_default,
172.30.0.0/24) without an ipv4_address. The default network declared
no ip_range, so Docker IPAM handed out the lowest free address and the
worktree stacks took 172.30.0.2, .3, .8, .9 and, whenever the primary
container was down, .10, the primary's INFINITO_IP4. The primary's
`make compose-up` then failed with "Address already in use", and with
it `make quality-high` on main.

The default network now declares ip_range: ${INFINITO_IP_RANGE}, the
upper half of INFINITO_SUBNET. Every static address of the stack (.1,
.4 to .6, .10, .53) sits in the lower half, so a container without a
static address can no longer take one.

INFINITO_IP_RANGE is derived from the final INFINITO_SUBNET after
custom.env is applied, because a worktree pins its subnet there and
`make dotenv-force` regenerates without it in the environment. Existing
worktrees therefore get their own slot's range without a new pin.

A lint checks that every static IP in default.env and every
172.30.${slot}.N pin in scripts/system/worktree/up.sh lies below the
pool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 21:44:16 +02:00

41 lines
1.2 KiB
Python

"""INFINITO_IP_RANGE: the dynamic IPAM pool of the compose stack's bridge.
Docker hands every container that joins the bridge without an
``ipv4_address`` the lowest free address of the pool. Worktree stacks join the
primary's network as the cache network, so without a pool they took the
primary's static addresses below .128. The pool is the upper half of the final
``INFINITO_SUBNET``; every static address sits in the lower half.
"""
from __future__ import annotations
import ipaddress
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import EnvBuilder
KEY = "INFINITO_IP_RANGE"
SUBNET_KEY = "INFINITO_SUBNET"
COMMENT = "Dynamic IPAM pool of INFINITO_SUBNET (its upper half); every static IP of the stack sits below it."
def upper_half(subnet: str) -> str:
"""Return the upper half of ``subnet``.
Args:
subnet: an IPv4 CIDR such as ``172.30.0.0/24``.
Returns:
The CIDR of its upper half, e.g. ``172.30.0.128/25``.
"""
network = ipaddress.ip_network(subnet, strict=True)
return str(list(network.subnets(prefixlen_diff=1))[1])
def finalize(eb: EnvBuilder) -> None:
subnet = eb.get(SUBNET_KEY)
if not subnet:
return
eb.set(KEY, upper_half(subnet), comment=COMMENT)