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:
+35
-14
@@ -508,8 +508,12 @@ def update_config():
|
|||||||
# Write new .env with updated IPs (and current ports) for next container start
|
# Write new .env with updated IPs (and current ports) for next container start
|
||||||
env_file = os.environ.get('COMPOSE_ENV_FILE', '/app/.env.compose')
|
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))
|
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)
|
# Mark ALL containers as needing restart; network_recreate signals that
|
||||||
_set_pending_restart([f'ip_range changed to {new_range} — container IPs updated'], ['*'])
|
# 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
|
# Detect port changes across service configs and identity
|
||||||
# Maps (service_key, field_name) → (port_env_key, [containers])
|
# Maps (service_key, field_name) → (port_env_key, [containers])
|
||||||
@@ -595,10 +599,11 @@ def _collect_service_ports(configs: dict) -> dict:
|
|||||||
return ports
|
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.
|
"""Record that specific containers need to be restarted to apply configuration.
|
||||||
|
|
||||||
containers: list of docker-compose service names, or None/'*' to restart all.
|
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.
|
Merges with any existing pending state so multiple changes accumulate.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
@@ -616,13 +621,14 @@ def _set_pending_restart(changes: list, containers: list = None):
|
|||||||
'changed_at': _dt.utcnow().isoformat(),
|
'changed_at': _dt.utcnow().isoformat(),
|
||||||
'changes': existing_changes + changes,
|
'changes': existing_changes + changes,
|
||||||
'containers': new_containers,
|
'containers': new_containers,
|
||||||
|
'network_recreate': network_recreate or existing.get('network_recreate', False),
|
||||||
}
|
}
|
||||||
config_manager._save_all_configs()
|
config_manager._save_all_configs()
|
||||||
|
|
||||||
|
|
||||||
def _clear_pending_restart():
|
def _clear_pending_restart():
|
||||||
config_manager.configs['_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()
|
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 flag before we restart so it shows cleared after new containers start
|
||||||
_clear_pending_restart()
|
_clear_pending_restart()
|
||||||
|
|
||||||
# Build compose args: restart all, or only the specific changed 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)
|
||||||
|
|
||||||
if '*' in containers:
|
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']
|
compose_up_args = ['up', '-d']
|
||||||
else:
|
else:
|
||||||
|
compose_down_args = None
|
||||||
|
compose_up_args = ['up', '-d']
|
||||||
|
else:
|
||||||
|
compose_down_args = None
|
||||||
compose_up_args = ['up', '-d', '--no-deps'] + containers
|
compose_up_args = ['up', '-d', '--no-deps'] + containers
|
||||||
|
|
||||||
# Run docker compose up -d in a background thread; the 0.3s delay lets Flask
|
base_cmd = ['docker', 'compose',
|
||||||
# finish sending this response before cell-api itself gets recreated.
|
'--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():
|
def _do_apply():
|
||||||
import time as _time
|
import time as _time
|
||||||
_time.sleep(0.3)
|
_time.sleep(0.3)
|
||||||
result = subprocess.run(
|
if compose_down_args:
|
||||||
['docker', 'compose',
|
r = subprocess.run(base_cmd + compose_down_args,
|
||||||
'--project-directory', project_dir,
|
capture_output=True, text=True, timeout=60)
|
||||||
'-f', '/app/docker-compose.yml',
|
if r.returncode != 0:
|
||||||
'--env-file', '/app/.env.compose'] + compose_up_args,
|
logger.error(f"docker compose down failed: {r.stderr.strip()}")
|
||||||
capture_output=True, text=True, timeout=120
|
return
|
||||||
)
|
result = subprocess.run(base_cmd + compose_up_args,
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error(f"docker compose up failed: {result.stderr.strip()}")
|
logger.error(f"docker compose up failed: {result.stderr.strip()}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user