fix(engine-b): write Traefik route to the serving host over SSH, not locally
TraefikWriter now targets the host that serves the traffic (DNS points at it): SshTraefikWriter SSHes in and writes the file-provider YAML there. Interface takes the traffic host + guest backend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
483a79a822
commit
157496f0c5
|
|
@ -41,11 +41,12 @@ class ConfigureDnsAndTls extends CustomerStep
|
||||||
$this->recordResource($run, $host, 'dns_record_id', $recordId);
|
$this->recordResource($run, $host, 'dns_record_id', $recordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traefik file-provider route → the guest VM (fall back to the host only
|
// Traefik file-provider route, written on the serving host (DNS points at
|
||||||
// if the guest address wasn't captured).
|
// it) and pointing at the guest VM.
|
||||||
if (! $instance->route_written) {
|
if (! $instance->route_written) {
|
||||||
$target = $instance->guest_ip ?: ($host->wg_ip ?? $host->public_ip);
|
$trafficHost = $host->wg_ip ?? $host->public_ip;
|
||||||
$this->traefik->write($instance->subdomain, $target);
|
$backend = $instance->guest_ip ?: $host->public_ip;
|
||||||
|
$this->traefik->write($trafficHost, $instance->subdomain, $backend);
|
||||||
$instance->update(['route_written' => true]);
|
$instance->update(['route_written' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@ namespace App\Services\Traefik;
|
||||||
|
|
||||||
class FakeTraefikWriter implements TraefikWriter
|
class FakeTraefikWriter implements TraefikWriter
|
||||||
{
|
{
|
||||||
/** @var array<string, string> subdomain => target */
|
/** @var array<string, string> subdomain => backend */
|
||||||
public array $routes = [];
|
public array $routes = [];
|
||||||
|
|
||||||
|
/** @var array<string, string> subdomain => traffic host */
|
||||||
|
public array $hosts = [];
|
||||||
|
|
||||||
public bool $certReady = true;
|
public bool $certReady = true;
|
||||||
|
|
||||||
public function write(string $subdomain, string $targetHost): void
|
public function write(string $trafficHost, string $subdomain, string $backend): void
|
||||||
{
|
{
|
||||||
$this->routes[$subdomain] = $targetHost;
|
$this->routes[$subdomain] = $backend;
|
||||||
|
$this->hosts[$subdomain] = $trafficHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remove(string $subdomain): void
|
public function remove(string $trafficHost, string $subdomain): void
|
||||||
{
|
{
|
||||||
unset($this->routes[$subdomain]);
|
unset($this->routes[$subdomain], $this->hosts[$subdomain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function certReachable(string $fqdn): bool
|
public function certReachable(string $fqdn): bool
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,32 @@
|
||||||
|
|
||||||
namespace App\Services\Traefik;
|
namespace App\Services\Traefik;
|
||||||
|
|
||||||
|
use App\Services\Ssh\RemoteShell;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes Traefik file-provider YAML into the configured dynamic directory (where
|
* Writes the Traefik file-provider YAML onto the host that actually serves the
|
||||||
* Traefik runs) and checks HTTP-01 cert issuance by probing the fqdn over HTTPS.
|
* traffic (DNS points at it), via SSH — not the worker's local filesystem — so
|
||||||
* The concrete write path (host SSH / mounted volume) is a deployment watch-item.
|
* that host's Traefik picks up the route. Checks HTTP-01 cert issuance by
|
||||||
* Not unit-tested (live I/O).
|
* probing the fqdn. Not unit-tested (live I/O).
|
||||||
*/
|
*/
|
||||||
class SshTraefikWriter implements TraefikWriter
|
class SshTraefikWriter implements TraefikWriter
|
||||||
{
|
{
|
||||||
public function write(string $subdomain, string $targetHost): void
|
public function __construct(private RemoteShell $shell) {}
|
||||||
|
|
||||||
|
public function write(string $trafficHost, string $subdomain, string $backend): void
|
||||||
{
|
{
|
||||||
$zone = (string) config('provisioning.dns.zone');
|
$zone = (string) config('provisioning.dns.zone');
|
||||||
$fqdn = "{$subdomain}.{$zone}";
|
$yaml = $this->render($subdomain, "{$subdomain}.{$zone}", $backend);
|
||||||
$yaml = $this->render($subdomain, $fqdn, $targetHost);
|
|
||||||
|
|
||||||
$path = rtrim((string) config('provisioning.traefik.dynamic_path'), '/')."/{$subdomain}.yml";
|
$this->shell->connectWithKey($trafficHost, 'root', (string) config('provisioning.ssh.private_key'));
|
||||||
if (file_put_contents($path, $yaml) === false) {
|
$this->shell->putFile($this->path($subdomain), $yaml); // putFile throws on failure
|
||||||
throw new RuntimeException("Failed to write Traefik config: {$path}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remove(string $subdomain): void
|
public function remove(string $trafficHost, string $subdomain): void
|
||||||
{
|
{
|
||||||
$path = rtrim((string) config('provisioning.traefik.dynamic_path'), '/')."/{$subdomain}.yml";
|
$this->shell->connectWithKey($trafficHost, 'root', (string) config('provisioning.ssh.private_key'));
|
||||||
if (is_file($path)) {
|
$this->shell->run('rm -f '.escapeshellarg($this->path($subdomain)));
|
||||||
unlink($path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function certReachable(string $fqdn): bool
|
public function certReachable(string $fqdn): bool
|
||||||
|
|
@ -45,7 +42,12 @@ class SshTraefikWriter implements TraefikWriter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function render(string $subdomain, string $fqdn, string $targetHost): string
|
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", [
|
return implode("\n", [
|
||||||
'http:',
|
'http:',
|
||||||
|
|
@ -60,7 +62,7 @@ class SshTraefikWriter implements TraefikWriter
|
||||||
" {$subdomain}:",
|
" {$subdomain}:",
|
||||||
' loadBalancer:',
|
' loadBalancer:',
|
||||||
' servers:',
|
' servers:',
|
||||||
" - url: \"http://{$targetHost}:80\"",
|
" - url: \"http://{$backend}:80\"",
|
||||||
'',
|
'',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ namespace App\Services\Traefik;
|
||||||
|
|
||||||
interface TraefikWriter
|
interface TraefikWriter
|
||||||
{
|
{
|
||||||
/** Write a file-provider router for subdomain → target host. */
|
/**
|
||||||
public function write(string $subdomain, string $targetHost): void;
|
* Write a file-provider router on the Traefik host ($trafficHost) that routes
|
||||||
|
* the subdomain to the guest ($backend).
|
||||||
|
*/
|
||||||
|
public function write(string $trafficHost, string $subdomain, string $backend): void;
|
||||||
|
|
||||||
public function remove(string $subdomain): void;
|
public function remove(string $trafficHost, string $subdomain): void;
|
||||||
|
|
||||||
/** True once the ACME (HTTP-01) certificate for the fqdn is served. */
|
/** True once the ACME (HTTP-01) certificate for the fqdn is served. */
|
||||||
public function certReachable(string $fqdn): bool;
|
public function certReachable(string $fqdn): bool;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue