render($subdomain, "{$subdomain}.{$zone}", $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"; } private function render(string $subdomain, string $fqdn, string $backend): string { return implode("\n", [ 'http:', ' routers:', " {$subdomain}:", " rule: \"Host(`{$fqdn}`)\"", " service: \"{$subdomain}\"", ' entryPoints: ["websecure"]', ' tls:', ' certResolver: "letsencrypt"', ' services:', " {$subdomain}:", ' loadBalancer:', ' servers:', " - url: \"http://{$backend}:80\"", '', ]); } }