user(); if ($operator !== null && $operator->isActive() && $operator->can('instances.restart')) { return true; } $customer = Customer::forUser(Auth::guard('web')->user()); return $customer !== null && (int) $instance->customer_id === (int) $customer->id; } /** * Start a restart run for this instance, or return null when there is * nothing to start. * * Null is the ordinary "no" and not a failure — a machine that is still * being built, one whose VM has gone, one whose service has ended, or one * that already has a run in flight all have a better reason to be left * alone than a restart has to interrupt them. A refusal on AUTHORISATION is * the one case that throws, because it is not an ordinary outcome and the * caller must not be able to mistake it for one. * * @throws AuthorizationException */ public function __invoke(Instance $instance): ?ProvisioningRun { if (! $this->allows($instance)) { throw new AuthorizationException; } if (! $instance->hasLiveMachine()) { return null; } // The same lock ReapplyInstanceAddress takes, for the same reason: two // requests can reach this in one instant — the customer pressing the // button while an operator presses it for them is the obvious pair — // and two restart runs against one machine would have the second one // shutting the guest down while the first waits for it to come back. // Nobody waits for the lock: a restart that lost the race has nothing // to add, because the run that won does exactly the same thing. $lock = Cache::lock('instance-restart:'.$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; // Checked against the ORDER, because that is the subject every customer // pipeline shares. A restart beside an unfinished build would stop the // machine the build is still writing to; a restart beside a plan change // would race the config PUT it is about to boot. Whatever is in flight // gets to finish, and the customer can press the button again after. if ($this->hasRunInFlight($order)) { return null; } $run = ProvisioningRun::create([ 'subject_type' => Order::class, 'subject_id' => $order->id, 'pipeline' => 'restart', 'status' => ProvisioningRun::STATUS_PENDING, 'current_step' => 0, // Everything the four steps read: instance_id is how CustomerStep // finds the machine, node and vmid are how it reaches 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('Restarting a customer instance.', [ 'instance' => $instance->uuid, 'restart_pending_since' => $instance->restart_required_since?->toIso8601String(), 'run' => $run->uuid, ]); return $run; } 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(); } }