CluPilotCloud/app/Actions/ReapplyInstanceAddress.php

122 lines
4.7 KiB
PHP

<?php
namespace App\Actions;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Provisioning\WorkInFlight;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
/**
* Make an instance's address true again — the router, the certificate and the
* hostname Nextcloud answers to — without touching anything else about it.
*
* A custom domain used to be announced and never served: the portal, the
* credentials mail and Instance::address() all reported it the moment its TXT
* proof appeared, while the only thing that ever wrote a route was the initial
* provisioning run, and it wrote the platform subdomain. This is the missing
* half — the moment a proven domain becomes an address, and the moment a
* withdrawn one stops being one.
*
* Called from the three places the address can change: the nightly proof check
* when verification flips, the customer's own domain page, and a package change
* that takes the right to a domain away. Deliberately not called from anywhere
* that merely LOOKS at the domain — a re-apply is remote work on a live
* machine, so it happens on a change and not on a schedule.
*/
class ReapplyInstanceAddress
{
/**
* Start an address run for this instance, or return null when there is
* nothing to start.
*
* Null is the ordinary answer, not a failure: an instance still being
* built, one whose machine no longer exists, or one that already has a run
* in flight all have their address applied by that run instead.
*/
public function __invoke(?Instance $instance): ?ProvisioningRun
{
if ($instance === null || ! $this->isReapplyable($instance)) {
return null;
}
// The lock closes the window between asking whether a run exists and
// creating one. Two requests can reach this in the same instant — the
// customer's own "check now" and the nightly command are the obvious
// pair — and two runs against one machine would write the same router
// twice and race each other's occ calls. Nobody waits for the lock: a
// re-apply that lost the race has nothing to add, because the run that
// won reads the same domain state it would have.
$lock = Cache::lock('instance-address:'.$instance->uuid, 30);
if (! $lock->get()) {
return null;
}
try {
return $this->start($instance);
} finally {
$lock->release();
}
}
private function start(Instance $instance): ?ProvisioningRun
{
$order = $instance->order;
// A run already under way applies whatever the domain state says when it
// gets to the step — but only if it HAS the step. Asked of the two steps
// this pipeline consists of rather than of the mere existence of a run:
// `customer` and `plan-change` both carry them and there is nothing to
// add, while `restart`, `storage` and `quota` carry neither, and standing
// aside for one of those was how a domain proven during a restart stayed
// unrouted for good. See App\Provisioning\WorkInFlight.
if (app(WorkInFlight::class)->covers($order, 'address')) {
return null;
}
$run = ProvisioningRun::create([
'subject_type' => Order::class,
'subject_id' => $order->id,
'pipeline' => 'address',
'status' => ProvisioningRun::STATUS_PENDING,
'current_step' => 0,
// Everything the two steps read. instance_id is how CustomerStep
// finds the machine; node and vmid are how it reaches inside it.
'context' => [
'instance_id' => $instance->id,
'host_id' => $instance->host_id,
'node' => $instance->host?->node,
'vmid' => $instance->vmid,
'subdomain' => $instance->subdomain,
],
]);
AdvanceRunJob::dispatch($run->uuid);
Log::info('Re-applying an instance address.', [
'instance' => $instance->uuid,
'domain' => $instance->custom_domain,
'verified' => $instance->domainIsVerified(),
'run' => $run->uuid,
]);
return $run;
}
/**
* Is there a machine here whose address can be re-applied at all?
*
* The question is not this action's alone — a plan change asks exactly the
* same one before it starts a run against a live machine — so the rule sits
* on the model and both read it there. See Instance::hasLiveMachine().
*/
private function isReapplyable(Instance $instance): bool
{
return $instance->hasLiveMachine();
}
}