render($subdomain, "{$subdomain}.{$zone}", $backend); $this->shell->connectWithKey($trafficHost, 'root', (string) config('provisioning.ssh.private_key')); $this->shell->putFile($this->path($subdomain), $yaml); // putFile throws on failure } public function remove(string $trafficHost, string $subdomain): void { $this->shell->connectWithKey($trafficHost, 'root', (string) config('provisioning.ssh.private_key')); $this->shell->run('rm -f '.escapeshellarg($this->path($subdomain))); } 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((string) config('provisioning.traefik.dynamic_path'), '/')."/{$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\"", '', ]); } }