render($subdomain, $hostnames, $backend); $this->shell->connectWithKey($trafficHost, 'root', $this->privateKey()); $this->shell->putFile($this->path($subdomain), $yaml); // putFile throws on failure } public function remove(string $trafficHost, string $subdomain): void { $this->shell->connectWithKey($trafficHost, 'root', $this->privateKey()); $this->shell->run('rm -f '.escapeshellarg($this->path($subdomain))); } /** * The identity CluPilot deploys to every host: the one stored in the * console if there is one, else CLUPILOT_SSH_PRIVATE_KEY(_PATH). Read * HERE, at the point of use — see SecretVault's docblock (rule 3). */ private function privateKey(): string { return (string) app(SecretVault::class)->get('ssh.private_key'); } public function certReachable(string $fqdn): bool { // While DNS propagates / Traefik warms up, the client throws // (ConnectionException, TLS) — that's "not ready yet", not a hard error, // so the step keeps polling instead of burning the retry budget. try { return Http::timeout(5)->connectTimeout(5)->get("https://{$fqdn}/status.php")->successful(); } catch (\Throwable) { return false; } } private function path(string $subdomain): string { return rtrim(ProvisioningSettings::traefikDynamicPath(), '/')."/{$subdomain}.yml"; } /** * One router, one rule, every hostname in it. * * Traefik's `Host()` matcher takes several names in one call, and a single * rule keeps the certResolver, the service and the entryPoint stated once — * a second router would have to repeat all three and could drift from the * first. * * @param array $hostnames */ private function render(string $subdomain, array $hostnames, string $backend): string { $names = implode(', ', array_map(fn (string $host) => "`{$host}`", $hostnames)); return implode("\n", [ 'http:', ' routers:', " {$subdomain}:", " rule: \"Host({$names})\"", " service: \"{$subdomain}\"", ' entryPoints: ["websecure"]', ' tls:', ' certResolver: "letsencrypt"', ' services:', " {$subdomain}:", ' loadBalancer:', ' servers:', " - url: \"http://{$backend}:80\"", '', ]); } }