feat: implement prerequisite validation and installation for server setup

This commit is contained in:
Andras Bacsai 2025-11-21 09:49:33 +01:00
parent ce4f8d02a2
commit 01957f2752
9 changed files with 218 additions and 32 deletions

View file

@ -59,8 +59,6 @@ public function handle(Server $server)
$command = collect([]); $command = collect([]);
if (isDev() && $server->id === 0) { if (isDev() && $server->id === 0) {
$command = $command->merge([ $command = $command->merge([
"echo 'Installing Prerequisites...'",
'sleep 1',
"echo 'Installing Docker Engine...'", "echo 'Installing Docker Engine...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
'sleep 4', 'sleep 4',
@ -70,35 +68,6 @@ public function handle(Server $server)
return remote_process($command, $server); return remote_process($command, $server);
} else { } else {
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'apt-get update -y',
'command -v curl >/dev/null || apt install -y curl',
'command -v wget >/dev/null || apt install -y wget',
'command -v git >/dev/null || apt install -y git',
'command -v jq >/dev/null || apt install -y jq',
]);
} elseif ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'command -v curl >/dev/null || dnf install -y curl',
'command -v wget >/dev/null || dnf install -y wget',
'command -v git >/dev/null || dnf install -y git',
'command -v jq >/dev/null || dnf install -y jq',
]);
} elseif ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'zypper update -y',
'command -v curl >/dev/null || zypper install -y curl',
'command -v wget >/dev/null || zypper install -y wget',
'command -v git >/dev/null || zypper install -y git',
'command -v jq >/dev/null || zypper install -y jq',
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([ $command = $command->merge([
"echo 'Installing Docker Engine...'", "echo 'Installing Docker Engine...'",
]); ]);

View file

@ -0,0 +1,57 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class InstallPrerequisites
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server)
{
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install prerequisites manually.');
}
$command = collect([]);
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'apt-get update -y',
'command -v curl >/dev/null || apt install -y curl',
'command -v wget >/dev/null || apt install -y wget',
'command -v git >/dev/null || apt install -y git',
'command -v jq >/dev/null || apt install -y jq',
]);
} elseif ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'command -v curl >/dev/null || dnf install -y curl',
'command -v wget >/dev/null || dnf install -y wget',
'command -v git >/dev/null || dnf install -y git',
'command -v jq >/dev/null || dnf install -y jq',
]);
} elseif ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'zypper update -y',
'command -v curl >/dev/null || zypper install -y curl',
'command -v wget >/dev/null || zypper install -y wget',
'command -v git >/dev/null || zypper install -y git',
'command -v jq >/dev/null || zypper install -y jq',
]);
} else {
throw new \Exception('Unsupported OS type for prerequisites installation');
}
$command->push("echo 'Prerequisites installed successfully.'");
return remote_process($command, $server);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidatePrerequisites
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server): bool
{
$requiredCommands = ['git', 'curl', 'jq'];
foreach ($requiredCommands as $cmd) {
$found = instant_remote_process(["command -v {$cmd}"], $server, false);
if (! $found) {
return false;
}
}
return true;
}
}

View file

@ -45,6 +45,15 @@ public function handle(Server $server)
throw new \Exception($this->error); throw new \Exception($this->error);
} }
$prerequisitesInstalled = $server->validatePrerequisites();
if (! $prerequisitesInstalled) {
$this->error = 'Prerequisites (git, curl, jq) are not installed. Please install them before continuing or use the validation with installation endpoint.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_installed = $server->validateDockerEngine(); $this->docker_installed = $server->validateDockerEngine();
$this->docker_compose_installed = $server->validateDockerCompose(); $this->docker_compose_installed = $server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) { if (! $this->docker_installed || ! $this->docker_compose_installed) {

View file

@ -72,6 +72,37 @@ public function handle(): void
return; return;
} }
// Check and install prerequisites
$prerequisitesInstalled = $this->server->validatePrerequisites();
if (! $prerequisitesInstalled) {
if ($this->numberOfTries >= $this->maxTries) {
$errorMessage = 'Prerequisites (git, curl, jq) could not be installed after '.$this->maxTries.' attempts. Please install them manually before continuing.';
$this->server->update([
'validation_logs' => $errorMessage,
'is_validating' => false,
]);
Log::error('ValidateAndInstallServer: Prerequisites installation failed after max tries', [
'server_id' => $this->server->id,
'attempts' => $this->numberOfTries,
]);
return;
}
Log::info('ValidateAndInstallServer: Installing prerequisites', [
'server_id' => $this->server->id,
'attempt' => $this->numberOfTries + 1,
]);
// Install prerequisites
$this->server->installPrerequisites();
// Retry validation after installation
self::dispatch($this->server, $this->numberOfTries + 1)->delay(now()->addSeconds(30));
return;
}
// Check if Docker is installed // Check if Docker is installed
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();
$dockerComposeInstalled = $this->server->validateDockerCompose(); $dockerComposeInstalled = $this->server->validateDockerCompose();

View file

@ -320,6 +320,21 @@ public function validateServer()
return handleError(error: $e, livewire: $this); return handleError(error: $e, livewire: $this);
} }
try {
// Check prerequisites
$prerequisitesInstalled = $this->createdServer->validatePrerequisites();
if (! $prerequisitesInstalled) {
$this->createdServer->installPrerequisites();
// Recheck after installation
$prerequisitesInstalled = $this->createdServer->validatePrerequisites();
if (! $prerequisitesInstalled) {
throw new \Exception('Prerequisites (git, curl, jq) could not be installed. Please install them manually.');
}
}
} catch (\Throwable $e) {
return handleError(error: $e, livewire: $this);
}
try { try {
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true); $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);

View file

@ -25,6 +25,8 @@ class ValidateAndInstall extends Component
public $supported_os_type = null; public $supported_os_type = null;
public $prerequisites_installed = null;
public $docker_installed = null; public $docker_installed = null;
public $docker_compose_installed = null; public $docker_compose_installed = null;
@ -33,12 +35,15 @@ class ValidateAndInstall extends Component
public $error = null; public $error = null;
public string $installationStep = 'Prerequisites';
public bool $ask = false; public bool $ask = false;
protected $listeners = [ protected $listeners = [
'init', 'init',
'validateConnection', 'validateConnection',
'validateOS', 'validateOS',
'validatePrerequisites',
'validateDockerEngine', 'validateDockerEngine',
'validateDockerVersion', 'validateDockerVersion',
'refresh' => '$refresh', 'refresh' => '$refresh',
@ -48,6 +53,7 @@ public function init(int $data = 0)
{ {
$this->uptime = null; $this->uptime = null;
$this->supported_os_type = null; $this->supported_os_type = null;
$this->prerequisites_installed = null;
$this->docker_installed = null; $this->docker_installed = null;
$this->docker_version = null; $this->docker_version = null;
$this->docker_compose_installed = null; $this->docker_compose_installed = null;
@ -69,6 +75,7 @@ public function retry()
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->uptime = null; $this->uptime = null;
$this->supported_os_type = null; $this->supported_os_type = null;
$this->prerequisites_installed = null;
$this->docker_installed = null; $this->docker_installed = null;
$this->docker_compose_installed = null; $this->docker_compose_installed = null;
$this->docker_version = null; $this->docker_version = null;
@ -103,6 +110,40 @@ public function validateOS()
return; return;
} }
$this->dispatch('validatePrerequisites');
}
public function validatePrerequisites()
{
$this->prerequisites_installed = $this->server->validatePrerequisites();
if (! $this->prerequisites_installed) {
if ($this->install) {
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Prerequisites (git, curl, jq) could not be installed. Please install them manually before continuing.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
} else {
if ($this->number_of_tries <= $this->max_tries) {
$this->installationStep = 'Prerequisites';
$activity = $this->server->installPrerequisites();
$this->number_of_tries++;
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries);
}
return;
}
} else {
$this->error = 'Prerequisites (git, curl, jq) are not installed. Please install them before continuing.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
}
}
$this->dispatch('validateDockerEngine'); $this->dispatch('validateDockerEngine');
} }
@ -121,6 +162,7 @@ public function validateDockerEngine()
return; return;
} else { } else {
if ($this->number_of_tries <= $this->max_tries) { if ($this->number_of_tries <= $this->max_tries) {
$this->installationStep = 'Docker';
$activity = $this->server->installDocker(); $activity = $this->server->installDocker();
$this->number_of_tries++; $this->number_of_tries++;
$this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries); $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries);

View file

@ -4,7 +4,9 @@
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Actions\Server\InstallPrerequisites;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Actions\Server\ValidatePrerequisites;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Events\ServerReachabilityChanged; use App\Events\ServerReachabilityChanged;
use App\Helpers\SslHelper; use App\Helpers\SslHelper;
@ -1184,6 +1186,16 @@ public function installDocker()
return InstallDocker::run($this); return InstallDocker::run($this);
} }
public function validatePrerequisites(): bool
{
return ValidatePrerequisites::run($this);
}
public function installPrerequisites()
{
return InstallPrerequisites::run($this);
}
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
{ {
$dockerBinary = instant_remote_process(['command -v docker'], $this, false, no_sudo: true); $dockerBinary = instant_remote_process(['command -v docker'], $this, false, no_sudo: true);

View file

@ -52,6 +52,30 @@
@endif @endif
@endif @endif
@if ($uptime && $supported_os_type) @if ($uptime && $supported_os_type)
@if ($prerequisites_installed)
<div class="flex w-64 gap-2">Prerequisites are installed: <svg class="w-5 h-5 text-success"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Prerequisites are installed: <svg class="w-5 h-5 text-error"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Prerequisites are installed:" /></div>
@endif
@endif
@endif
@if ($uptime && $supported_os_type && $prerequisites_installed)
@if ($docker_installed) @if ($docker_installed)
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-success" <div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-success"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
@ -120,7 +144,7 @@
@endif @endif
@endif @endif
<livewire:activity-monitor header="Docker Installation Logs" :showWaiting="false" /> <livewire:activity-monitor header="{{ $installationStep }} Installation Logs" :showWaiting="false" />
@isset($error) @isset($error)
<pre class="font-bold whitespace-pre-line text-error">{!! $error !!}</pre> <pre class="font-bold whitespace-pre-line text-error">{!! $error !!}</pre>
<x-forms.button canGate="update" :canResource="$server" wire:click="retry" class="mt-4"> <x-forms.button canGate="update" :canResource="$server" wire:click="retry" class="mt-4">