feat(lb): sync the cert to the front at the serve-only cutover (Sourcery #8)

A node converted to serve-only while it already holds a valid cert would serve the installer's
self-signed server.crt on the Go front until the cert is next due, because syncFrontCert only
runs on a certbot_generate and the daily cron sends none while the cert is valid. Queue one
certbot_generate at the cutover (MAIN has the domain; a serve-only node has no DB to resolve it
itself) so the node's existing-cert path runs syncFrontCert within the minute; a fresh node
issues via the webroot. Skipped when the node has only an IP (no http-01 name).
This commit is contained in:
obscuremind
2026-10-22 18:35:58 +03:00
parent a291e3fe7a
commit 39361b9a2b
2 changed files with 36 additions and 0 deletions
+17
View File
@@ -4,6 +4,7 @@ namespace XcVm\Cli\Commands;
use XcVm\Cli\CommandInterface;
use XcVm\Core\Backup\BackupService;
use XcVm\Core\Cluster\NodeActions;
use XcVm\Core\Cluster\StreamRecords;
use XcVm\Core\Cluster\StreamVersions;
use XcVm\Core\Config\SettingsManager;
@@ -371,6 +372,22 @@ class ServerInstallCommand implements CommandInterface {
// http-nginx + php-fpm and the Go front is the sole listener; without it
// the node stays dual-stacked until the next restart. Serve-only only.
call_user_func($rRunSSH, $rConn, 'sudo systemctl restart xc_vm');
// Sync the node's real cert to the Go front now: a node converted to
// serve-only while it already holds a valid cert would otherwise serve the
// installer's self-signed server.crt until the cert is next due (the daily
// cron sends no certbot_generate while it is valid). Queue one — the node's
// existing-cert path runs syncFrontCert (a fresh node issues via webroot).
// Skipped when the node has only an IP (no http-01 name).
$rCertDomains = [];
foreach (explode(',', (string) ($rServers[$rServerID]['domain_name'] ?? '')) as $rCertDomain) {
$rCertDomain = trim($rCertDomain);
if ($rCertDomain !== '' && !filter_var($rCertDomain, FILTER_VALIDATE_IP)) {
$rCertDomains[] = $rCertDomain;
}
}
if ($rCertDomains !== []) {
NodeActions::certbot($rServerID, $rCertDomains, $db);
}
} else {
// Downgrade out of serve-only: the service already (re)started above
// (finalizeHostAfterRuntime) while the marker was still present, so it booted
+19
View File
@@ -35,6 +35,25 @@ final class CertbotServeOnlyTest extends TestCase {
$this->assertMatchesRegularExpression("/\\} else \\{[^}]*--standalone/", $rSrc, 'the other branch keeps --standalone');
}
public function testServeOnlyCutoverQueuesACertbotSyncForTheFront(): void {
// A node converted to serve-only while it already holds a valid cert would serve the
// installer's self-signed server.crt until the cert is next due (the daily cron sends
// no certbot_generate while it is valid). The cutover queues one certbot_generate so
// the node's existing-cert path runs syncFrontCert within the minute; skipped when the
// node has only an IP (no http-01 name).
$rCmd = (string) file_get_contents(MAIN_HOME . 'Cli/Commands/ServerInstallCommand.php');
$this->assertMatchesRegularExpression(
"/if \\(\\\$rServeOnly\\) \\{.*NodeActions::certbot\\(\\\$rServerID, \\\$rCertDomains, \\\$db\\)/s",
$rCmd,
'the serve-only cutover queues a certbot_generate so an existing cert reaches the front'
);
$this->assertMatchesRegularExpression(
"/if \\(\\\$rCertDomains !== \\[\\]\\) \\{\\s*NodeActions::certbot/",
$rCmd,
'the certbot dispatch is skipped when the node has no domain name (IP-only)'
);
}
public function testSyncFrontCertCopiesForServeOnlyOnly(): void {
// On a serve-only node the Go front reads server.crt/server.key, so a renewed
// certbot cert must be copied there (its CertReloader hot-reloads on mtime).