112 lines
4.9 KiB
PHP
112 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Provisioning\PipelineRegistry;
|
|
use App\Services\Dns\HetznerDnsClient;
|
|
use App\Services\Dns\HttpHetznerDnsClient;
|
|
use App\Services\Monitoring\HttpMonitoringClient;
|
|
use App\Services\Monitoring\MonitoringClient;
|
|
use App\Services\Proxmox\HttpProxmoxClient;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use App\Services\Ssh\PhpseclibRemoteShell;
|
|
use App\Services\Ssh\RemoteShell;
|
|
use App\Services\Traefik\SshTraefikWriter;
|
|
use App\Services\Traefik\TraefikWriter;
|
|
use App\Services\Wireguard\LocalWireguardHub;
|
|
use App\Services\Wireguard\WireguardHub;
|
|
use App\Mail\MaintenanceCancelledMail;
|
|
use App\Models\MaintenanceNotification;
|
|
use App\Services\Maintenance\MaintenanceNotifier;
|
|
use Illuminate\Mail\Events\MessageSending;
|
|
use Illuminate\Mail\Events\MessageSent;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
class AppServiceProvider extends ServiceProvider
|
|
{
|
|
/**
|
|
* Register any application services.
|
|
*/
|
|
public function register(): void
|
|
{
|
|
$this->app->singleton(PipelineRegistry::class, fn () => new PipelineRegistry(
|
|
config('provisioning.pipelines', []),
|
|
));
|
|
|
|
// Real I/O implementations; tests swap in fakes via app()->instance().
|
|
$this->app->bind(RemoteShell::class, PhpseclibRemoteShell::class);
|
|
$this->app->bind(WireguardHub::class, LocalWireguardHub::class);
|
|
$this->app->bind(ProxmoxClient::class, HttpProxmoxClient::class);
|
|
$this->app->bind(HetznerDnsClient::class, HttpHetznerDnsClient::class);
|
|
$this->app->bind(TraefikWriter::class, SshTraefikWriter::class);
|
|
$this->app->bind(MonitoringClient::class, HttpMonitoringClient::class);
|
|
}
|
|
|
|
/**
|
|
* Bootstrap any application services.
|
|
*/
|
|
public function boot(): void
|
|
{
|
|
// Send-time guard for maintenance mail (X-CP-Notification carries the
|
|
// ledger id). Uses a distinct in-flight claim (claimed_at) so exactly one
|
|
// copy ships without ever pre-marking delivery:
|
|
// • already delivered (sent_at set) or window cancelled → suppress;
|
|
// • otherwise atomically claim claimed_at (null or older than the TTL) —
|
|
// the winner sends; a concurrent duplicate loses the claim and aborts.
|
|
// sent_at is set only on real delivery (MessageSent), so a transport
|
|
// failure leaves the claim to expire and the mail stays retryable.
|
|
Event::listen(MessageSending::class, function (MessageSending $event) {
|
|
$header = $event->message->getHeaders()->get('X-CP-Notification');
|
|
if ($header === null) {
|
|
return null;
|
|
}
|
|
$id = (int) $header->getBodyAsString();
|
|
$notification = MaintenanceNotification::query()->with('window')->find($id);
|
|
if ($notification === null) {
|
|
return null;
|
|
}
|
|
if ($notification->sent_at !== null) {
|
|
return false; // already delivered — suppress a duplicate copy
|
|
}
|
|
if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled') {
|
|
return false; // window cancelled meanwhile — do not deliver
|
|
}
|
|
|
|
$claimed = MaintenanceNotification::query()
|
|
->whereKey($id)
|
|
->whereNull('sent_at')
|
|
->where(fn ($q) => $q->whereNull('claimed_at')->orWhere('claimed_at', '<=', now()->subMinutes(15)))
|
|
->update(['claimed_at' => now()]);
|
|
|
|
return $claimed === 0 ? false : null; // lost the claim → a sibling is delivering
|
|
});
|
|
|
|
// Stamp a maintenance-notification ledger row as delivered only once the
|
|
// mail is actually sent (the X-CP-Notification header carries the id).
|
|
// Until then sent_at stays null → the row is a retryable marker. If an
|
|
// announcement delivered for an already-cancelled window (the cancel race),
|
|
// queue a catch-up cancellation so that customer isn't left mis-informed.
|
|
Event::listen(MessageSent::class, function (MessageSent $event) {
|
|
$header = $event->message->getHeaders()->get('X-CP-Notification');
|
|
if ($header === null) {
|
|
return;
|
|
}
|
|
$notification = MaintenanceNotification::query()->with(['window', 'customer'])->find((int) $header->getBodyAsString());
|
|
if ($notification === null) {
|
|
return;
|
|
}
|
|
$notification->update(['sent_at' => now()]);
|
|
|
|
if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled' && $notification->customer !== null) {
|
|
app(MaintenanceNotifier::class)->deliver(
|
|
$notification->window,
|
|
$notification->customer,
|
|
'cancelled',
|
|
new MaintenanceCancelledMail($notification->window, $notification->customer),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|