fix: better server validation and installation process

fix: add destination to queue deployment
feat: force start deployment
This commit is contained in:
Andras Bacsai
2024-02-05 14:40:54 +01:00
parent 0c3ed3d393
commit 49f5240ff8
34 changed files with 443 additions and 184 deletions

View File

@@ -43,6 +43,7 @@ class InstallDocker
"echo 'Restarting Docker Engine...'",
"ls -l /tmp"
]);
return remote_process($command, $server);
} else {
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
@@ -89,7 +90,6 @@ class InstallDocker
"echo 'Done!'",
]);
}
return remote_process($command, $server);
}
}

View File

@@ -122,7 +122,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = Server::find($this->application_deployment_queue->server_id);
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
@@ -561,12 +562,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
// }
}
private function deploy_nixpacks_buildpack()
{
@@ -791,7 +787,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_to_additional_destinations()
{
if (str($this->application->additional_destinations)->isEmpty()) {
return;
}
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return;
}
if ($destination_ids->contains($this->destination->id)) {
ray('Same destination found in additional destinations. Skipping.');
return;
}
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
@@ -799,11 +806,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
continue;
}
$this->server = $server;
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}.");
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
server: $server,
destination: $destination,
no_questions_asked: true,
);
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'),
]));
}
}
private function set_base_dir()
@@ -1507,11 +1524,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => $status,
]);
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}

View File

@@ -15,7 +15,7 @@ class ActivityMonitor extends Component
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newMonitorActivity'];
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{

View File

@@ -12,6 +12,7 @@ use Livewire\Component;
class Index extends Component
{
protected $listeners = ['serverInstalled' => 'validateServer'];
public string $currentState = 'welcome';
public ?string $selectedServerType = null;
@@ -93,7 +94,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if (isDev()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
} else {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
@@ -190,6 +195,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->addInitialNetwork();
$this->validateServer();
}
public function installServer()
{
$this->dispatch('validateServer', true);
}
public function validateServer()
{
try {
@@ -228,7 +237,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->dispatch('installDocker');
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class NewActivityMonitor extends Component
{
public ?string $header = null;
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity();
$this->isPollingActive = true;
}
public function hydrateActivity()
{
$this->activity = Activity::find($this->activityId);
}
public function polling()
{
$this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach ($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
ray('Dispatched event: ' . $this->eventToDispatch);
}
}
}
}

View File

@@ -7,8 +7,6 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Livewire\Component;
class DeploymentNavbar extends Component
@@ -37,7 +35,15 @@ class DeploymentNavbar extends Component
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue');
}
public function force_start()
{
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
}
}
public function cancel()
{
try {
@@ -67,7 +73,6 @@ class DeploymentNavbar extends Component
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// queue_next_deployment($this->application);
}
}
}

View File

@@ -46,26 +46,6 @@ class Heading extends Component
$this->deploy(force_rebuild: true);
}
public function deployNew()
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
public function deploy(bool $force_rebuild = false)
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {

View File

@@ -58,19 +58,19 @@ class Heading extends Component
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
}
}

View File

@@ -129,7 +129,7 @@ class Import extends Component
if (!empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
} catch (\Throwable $e) {
$this->validated = false;

View File

@@ -57,7 +57,7 @@ class Navbar extends Component
}
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
public function stop(bool $forceCleanup = false)
{
@@ -82,6 +82,6 @@ class Navbar extends Component
StopService::run($this->service);
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Application extends Component
class ServiceApplicationView extends Component
{
public ServiceApplication $application;
public $parameters;
@@ -20,7 +20,7 @@ class Application extends Component
];
public function render()
{
return view('livewire.project.service.application');
return view('livewire.project.service.service-application-view');
}
public function instantSave()
{

View File

@@ -115,7 +115,7 @@ class ExecuteContainerCommand extends Component
$exec = "docker exec {$this->container} {$cmd}";
}
$activity = remote_process([$exec], $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -31,7 +31,7 @@ class RunCommand extends Component
$this->validate();
try {
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server;
use Livewire\Component;
@@ -14,7 +13,8 @@ class Form extends Component
public ?string $wildcard_domain = null;
public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false;
protected $listeners = ['serverRefresh'];
protected $listeners = ['serverInstalled'];
protected $rules = [
'server.name' => 'required',
@@ -49,9 +49,10 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function serverRefresh($install = true)
public function serverInstalled()
{
$this->validateServer($install);
$this->server->refresh();
$this->server->settings->refresh();
}
public function instantSave()
{
@@ -64,13 +65,6 @@ class Form extends Component
return handleError($e, $this);
}
}
public function installDocker()
{
$this->dispatch('installDocker');
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id);
}
public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection();
@@ -80,48 +74,13 @@ class Form extends Component
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
}
public function validateServer($install = true)
{
try {
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->dispatch('success', 'Docker Engine is installed.<br> Checking version.');
} else {
$install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->dispatch('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
}
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('proxyStatusUpdated');
}
$this->dispatch('validateServer', $install);
}
public function submit()

View File

@@ -71,7 +71,7 @@ class Deploy extends Component
{
try {
$activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -12,10 +12,8 @@ class Status extends Component
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
}
public function startProxyPolling()
{
$this->checkProxy();

View File

@@ -11,7 +11,7 @@ class Show extends Component
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated' => '$refresh'];
protected $listeners = ['serverInstalled' => '$refresh'];
public function mount()
{
$this->parameters = get_route_parameters();

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class ValidateAndInstall extends Component
{
public Server $server;
public int $number_of_tries = 0;
public int $max_tries = 1;
public bool $install = true;
public $uptime = null;
public $supported_os_type = null;
public $docker_installed = null;
public $docker_version = null;
public $error = null;
protected $listeners = ['validateServer', 'validateDockerEngine'];
public function validateServer(bool $install = true)
{
$this->install = $install;
$this->uptime = null;
$this->supported_os_type = null;
$this->docker_installed = null;
$this->docker_version = null;
try {
$this->validateConnection();
$this->validateOS();
$this->validateDockerEngine();
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function validateConnection()
{
$this->uptime = $this->server->validateConnection();
if (!$this->uptime) {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
}
public function validateOS()
{
$this->supported_os_type = $this->server->validateOS();
if (!$this->supported_os_type) {
$this->dispatch('error', 'Server OS type is not supported.', 'Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
}
public function validateDockerEngine()
{
$this->docker_installed = $this->server->validateDockerEngine();
if (!$this->docker_installed) {
if ($this->install) {
ray($this->number_of_tries, $this->max_tries);
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
} else {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
return;
}
} else {
$this->dispatch('error', 'Docker Engine is not installed.', 'Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
} else {
$this->validateDockerVersion();
}
}
public function validateDockerVersion()
{
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated successfully.');
} else {
$this->dispatch('error', 'Docker Engine version is not 22+.', 'Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
}
public function render()
{
return view('livewire.server.validate-and-install');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
@@ -411,6 +412,11 @@ class Server extends BaseModel
return true;
}
public function installDocker()
{
$activity = InstallDocker::run($this);
return $activity;
}
public function validateDockerEngine($throwError = false)
{
$dockerBinary = instant_remote_process(["command -v docker"], $this, false);