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

84 lines
3.4 KiB
PHP

<?php
namespace App\Provisioning\Steps\Customer;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Proxmox\ProxmoxClient;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
class DeployApplicationStack extends CustomerStep
{
/** How long the stack has to come up and report itself installed. */
private const READY_DEADLINE = 1080;
public function __construct(private ProxmoxClient $pve) {}
public function key(): string
{
return 'deploy_application_stack';
}
public function maxDuration(): int
{
// Above READY_DEADLINE, so the step's own named failure fires first.
return 1200;
}
public function execute(ProvisioningRun $run): StepResult
{
$instance = $this->instance($run);
$node = (string) $run->context('node');
$vmid = (int) $run->context('vmid');
$pve = $this->pve->forHost($instance->host);
// Deploy once (guarded). The DB password is persisted ENCRYPTED before the
// stack starts and reused on retry, so a crash before `stack_deployed`
// can't regenerate a password that mismatches the initialized database.
if (! $run->context('stack_deployed')) {
$encrypted = $run->context('db_password');
$dbPassword = $encrypted !== null ? Crypt::decryptString($encrypted) : Str::random(28);
if ($encrypted === null) {
$run->mergeContext(['db_password' => Crypt::encryptString($dbPassword)]);
}
$this->guest($pve, $run,
'install -d /opt/nextcloud && printf %s '.escapeshellarg('MYSQL_PASSWORD='.$dbPassword."\n").
' > /opt/nextcloud/.env');
$this->guest($pve, $run, 'cd /opt/nextcloud && docker compose up -d');
$run->mergeContext(['stack_deployed' => true]);
}
// Poll until Nextcloud is INSTALLED, not merely answering (poll doesn't
// consume the retry budget).
//
// `curl -sf` alone was the whole readiness test, and status.php answers
// 200 on an instance that has never been installed — the body then reads
// {"installed":false,…}. So the run marched on into `occ config:system:set`
// and `occ user:add`, neither of which is even REGISTERED as a command on
// an uninstalled Nextcloud, and the failure surfaced three steps later as
// an unexplained non-zero exit inside the guest. The flag is the one thing
// status.php says that distinguishes a web server from a Nextcloud.
$health = $pve->guestExec($node, $vmid,
'curl -sf http://localhost/status.php 2>/dev/null'
.' | grep -qE \'"installed"[[:space:]]*:[[:space:]]*true\' && echo ok || echo wait');
if (trim($health['out-data'] ?? '') !== 'ok') {
// Its own deadline, below maxDuration, so what an operator reads is
// what happened rather than 'step timed out'. Long: the first boot
// pulls the images, and a slow mirror is not a fault.
if ($run->started_at !== null && $run->started_at->copy()->addSeconds(self::READY_DEADLINE)->isPast()) {
return StepResult::fail('nextcloud_not_installed');
}
return StepResult::poll(15, 'waiting for Nextcloud to report itself installed');
}
$run->forgetContext('db_password'); // scrub once the stack is up
return StepResult::advance();
}
}