82 lines
2.8 KiB
PHP
82 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Traefik;
|
|
|
|
use App\Services\Secrets\SecretVault;
|
|
use App\Services\Ssh\RemoteShell;
|
|
use App\Support\ProvisioningSettings;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Writes the Traefik file-provider YAML onto the host that actually serves the
|
|
* traffic (DNS points at it), via SSH — not the worker's local filesystem — so
|
|
* that host's Traefik picks up the route. Checks HTTP-01 cert issuance by
|
|
* probing the fqdn. Not unit-tested (live I/O).
|
|
*/
|
|
class SshTraefikWriter implements TraefikWriter
|
|
{
|
|
public function __construct(private RemoteShell $shell) {}
|
|
|
|
public function write(string $trafficHost, string $subdomain, string $backend): void
|
|
{
|
|
$zone = ProvisioningSettings::dnsZone();
|
|
$yaml = $this->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\"",
|
|
'',
|
|
]);
|
|
}
|
|
}
|