CluPilotCloud/app/Provisioning/Jobs/PurgeHost.php

105 lines
4.2 KiB
PHP

<?php
namespace App\Provisioning\Jobs;
use App\Models\Host;
use App\Models\VpnPeer;
use App\Services\Dns\HetznerDnsClient;
use App\Services\Dns\HostDnsDirectory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
/**
* Finalizes host removal off the web request: deletes each run under its runner
* lock (waiting for any in-flight step to finish rather than racing it), then
* removes the WireGuard peer and the host record. Runs on the provisioning queue.
*/
class PurgeHost implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Retried, because the DNS deletion below can fail for a minute and the job
// is re-entrant: it re-reads the host, already-deleted runs are simply gone,
// and the peer removal is idempotent.
public $tries = 3;
/** @return list<int> seconds between attempts */
public function backoff(): array
{
return [30, 120];
}
public $timeout = 2200;
public function __construct(public string $uuid)
{
$this->onConnection('provisioning');
$this->onQueue('provisioning');
}
public function handle(): void
{
$host = Host::query()->where('uuid', $this->uuid)->first();
if ($host === null) {
return;
}
foreach ($host->runs()->get() as $run) {
// Block until the runner lock is free (a long step may hold it for up
// to the step timeout), then delete — cascades events + resources.
Cache::lock('run:'.$run->uuid, 2200)->block(2100, fn () => $run->delete());
}
// Re-read after waiting on the run locks above: a registration that was
// still running may have written dns_record_id in the meantime, and the
// stale copy loaded before the wait would skip the deletion and then
// delete the row that held the id.
$host->refresh();
// Legacy: a host onboarded before RegisterHostDns moved off the public
// Hetzner zone may still carry a public record id here — remove it so
// a deleted host does not leave its (already-published) management
// address behind. Hosts onboarded since never set this column; see
// clupilot:prune-host-dns for the ones already published elsewhere.
//
// The record identifier lives on the row about to be deleted, so this
// has to happen first — otherwise the host's management address stays
// published with nothing left to clean it up.
if (filled($host->dns_record_id)) {
// Deliberately NOT swallowed: the row about to be deleted holds the
// only reference to that record, so continuing past a failure would
// leave the machine's management address published for good with no
// way left to find it. Failing here lets the queue retry, and a
// permanently broken DNS provider becomes a visible failed job
// rather than a silent leak.
app(HetznerDnsClient::class)->deleteRecord($host->dns_record_id);
}
// The internal hostsdir entry (vpn-dns) — same reasoning as above,
// for the name every host has actually been getting since. The name
// lives on the row about to be deleted, so it has to come out first.
if (filled($host->dns_name)) {
app(HostDnsDirectory::class)->remove($host->dns_name);
}
if (filled($host->wg_pubkey)) {
// Tombstone the console's view of this peer as well. The FK only
// nulls host_id, which would leave an enabled-but-absent row behind:
// shown forever as "wird angewendet", and re-enabling it would put a
// deleted host's key back on the hub. Soft-deleting hands it to the
// same machinery every other revocation uses — the removal below
// clears the tombstone, and a sync retries if that removal is lost.
VpnPeer::query()->where('public_key', $host->wg_pubkey)->delete();
RemoveWireguardPeer::dispatch($host->wg_pubkey);
}
$host->delete();
}
}