fix(apply): handle ip_range network recreation; propagate IPs+ports to service pages
When ip_range changes, Docker cannot modify a network subnet in-place. _set_pending_restart now accepts network_recreate=True; apply endpoint runs `docker compose down` before `up -d` in that case so the bridge network is fully recreated with the new subnet. Service page fixes: - GET /api/config includes service_ips (dns, vip_mail, vip_calendar, vip_files, vip_webdav) computed via ip_utils - Email/Calendar/Files pages read IPs and ports from useConfig() instead of hardcoded 172.20.0.x constants and default port literals - Apply feedback: spinner → success/timeout/error banners via health polling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+39
-18
@@ -508,8 +508,12 @@ def update_config():
|
||||
# Write new .env with updated IPs (and current ports) for next container start
|
||||
env_file = os.environ.get('COMPOSE_ENV_FILE', '/app/.env.compose')
|
||||
ip_utils.write_env_file(new_range, env_file, _collect_service_ports(config_manager.configs))
|
||||
# Mark ALL containers as needing restart (IPs affect every container)
|
||||
_set_pending_restart([f'ip_range changed to {new_range} — container IPs updated'], ['*'])
|
||||
# Mark ALL containers as needing restart; network_recreate signals that
|
||||
# docker compose down is required before up (Docker can't change subnet in-place)
|
||||
_set_pending_restart(
|
||||
[f'ip_range changed to {new_range} — network will be recreated'],
|
||||
['*'], network_recreate=True
|
||||
)
|
||||
|
||||
# Detect port changes across service configs and identity
|
||||
# Maps (service_key, field_name) → (port_env_key, [containers])
|
||||
@@ -595,10 +599,11 @@ def _collect_service_ports(configs: dict) -> dict:
|
||||
return ports
|
||||
|
||||
|
||||
def _set_pending_restart(changes: list, containers: list = None):
|
||||
def _set_pending_restart(changes: list, containers: list = None, network_recreate: bool = False):
|
||||
"""Record that specific containers need to be restarted to apply configuration.
|
||||
|
||||
containers: list of docker-compose service names, or None/'*' to restart all.
|
||||
network_recreate: True when the Docker bridge subnet changed (requires down+up).
|
||||
Merges with any existing pending state so multiple changes accumulate.
|
||||
"""
|
||||
from datetime import datetime as _dt
|
||||
@@ -616,13 +621,14 @@ def _set_pending_restart(changes: list, containers: list = None):
|
||||
'changed_at': _dt.utcnow().isoformat(),
|
||||
'changes': existing_changes + changes,
|
||||
'containers': new_containers,
|
||||
'network_recreate': network_recreate or existing.get('network_recreate', False),
|
||||
}
|
||||
config_manager._save_all_configs()
|
||||
|
||||
|
||||
def _clear_pending_restart():
|
||||
config_manager.configs['_pending_restart'] = {
|
||||
'needs_restart': False, 'changes': [], 'containers': []
|
||||
'needs_restart': False, 'changes': [], 'containers': [], 'network_recreate': False
|
||||
}
|
||||
config_manager._save_all_configs()
|
||||
|
||||
@@ -669,24 +675,39 @@ def apply_pending_config():
|
||||
# Clear pending flag before we restart so it shows cleared after new containers start
|
||||
_clear_pending_restart()
|
||||
|
||||
# Build compose args: restart all, or only the specific changed containers
|
||||
if '*' in containers:
|
||||
compose_up_args = ['up', '-d']
|
||||
else:
|
||||
compose_up_args = ['up', '-d', '--no-deps'] + containers
|
||||
# Check if the IP range (network subnet) is changing — Docker cannot modify an
|
||||
# existing network's subnet in-place, so we need `down` + `up` in that case.
|
||||
needs_network_recreate = pending.get('network_recreate', False)
|
||||
|
||||
# Run docker compose up -d in a background thread; the 0.3s delay lets Flask
|
||||
# finish sending this response before cell-api itself gets recreated.
|
||||
if '*' in containers:
|
||||
if needs_network_recreate:
|
||||
# down removes containers AND the bridge network; up recreates everything
|
||||
compose_down_args = ['down']
|
||||
compose_up_args = ['up', '-d']
|
||||
else:
|
||||
compose_down_args = None
|
||||
compose_up_args = ['up', '-d']
|
||||
else:
|
||||
compose_down_args = None
|
||||
compose_up_args = ['up', '-d', '--no-deps'] + containers
|
||||
|
||||
base_cmd = ['docker', 'compose',
|
||||
'--project-directory', project_dir,
|
||||
'-f', '/app/docker-compose.yml',
|
||||
'--env-file', '/app/.env.compose']
|
||||
|
||||
# Run in a background thread; 0.3 s delay lets Flask send this response first.
|
||||
def _do_apply():
|
||||
import time as _time
|
||||
_time.sleep(0.3)
|
||||
result = subprocess.run(
|
||||
['docker', 'compose',
|
||||
'--project-directory', project_dir,
|
||||
'-f', '/app/docker-compose.yml',
|
||||
'--env-file', '/app/.env.compose'] + compose_up_args,
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if compose_down_args:
|
||||
r = subprocess.run(base_cmd + compose_down_args,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if r.returncode != 0:
|
||||
logger.error(f"docker compose down failed: {r.stderr.strip()}")
|
||||
return
|
||||
result = subprocess.run(base_cmd + compose_up_args,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"docker compose up failed: {result.stderr.strip()}")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user