fix: renew_cert regenerates Caddyfile before reload
Unit Tests / test (push) Successful in 7m32s

A stale or empty-token Caddyfile on disk caused Caddy to reject the
/load request, so the Renew button appeared to do nothing. Now
renew_cert() calls regenerate_with_installed([]) first, which writes a
fresh Caddyfile from current identity/config before reloading Caddy.
This ensures a broken on-disk file never blocks ACME renewal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 14:38:30 -04:00
parent 6bd5f02b03
commit da302b5d54
2 changed files with 21 additions and 10 deletions
+16 -5
View File
@@ -649,11 +649,12 @@ class CaddyManager(BaseServiceManager):
# ── Active cert management ────────────────────────────────────────────
def renew_cert(self) -> Dict[str, Any]:
"""Trigger ACME cert renewal by reloading Caddy.
"""Regenerate the Caddyfile, reload Caddy, and trigger ACME cert renewal.
Returns immediately with status='pending' so the caller can poll
GET /api/caddy/cert-status to track progress. Not applicable to
LAN mode — callers should use upload_custom_cert() instead.
Regenerates first so a stale or broken on-disk Caddyfile never blocks
the reload. Returns immediately with status='pending'; the caller
polls GET /api/caddy/cert-status to track progress. Not applicable
to LAN mode — callers should use upload_custom_cert() instead.
"""
ident = (self.config_manager.get_identity() if self.config_manager else {}) or {}
domain_mode = ident.get('domain_mode', 'lan')
@@ -665,7 +666,17 @@ class CaddyManager(BaseServiceManager):
'Upload a custom certificate instead.',
}
if not self.reload_caddy():
# Regenerate → write → reload in one shot so the Caddyfile is always fresh.
if self.config_manager:
try:
ok = self.regenerate_with_installed([])
except Exception as exc:
logger.error('renew_cert: regenerate_with_installed failed: %s', exc)
ok = False
else:
ok = self.reload_caddy()
if not ok:
return {'ok': False, 'error': 'Caddy reload failed — check Caddy logs.'}
# Invalidate the cached status so the next poll triggers a fresh SSL check.
+5 -5
View File
@@ -493,17 +493,17 @@ class TestRenewCert(unittest.TestCase):
self.assertFalse(result['ok'])
self.assertIn('LAN', result['error'])
def test_acme_mode_calls_reload(self):
def test_acme_mode_calls_regenerate(self):
mgr = _mgr(identity={'domain_mode': 'pic_ngo'})
with patch.object(mgr, 'reload_caddy', return_value=True) as mock_reload:
with patch.object(mgr, 'regenerate_with_installed', return_value=True) as mock_regen:
result = mgr.renew_cert()
mock_reload.assert_called_once()
mock_regen.assert_called_once_with([])
self.assertTrue(result['ok'])
self.assertEqual(result['status'], 'pending')
def test_reload_failure_propagated(self):
mgr = _mgr(identity={'domain_mode': 'cloudflare'})
with patch.object(mgr, 'reload_caddy', return_value=False):
with patch.object(mgr, 'regenerate_with_installed', return_value=False):
result = mgr.renew_cert()
self.assertFalse(result['ok'])
self.assertIn('reload failed', result['error'])
@@ -512,7 +512,7 @@ class TestRenewCert(unittest.TestCase):
import time
mgr = _mgr(identity={'domain_mode': 'pic_ngo'})
mgr._cert_refreshed_at = time.monotonic()
with patch.object(mgr, 'reload_caddy', return_value=True):
with patch.object(mgr, 'regenerate_with_installed', return_value=True):
mgr.renew_cert()
self.assertIsNone(mgr._cert_refreshed_at)