CluPilotCloud/app/Provisioning/Steps/Customer/SettlePlanServices.php

114 lines
4.0 KiB
PHP

<?php
namespace App\Provisioning\Steps\Customer;
use App\Models\Instance;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Monitoring\MonitoringClient;
use App\Services\Proxmox\ProxmoxClient;
use App\Support\ProvisioningSettings;
use Throwable;
/**
* Backup and monitoring, after a plan change — checked, and almost always left
* alone.
*
* Neither of them depends on the package today. Every instance is backed up
* nightly at 02:00 whatever it costs, and the monitoring check is
* `https://{subdomain}/status.php`, which a plan change does not move. So the
* honest thing for this step to do in the ordinary case is nothing: deleting and
* re-creating a vzdump job to arrive at the same schedule would be remote work
* on a live host in exchange for a second job to keep in step with, and
* re-registering a monitor would leave two checks alerting on one instance.
*
* It is a step rather than an omission for two reasons. It is the named place a
* plan-dependent backup or monitoring rule goes the day there is one — the
* alternative being that somebody adds it to whichever step they happen to be
* reading. And there is one real gap it closes: an instance whose build
* registered neither, because monitoring was unreachable and the run went on
* DEGRADED, or because the build was interrupted after these steps. A plan
* change is a reasonable moment to notice that and put it right.
*/
class SettlePlanServices extends CustomerStep
{
public function __construct(
private ProxmoxClient $pve,
private MonitoringClient $monitoring,
) {}
public function key(): string
{
return 'settle_plan_services';
}
public function maxDuration(): int
{
return 120;
}
public function execute(ProvisioningRun $run): StepResult
{
$instance = $this->instance($run);
if ($instance === null) {
return StepResult::fail('no_instance');
}
// Every package, the same nightly window — see the class docblock for
// why this is read from one place rather than derived from the plan.
$schedule = '02:00';
if (! $instance->backups()->exists()) {
$jobId = $this->pve->forHost($instance->host)
->createBackupJob((string) $run->context('node'), (int) $instance->vmid, $schedule);
// Local row before anything else can fail, exactly as RegisterBackup
// does it, so a retry finds the job rather than creating a second.
$instance->backups()->firstOrCreate(
['external_job_id' => $jobId],
['schedule' => $schedule, 'status' => 'scheduled'],
);
}
$this->settleMonitoring($run, $instance);
return StepResult::advance();
}
/**
* Monitoring is observability, not the product.
*
* The same judgement RegisterMonitoring makes during a build applies here
* with more force: a customer's plan change must not fail because Kuma is
* down. The gap is recorded on the run and the change goes through.
*/
private function settleMonitoring(ProvisioningRun $run, Instance $instance): void
{
$url = 'https://'.$instance->subdomain.'.'.ProvisioningSettings::dnsZone().'/status.php';
if ($instance->monitoringTargets()->where('url', $url)->exists()) {
return;
}
try {
$targetId = $this->monitoring->registerTarget($instance->subdomain, $url);
} catch (Throwable $e) {
$run->events()->create([
'step' => $this->key(),
'attempt' => $run->attempt,
'outcome' => 'info',
'message' => 'Monitoring übersprungen (Dienst nicht erreichbar): '.$e->getMessage(),
]);
return;
}
$instance->monitoringTargets()->firstOrCreate(
['external_id' => $targetId],
// 'unknown', not 'up': registering a check is not passing one.
['url' => $url, 'status' => 'unknown'],
);
}
}