fix: port changes now propagate to containers via env file in-place writes
Root cause: write_env_file used os.replace() which creates a new inode. Docker file bind-mounts track the original inode at mount time, so the container's /app/.env.compose never saw updates — docker compose always read the stale port value and skipped container recreation. Fixes: - ip_utils.write_env_file: write in-place (open 'w') instead of os.replace() so Docker bind-mounted files see the update immediately - apply_pending_config: add --force-recreate to docker compose up for specific-container restarts, bypassing config-hash comparison as a belt-and-suspenders measure Tests added: - TestWriteEnvFileInPlace: verifies inode is preserved across writes - TestApplyPendingConfigForceRecreate: verifies --force-recreate is in the docker compose command for specific-container restarts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -214,5 +214,50 @@ class TestWriteEnvFilePorts(unittest.TestCase):
|
||||
self.assertIn(var + '=', content, f'{var} missing from .env')
|
||||
|
||||
|
||||
class TestWriteEnvFileInPlace(unittest.TestCase):
|
||||
"""write_env_file must update the file in-place (same inode) so Docker
|
||||
file bind-mounts inside containers see the change immediately.
|
||||
os.replace() would create a new inode and the bind-mount would remain
|
||||
pointing at the stale inode."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp()
|
||||
self.env_path = os.path.join(self.tmp, '.env')
|
||||
# Pre-create the file so it has an initial inode
|
||||
with open(self.env_path, 'w') as f:
|
||||
f.write('INITIAL=1\n')
|
||||
self.initial_inode = os.stat(self.env_path).st_ino
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp)
|
||||
|
||||
def test_same_inode_after_write(self):
|
||||
"""Inode must NOT change after write_env_file — bind-mounts track the inode."""
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
|
||||
after_inode = os.stat(self.env_path).st_ino
|
||||
self.assertEqual(self.initial_inode, after_inode,
|
||||
'write_env_file changed the file inode — Docker bind-mounts '
|
||||
'would not see the update')
|
||||
|
||||
def test_same_inode_after_port_change(self):
|
||||
"""Inode must be preserved even when port values change."""
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path, {'wg_port': 51820})
|
||||
inode_first = os.stat(self.env_path).st_ino
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path, {'wg_port': 51821})
|
||||
inode_second = os.stat(self.env_path).st_ino
|
||||
self.assertEqual(inode_first, inode_second,
|
||||
'write_env_file changed inode on second write')
|
||||
self.assertIn('WG_PORT=51821', open(self.env_path).read())
|
||||
|
||||
def test_content_visible_via_open_after_write(self):
|
||||
"""After write_env_file the new content is immediately readable through
|
||||
the same file descriptor path (same inode)."""
|
||||
ip_utils.write_env_file('172.20.0.0/16', self.env_path, {'wg_port': 9999})
|
||||
content = open(self.env_path).read()
|
||||
self.assertIn('WG_PORT=9999', content)
|
||||
self.assertNotIn('INITIAL=1', content)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user