133 lines
5.0 KiB
PHP
133 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Actions;
|
|
|
|
use App\Models\Instance;
|
|
use App\Models\Order;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\Jobs\AdvanceRunJob;
|
|
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, which is this run's state or newer. Checked
|
|
// against the ORDER, because that is the subject both pipelines share:
|
|
// starting an address run beside an unfinished customer run would have
|
|
// two runs writing one router and one Nextcloud config.
|
|
if ($this->hasRunInFlight($order)) {
|
|
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();
|
|
}
|
|
|
|
private function hasRunInFlight(Order $order): bool
|
|
{
|
|
return ProvisioningRun::query()
|
|
->where('subject_type', Order::class)
|
|
->where('subject_id', $order->id)
|
|
->whereIn('status', [
|
|
ProvisioningRun::STATUS_PENDING,
|
|
ProvisioningRun::STATUS_RUNNING,
|
|
ProvisioningRun::STATUS_WAITING,
|
|
ProvisioningRun::STATUS_PAUSED,
|
|
])
|
|
->exists();
|
|
}
|
|
}
|