instance($run); $node = (string) $run->context('node'); $vmid = (int) $run->context('vmid'); $fqdn = $instance->subdomain.'.'.ProvisioningSettings::dnsZone(); $pve = $this->pve->forHost($instance->host); // Terminal, and the only cert case that is: `cert_ok` is written by // ConfigureDnsAndTls, which fails the run itself if the platform // certificate never appears. Standing here without it means the pipeline // reached acceptance in a state it cannot reach, and no number of // attempts fixes that. if (! $instance->cert_ok) { return StepResult::fail('acceptance_failed:cert'); } // TLS + routing actually serving, asked of the outside world. if (! $this->traefik->certReachable($fqdn)) { return $this->askAgain('acceptance_failed:cert'); } // Nextcloud installed, not in maintenance, DB reachable. $status = $pve->guestExec($node, $vmid, NextcloudOcc::command('status --output=json')); $health = json_decode($status['out-data'] ?? '', true) ?: []; if ((int) ($status['exitcode'] ?? 1) !== 0 || ($health['installed'] ?? false) !== true || ($health['maintenance'] ?? false) !== false) { return $this->askAgain('acceptance_failed:nextcloud'); } // The admin account exists and is queryable. $admin = $pve->guestExec($node, $vmid, NextcloudOcc::command('user:info '.escapeshellarg((string) $instance->nc_admin_ref))); if ((int) ($admin['exitcode'] ?? 1) !== 0) { return $this->askAgain('acceptance_failed:admin'); } // Terminal for the same reason as cert_ok above: this is a breadcrumb THIS // run wrote at RegisterBackup, read out of our own database. If it is not // there, no backup job was registered, and asking again five times only // delays saying so. if (! $this->hasResource($run, 'backup_job_id')) { return StepResult::fail('acceptance_failed:backup'); } // Monitoring is a SECONDARY signal: it depends on an external poller, and // a freshly created monitor legitimately has no heartbeat yet. The four // checks above already prove the service itself works, so only gate on // monitoring when the operator declared it required — otherwise record it // and deliver (consistent with RegisterMonitoring's degrade-on-outage). $target = $instance->monitoringTargets()->first(); $monitoringGreen = $target !== null && $this->monitoring->isHealthy($target->external_id); if (! $monitoringGreen) { if (config('provisioning.monitoring.required', false)) { return StepResult::fail('acceptance_failed:monitoring'); } $run->events()->create([ 'step' => $this->key(), 'attempt' => $run->attempt, 'outcome' => 'info', 'message' => $target === null ? 'Abnahme ohne Monitoring: kein Monitor registriert.' : 'Abnahme ohne Monitoring-Bestätigung: Monitor noch nicht grün.', ]); } return StepResult::advance(); } /** * Ask the probes again, and keep the reason for the day they run out. * * retry(), not poll(): a poll does not consume the budget, so a genuinely * broken instance would sit here re-probing until the step's own maxDuration * turned it into a nameless timeout. A retry is bounded by max_attempts and * RunRunner hands this exact reason to failRun() when the last one is spent, * so an operator still reads `acceptance_failed:nextcloud` rather than * "step timed out". */ private function askAgain(string $reason): StepResult { return StepResult::retry(self::PROBE_RETRY_SECONDS, $reason); } }