From 01957f2752b7ea03e413a0884e185f6fec82fa11 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:49:33 +0100 Subject: [PATCH 1/3] feat: implement prerequisite validation and installation for server setup --- app/Actions/Server/InstallDocker.php | 31 ---------- app/Actions/Server/InstallPrerequisites.php | 57 +++++++++++++++++++ app/Actions/Server/ValidatePrerequisites.php | 27 +++++++++ app/Actions/Server/ValidateServer.php | 9 +++ app/Jobs/ValidateAndInstallServerJob.php | 31 ++++++++++ app/Livewire/Boarding/Index.php | 15 +++++ app/Livewire/Server/ValidateAndInstall.php | 42 ++++++++++++++ app/Models/Server.php | 12 ++++ .../server/validate-and-install.blade.php | 26 ++++++++- 9 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 app/Actions/Server/InstallPrerequisites.php create mode 100644 app/Actions/Server/ValidatePrerequisites.php diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 92dd7e8c3..36c540950 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -59,8 +59,6 @@ public function handle(Server $server) $command = collect([]); if (isDev() && $server->id === 0) { $command = $command->merge([ - "echo 'Installing Prerequisites...'", - 'sleep 1', "echo 'Installing Docker Engine...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'", 'sleep 4', @@ -70,35 +68,6 @@ public function handle(Server $server) return remote_process($command, $server); } 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([ "echo 'Installing Docker Engine...'", ]); diff --git a/app/Actions/Server/InstallPrerequisites.php b/app/Actions/Server/InstallPrerequisites.php new file mode 100644 index 000000000..1a7d3bbd9 --- /dev/null +++ b/app/Actions/Server/InstallPrerequisites.php @@ -0,0 +1,57 @@ +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); + } +} diff --git a/app/Actions/Server/ValidatePrerequisites.php b/app/Actions/Server/ValidatePrerequisites.php new file mode 100644 index 000000000..f74727112 --- /dev/null +++ b/app/Actions/Server/ValidatePrerequisites.php @@ -0,0 +1,27 @@ +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_compose_installed = $server->validateDockerCompose(); if (! $this->docker_installed || ! $this->docker_compose_installed) { diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php index 388791f10..a6dcd62f1 100644 --- a/app/Jobs/ValidateAndInstallServerJob.php +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -72,6 +72,37 @@ public function handle(): void 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 $dockerInstalled = $this->server->validateDockerEngine(); $dockerComposeInstalled = $this->server->validateDockerCompose(); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 9f1eac4d2..dfddd7f68 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -320,6 +320,21 @@ public function validateServer() 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 { $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index bbd7f3dd9..687eadd48 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -25,6 +25,8 @@ class ValidateAndInstall extends Component public $supported_os_type = null; + public $prerequisites_installed = null; + public $docker_installed = null; public $docker_compose_installed = null; @@ -33,12 +35,15 @@ class ValidateAndInstall extends Component public $error = null; + public string $installationStep = 'Prerequisites'; + public bool $ask = false; protected $listeners = [ 'init', 'validateConnection', 'validateOS', + 'validatePrerequisites', 'validateDockerEngine', 'validateDockerVersion', 'refresh' => '$refresh', @@ -48,6 +53,7 @@ public function init(int $data = 0) { $this->uptime = null; $this->supported_os_type = null; + $this->prerequisites_installed = null; $this->docker_installed = null; $this->docker_version = null; $this->docker_compose_installed = null; @@ -69,6 +75,7 @@ public function retry() $this->authorize('update', $this->server); $this->uptime = null; $this->supported_os_type = null; + $this->prerequisites_installed = null; $this->docker_installed = null; $this->docker_compose_installed = null; $this->docker_version = null; @@ -103,6 +110,40 @@ public function validateOS() 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'); } @@ -121,6 +162,7 @@ public function validateDockerEngine() return; } else { if ($this->number_of_tries <= $this->max_tries) { + $this->installationStep = 'Docker'; $activity = $this->server->installDocker(); $this->number_of_tries++; $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries); diff --git a/app/Models/Server.php b/app/Models/Server.php index e88af2b15..9210e801b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -4,7 +4,9 @@ use App\Actions\Proxy\StartProxy; use App\Actions\Server\InstallDocker; +use App\Actions\Server\InstallPrerequisites; use App\Actions\Server\StartSentinel; +use App\Actions\Server\ValidatePrerequisites; use App\Enums\ProxyTypes; use App\Events\ServerReachabilityChanged; use App\Helpers\SslHelper; @@ -1184,6 +1186,16 @@ public function installDocker() return InstallDocker::run($this); } + public function validatePrerequisites(): bool + { + return ValidatePrerequisites::run($this); + } + + public function installPrerequisites() + { + return InstallPrerequisites::run($this); + } + public function validateDockerEngine($throwError = false) { $dockerBinary = instant_remote_process(['command -v docker'], $this, false, no_sudo: true); diff --git a/resources/views/livewire/server/validate-and-install.blade.php b/resources/views/livewire/server/validate-and-install.blade.php index 572da85e8..85ea3105e 100644 --- a/resources/views/livewire/server/validate-and-install.blade.php +++ b/resources/views/livewire/server/validate-and-install.blade.php @@ -52,6 +52,30 @@ @endif @endif @if ($uptime && $supported_os_type) + @if ($prerequisites_installed) +
Prerequisites are installed: + + + + +
+ @else + @if ($error) +
Prerequisites are installed: + +
+ @else +
+ @endif + @endif + @endif + @if ($uptime && $supported_os_type && $prerequisites_installed) @if ($docker_installed)
Docker is installed: @@ -120,7 +144,7 @@ @endif @endif - + @isset($error)
{!! $error !!}
From 29135e00baf6b067f2a1098cb265c9640fc45679 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:14:48 +0100 Subject: [PATCH 2/3] feat: enhance prerequisite validation to return detailed results --- app/Actions/Server/ValidatePrerequisites.php | 23 ++++++++-- app/Actions/Server/ValidateServer.php | 7 +-- app/Jobs/ValidateAndInstallServerJob.php | 11 +++-- app/Livewire/Boarding/Index.php | 11 +++-- app/Livewire/Server/ValidateAndInstall.php | 11 +++-- app/Models/Server.php | 7 ++- .../Server/ValidatePrerequisitesTest.php | 46 +++++++++++++++++++ 7 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 tests/Unit/Actions/Server/ValidatePrerequisitesTest.php diff --git a/app/Actions/Server/ValidatePrerequisites.php b/app/Actions/Server/ValidatePrerequisites.php index f74727112..23c1db1d0 100644 --- a/app/Actions/Server/ValidatePrerequisites.php +++ b/app/Actions/Server/ValidatePrerequisites.php @@ -11,17 +11,30 @@ class ValidatePrerequisites public string $jobQueue = 'high'; - public function handle(Server $server): bool + /** + * Validate that required commands are available on the server. + * + * @return array{success: bool, missing: array, found: array} + */ + public function handle(Server $server): array { $requiredCommands = ['git', 'curl', 'jq']; + $missing = []; + $found = []; foreach ($requiredCommands as $cmd) { - $found = instant_remote_process(["command -v {$cmd}"], $server, false); - if (! $found) { - return false; + $result = instant_remote_process(["command -v {$cmd}"], $server, false); + if (! $result) { + $missing[] = $cmd; + } else { + $found[] = $cmd; } } - return true; + return [ + 'success' => empty($missing), + 'missing' => $missing, + 'found' => $found, + ]; } } diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php index a4840b194..0a20deae5 100644 --- a/app/Actions/Server/ValidateServer.php +++ b/app/Actions/Server/ValidateServer.php @@ -45,9 +45,10 @@ public function handle(Server $server) 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.'; + $validationResult = $server->validatePrerequisites(); + if (! $validationResult['success']) { + $missingCommands = implode(', ', $validationResult['missing']); + $this->error = "Prerequisites ({$missingCommands}) are not installed. Please install them before continuing or use the validation with installation endpoint."; $server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php index a6dcd62f1..ff5c2e4f5 100644 --- a/app/Jobs/ValidateAndInstallServerJob.php +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -73,10 +73,11 @@ public function handle(): void } // Check and install prerequisites - $prerequisitesInstalled = $this->server->validatePrerequisites(); - if (! $prerequisitesInstalled) { + $validationResult = $this->server->validatePrerequisites(); + if (! $validationResult['success']) { if ($this->numberOfTries >= $this->maxTries) { - $errorMessage = 'Prerequisites (git, curl, jq) could not be installed after '.$this->maxTries.' attempts. Please install them manually before continuing.'; + $missingCommands = implode(', ', $validationResult['missing']); + $errorMessage = "Prerequisites ({$missingCommands}) could not be installed after {$this->maxTries} attempts. Please install them manually before continuing."; $this->server->update([ 'validation_logs' => $errorMessage, 'is_validating' => false, @@ -84,6 +85,8 @@ public function handle(): void Log::error('ValidateAndInstallServer: Prerequisites installation failed after max tries', [ 'server_id' => $this->server->id, 'attempts' => $this->numberOfTries, + 'missing_commands' => $validationResult['missing'], + 'found_commands' => $validationResult['found'], ]); return; @@ -92,6 +95,8 @@ public function handle(): void Log::info('ValidateAndInstallServer: Installing prerequisites', [ 'server_id' => $this->server->id, 'attempt' => $this->numberOfTries + 1, + 'missing_commands' => $validationResult['missing'], + 'found_commands' => $validationResult['found'], ]); // Install prerequisites diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index dfddd7f68..25a2fd694 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -322,13 +322,14 @@ public function validateServer() try { // Check prerequisites - $prerequisitesInstalled = $this->createdServer->validatePrerequisites(); - if (! $prerequisitesInstalled) { + $validationResult = $this->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { $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.'); + $validationResult = $this->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + $missingCommands = implode(', ', $validationResult['missing']); + throw new \Exception("Prerequisites ({$missingCommands}) could not be installed. Please install them manually."); } } } catch (\Throwable $e) { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index 687eadd48..d2e45ded2 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -115,11 +115,13 @@ public function validateOS() public function validatePrerequisites() { - $this->prerequisites_installed = $this->server->validatePrerequisites(); - if (! $this->prerequisites_installed) { + $validationResult = $this->server->validatePrerequisites(); + $this->prerequisites_installed = $validationResult['success']; + if (! $validationResult['success']) { 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.'; + $missingCommands = implode(', ', $validationResult['missing']); + $this->error = "Prerequisites ({$missingCommands}) could not be installed. Please install them manually before continuing."; $this->server->update([ 'validation_logs' => $this->error, ]); @@ -136,7 +138,8 @@ public function validatePrerequisites() return; } } else { - $this->error = 'Prerequisites (git, curl, jq) are not installed. Please install them before continuing.'; + $missingCommands = implode(', ', $validationResult['missing']); + $this->error = "Prerequisites ({$missingCommands}) are not installed. Please install them before continuing."; $this->server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Models/Server.php b/app/Models/Server.php index 9210e801b..8b153c8ac 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1186,7 +1186,12 @@ public function installDocker() return InstallDocker::run($this); } - public function validatePrerequisites(): bool + /** + * Validate that required commands are available on the server. + * + * @return array{success: bool, missing: array, found: array} + */ + public function validatePrerequisites(): array { return ValidatePrerequisites::run($this); } diff --git a/tests/Unit/Actions/Server/ValidatePrerequisitesTest.php b/tests/Unit/Actions/Server/ValidatePrerequisitesTest.php new file mode 100644 index 000000000..8db6815d6 --- /dev/null +++ b/tests/Unit/Actions/Server/ValidatePrerequisitesTest.php @@ -0,0 +1,46 @@ +toBeTrue() + ->and('ValidatePrerequisites should return array with keys: '.implode(', ', $expectedKeys)) + ->toBeString(); +}); + +it('validates required commands list', function () { + // Verify the action checks for the correct prerequisites + $requiredCommands = ['git', 'curl', 'jq']; + + expect($requiredCommands)->toHaveCount(3) + ->and($requiredCommands)->toContain('git') + ->and($requiredCommands)->toContain('curl') + ->and($requiredCommands)->toContain('jq'); +}); + +it('return structure has correct types', function () { + // Verify the expected return structure types + $expectedStructure = [ + 'success' => 'boolean', + 'missing' => 'array', + 'found' => 'array', + ]; + + expect($expectedStructure['success'])->toBe('boolean') + ->and($expectedStructure['missing'])->toBe('array') + ->and($expectedStructure['found'])->toBe('array'); +}); From 30d206e7b9ed1501c7726d6e30fec22f49969228 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:44:04 +0100 Subject: [PATCH 3/3] feat: add async prerequisite installation with retry logic and visual feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit enhances the boarding flow to handle prerequisite installation asynchronously with proper retry logic and user feedback: - Add retry mechanism with max 3 attempts for prerequisite installation - Display live installation logs via ActivityMonitor during boarding - Reset ActivityMonitor state when starting new activity to prevent stale event dispatching - Support dynamic header updates in ActivityMonitor - Add prerequisitesInstalled event handler to revalidate after installation completes - Extract validation logic into continueValidation() method for cleaner flow - Add unit tests for prerequisite installation logic This improves UX by showing users real-time progress during prerequisite installation and handles installation failures gracefully with automatic retries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/ActivityMonitor.php | 10 +- app/Livewire/Boarding/Index.php | 59 +++++- app/Livewire/Server/ValidateAndInstall.php | 4 +- .../views/livewire/activity-monitor.blade.php | 2 +- .../views/livewire/boarding/index.blade.php | 7 + .../Livewire/BoardingPrerequisitesTest.php | 181 ++++++++++++++++++ 6 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/Livewire/BoardingPrerequisitesTest.php diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index 54034ef7a..d01b55afb 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -28,12 +28,20 @@ class ActivityMonitor extends Component protected $listeners = ['activityMonitor' => 'newMonitorActivity']; - public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null) + public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null, $header = null) { + // Reset event dispatched flag for new activity + self::$eventDispatched = false; + $this->activityId = $activityId; $this->eventToDispatch = $eventToDispatch; $this->eventData = $eventData; + // Update header if provided + if ($header !== null) { + $this->header = $header; + } + $this->hydrateActivity(); $this->isPollingActive = true; diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 25a2fd694..ab1a1aae9 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -14,7 +14,10 @@ class Index extends Component { - protected $listeners = ['refreshBoardingIndex' => 'validateServer']; + protected $listeners = [ + 'refreshBoardingIndex' => 'validateServer', + 'prerequisitesInstalled' => 'handlePrerequisitesInstalled', + ]; #[\Livewire\Attributes\Url(as: 'step', history: true)] public string $currentState = 'welcome'; @@ -76,6 +79,10 @@ class Index extends Component public ?string $minDockerVersion = null; + public int $prerequisiteInstallAttempts = 0; + + public int $maxPrerequisiteInstallAttempts = 3; + public function mount() { if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { @@ -324,18 +331,58 @@ public function validateServer() // Check prerequisites $validationResult = $this->createdServer->validatePrerequisites(); if (! $validationResult['success']) { - $this->createdServer->installPrerequisites(); - // Recheck after installation - $validationResult = $this->createdServer->validatePrerequisites(); - if (! $validationResult['success']) { + // Check if we've exceeded max attempts + if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) { $missingCommands = implode(', ', $validationResult['missing']); - throw new \Exception("Prerequisites ({$missingCommands}) could not be installed. Please install them manually."); + throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually."); } + + // Start async installation and wait for completion via ActivityMonitor + $activity = $this->createdServer->installPrerequisites(); + $this->prerequisiteInstallAttempts++; + $this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled'); + + // Return early - handlePrerequisitesInstalled() will be called when installation completes + return; } + + // Prerequisites are already installed, continue with validation + $this->continueValidation(); } catch (\Throwable $e) { return handleError(error: $e, livewire: $this); } + } + public function handlePrerequisitesInstalled() + { + try { + // Revalidate prerequisites after installation completes + $validationResult = $this->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + // Installation completed but prerequisites still missing - retry + $missingCommands = implode(', ', $validationResult['missing']); + + if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) { + throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually."); + } + + // Try again + $activity = $this->createdServer->installPrerequisites(); + $this->prerequisiteInstallAttempts++; + $this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled'); + + return; + } + + // Prerequisites validated successfully - continue with Docker validation + $this->continueValidation(); + } catch (\Throwable $e) { + return handleError(error: $e, livewire: $this); + } + } + + private function continueValidation() + { try { $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index d2e45ded2..c2dcd877b 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -132,7 +132,7 @@ public function validatePrerequisites() $this->installationStep = 'Prerequisites'; $activity = $this->server->installPrerequisites(); $this->number_of_tries++; - $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries); + $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } return; @@ -168,7 +168,7 @@ public function validateDockerEngine() $this->installationStep = 'Docker'; $activity = $this->server->installDocker(); $this->number_of_tries++; - $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries); + $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } return; diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php index 83f1ee5e3..386d8622d 100644 --- a/resources/views/livewire/activity-monitor.blade.php +++ b/resources/views/livewire/activity-monitor.blade.php @@ -5,7 +5,7 @@ ])> @if ($activity) @if (isset($header)) -
+

{{ $header }}

@if ($isPollingActive) diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 81873c892..ec344e552 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -546,6 +546,13 @@ class="p-6 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-2
+ @if ($prerequisiteInstallAttempts > 0) +
+

Installing Prerequisites

+ +
+ @endif + Server Validation diff --git a/tests/Unit/Livewire/BoardingPrerequisitesTest.php b/tests/Unit/Livewire/BoardingPrerequisitesTest.php new file mode 100644 index 000000000..180a274d2 --- /dev/null +++ b/tests/Unit/Livewire/BoardingPrerequisitesTest.php @@ -0,0 +1,181 @@ +makePartial(); + $server->shouldReceive('validatePrerequisites') + ->andReturn([ + 'success' => false, + 'missing' => ['git'], + 'found' => ['curl', 'jq'], + ]); + + $activity = Mockery::mock(Activity::class); + $activity->id = 'test-activity-123'; + $server->shouldReceive('installPrerequisites') + ->once() + ->andReturn($activity); + + $component = Mockery::mock(Index::class)->makePartial(); + $component->createdServer = $server; + $component->prerequisiteInstallAttempts = 0; + $component->maxPrerequisiteInstallAttempts = 3; + + // Key assertion: verify activityMonitor event is dispatched with correct params + $component->shouldReceive('dispatch') + ->once() + ->with('activityMonitor', 'test-activity-123', 'prerequisitesInstalled') + ->andReturnSelf(); + + // Invoke the prerequisite check logic (simulating what validateServer does) + $validationResult = $component->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + if ($component->prerequisiteInstallAttempts >= $component->maxPrerequisiteInstallAttempts) { + throw new Exception('Max attempts exceeded'); + } + $activity = $component->createdServer->installPrerequisites(); + $component->prerequisiteInstallAttempts++; + $component->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled'); + } + + expect($component->prerequisiteInstallAttempts)->toBe(1); +}); + +it('does not retry when prerequisites install successfully', function () { + // This test verifies the callback behavior when installation succeeds. + + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('validatePrerequisites') + ->andReturn([ + 'success' => true, + 'missing' => [], + 'found' => ['git', 'curl', 'jq'], + ]); + + // installPrerequisites should NOT be called again + $server->shouldNotReceive('installPrerequisites'); + + $component = Mockery::mock(Index::class)->makePartial(); + $component->createdServer = $server; + $component->prerequisiteInstallAttempts = 1; + $component->maxPrerequisiteInstallAttempts = 3; + + // Simulate the callback logic + $validationResult = $component->createdServer->validatePrerequisites(); + if ($validationResult['success']) { + // Prerequisites are now valid, we'd call continueValidation() + // For the test, just verify we don't try to install again + expect($validationResult['success'])->toBeTrue(); + } +}); + +it('retries when prerequisites still missing after callback', function () { + // This test verifies retry logic in the callback. + + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('validatePrerequisites') + ->andReturn([ + 'success' => false, + 'missing' => ['git'], + 'found' => ['curl', 'jq'], + ]); + + $activity = Mockery::mock(Activity::class); + $activity->id = 'retry-activity-456'; + $server->shouldReceive('installPrerequisites') + ->once() + ->andReturn($activity); + + $component = Mockery::mock(Index::class)->makePartial(); + $component->createdServer = $server; + $component->prerequisiteInstallAttempts = 1; // Already tried once + $component->maxPrerequisiteInstallAttempts = 3; + + $component->shouldReceive('dispatch') + ->once() + ->with('activityMonitor', 'retry-activity-456', 'prerequisitesInstalled') + ->andReturnSelf(); + + // Simulate callback logic + $validationResult = $component->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + if ($component->prerequisiteInstallAttempts < $component->maxPrerequisiteInstallAttempts) { + $activity = $component->createdServer->installPrerequisites(); + $component->prerequisiteInstallAttempts++; + $component->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled'); + } + } + + expect($component->prerequisiteInstallAttempts)->toBe(2); +}); + +it('throws exception when max attempts exceeded', function () { + // This test verifies that we stop retrying after max attempts. + + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('validatePrerequisites') + ->andReturn([ + 'success' => false, + 'missing' => ['git', 'curl'], + 'found' => ['jq'], + ]); + + // installPrerequisites should NOT be called when at max attempts + $server->shouldNotReceive('installPrerequisites'); + + $component = Mockery::mock(Index::class)->makePartial(); + $component->createdServer = $server; + $component->prerequisiteInstallAttempts = 3; // Already at max + $component->maxPrerequisiteInstallAttempts = 3; + + // Simulate callback logic - should throw exception + $validationResult = $component->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + if ($component->prerequisiteInstallAttempts >= $component->maxPrerequisiteInstallAttempts) { + $missingCommands = implode(', ', $validationResult['missing']); + throw new Exception("Prerequisites ({$missingCommands}) could not be installed after {$component->maxPrerequisiteInstallAttempts} attempts."); + } + } +})->throws(Exception::class, 'Prerequisites (git, curl) could not be installed after 3 attempts'); + +it('does not install when prerequisites already present', function () { + // This test verifies we skip installation when everything is already installed. + + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('validatePrerequisites') + ->andReturn([ + 'success' => true, + 'missing' => [], + 'found' => ['git', 'curl', 'jq'], + ]); + + // installPrerequisites should NOT be called + $server->shouldNotReceive('installPrerequisites'); + + $component = Mockery::mock(Index::class)->makePartial(); + $component->createdServer = $server; + $component->prerequisiteInstallAttempts = 0; + $component->maxPrerequisiteInstallAttempts = 3; + + // Simulate validation logic + $validationResult = $component->createdServer->validatePrerequisites(); + if (! $validationResult['success']) { + // Should not reach here + $component->prerequisiteInstallAttempts++; + } + + // Attempts should remain 0 + expect($component->prerequisiteInstallAttempts)->toBe(0); + expect($validationResult['success'])->toBeTrue(); +});