feat: replace hardcoded docker-compose IPs with .env-based substitution
docker-compose.yml now uses ${VAR:-default} for every container IP and
the network subnet, so there are no hardcoded addresses in the YAML.
How it works:
- setup_cell.py generates .env at project root from ip_range (gitignored).
- docker-compose reads .env automatically at startup.
- When ip_range changes in Settings, the API writes a new .env via
ip_utils.write_env_file(); DNS/firewall/vIPs update immediately.
- User runs `make start` to recreate containers with the new IPs.
api/ip_utils.py gains ENV_VAR_NAMES dict and write_env_file(ip_range, path).
The old update_docker_compose_ips() direct-patch approach is removed from app.py.
3 new tests added (TestWriteEnvFile); total 324 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+53
-58
@@ -80,72 +80,67 @@ class TestGetVirtualIps(unittest.TestCase):
|
||||
self.assertEqual(vips['webdav'], '10.10.0.24')
|
||||
|
||||
|
||||
class TestUpdateDockerComposeIps(unittest.TestCase):
|
||||
COMPOSE_TEMPLATE = """\
|
||||
version: '3.3'
|
||||
services:
|
||||
caddy:
|
||||
networks:
|
||||
cell-network:
|
||||
ipv4_address: 172.20.0.2
|
||||
dns:
|
||||
networks:
|
||||
cell-network:
|
||||
ipv4_address: 172.20.0.3
|
||||
api:
|
||||
networks:
|
||||
cell-network:
|
||||
ipv4_address: 172.20.0.10
|
||||
networks:
|
||||
cell-network:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
"""
|
||||
|
||||
class TestWriteEnvFile(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False)
|
||||
self.tmp.write(self.COMPOSE_TEMPLATE)
|
||||
self.tmp.close()
|
||||
self.tmp = tempfile.mkdtemp()
|
||||
self.env_path = os.path.join(self.tmp, '.env')
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self.tmp.name)
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp)
|
||||
|
||||
def test_returns_false_for_missing_file(self):
|
||||
self.assertFalse(
|
||||
ip_utils.update_docker_compose_ips('172.20.0.0/16', '10.0.0.0/24', '/nonexistent/path.yml')
|
||||
)
|
||||
|
||||
def test_subnet_updated(self):
|
||||
ip_utils.update_docker_compose_ips('172.20.0.0/16', '10.0.0.0/24', self.tmp.name)
|
||||
with open(self.tmp.name) as f:
|
||||
content = f.read()
|
||||
self.assertIn('10.0.0.0/24', content)
|
||||
self.assertNotIn('172.20.0.0/16', content)
|
||||
|
||||
def test_caddy_ip_updated(self):
|
||||
ip_utils.update_docker_compose_ips('172.20.0.0/16', '10.0.0.0/24', self.tmp.name)
|
||||
with open(self.tmp.name) as f:
|
||||
content = f.read()
|
||||
self.assertIn('10.0.0.2', content)
|
||||
self.assertNotIn('172.20.0.2', content)
|
||||
|
||||
def test_api_ip_updated(self):
|
||||
ip_utils.update_docker_compose_ips('172.20.0.0/16', '10.0.0.0/24', self.tmp.name)
|
||||
with open(self.tmp.name) as f:
|
||||
content = f.read()
|
||||
self.assertIn('10.0.0.10', content)
|
||||
self.assertNotIn('172.20.0.10', content)
|
||||
def test_creates_file(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
self.assertTrue(os.path.exists(self.env_path))
|
||||
|
||||
def test_returns_true_on_success(self):
|
||||
result = ip_utils.update_docker_compose_ips('172.20.0.0/16', '10.0.0.0/24', self.tmp.name)
|
||||
result = ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_noop_when_ranges_same(self):
|
||||
ip_utils.update_docker_compose_ips('172.20.0.0/16', '172.20.0.0/16', self.tmp.name)
|
||||
with open(self.tmp.name) as f:
|
||||
content = f.read()
|
||||
self.assertEqual(content, self.COMPOSE_TEMPLATE)
|
||||
def test_returns_false_on_unwritable_path(self):
|
||||
result = ip_utils.write_env_file('172.20.0.0/16', '/nonexistent/deep/path/.env')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_contains_cell_network(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
content = open(self.env_path).read()
|
||||
self.assertIn('CELL_NETWORK=172.20.0.0/16', content)
|
||||
|
||||
def test_contains_caddy_ip(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
content = open(self.env_path).read()
|
||||
self.assertIn('CADDY_IP=172.20.0.2', content)
|
||||
|
||||
def test_contains_all_env_vars(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
content = open(self.env_path).read()
|
||||
for var in ip_utils.ENV_VAR_NAMES.values():
|
||||
self.assertIn(var + '=', content, f'{var} missing from .env')
|
||||
|
||||
def test_custom_subnet_generates_correct_ips(self):
|
||||
ip_utils.write_env_file('10.5.0.0/24', self.env_path)
|
||||
content = open(self.env_path).read()
|
||||
self.assertIn('CELL_NETWORK=10.5.0.0/24', content)
|
||||
self.assertIn('CADDY_IP=10.5.0.2', content)
|
||||
self.assertIn('DNS_IP=10.5.0.3', content)
|
||||
self.assertIn('API_IP=10.5.0.10', content)
|
||||
self.assertNotIn('172.20', content)
|
||||
|
||||
def test_overwrite_updates_ips(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
ip_utils.write_env_file('10.0.0.0/24', self.env_path)
|
||||
content = open(self.env_path).read()
|
||||
self.assertIn('CADDY_IP=10.0.0.2', content)
|
||||
self.assertNotIn('172.20', content)
|
||||
|
||||
def test_each_line_is_key_equals_value(self):
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
for line in open(self.env_path).read().splitlines():
|
||||
if line.strip():
|
||||
self.assertIn('=', line, f'Bad line format: {line!r}')
|
||||
key, _, val = line.partition('=')
|
||||
self.assertTrue(key.isupper() or '_' in key)
|
||||
self.assertTrue(val.strip())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user