From 69bb4ae5ee8fe4a725dd812080614ebafe0fde32 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 13:40:33 +0100 Subject: [PATCH 01/15] Update release version to 4.0.0-beta.148 --- config/sentry.php | 2 +- config/version.php | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/sentry.php b/config/sentry.php index c07fda49b..8acf3d588 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.147', + 'release' => '4.0.0-beta.148', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index cf8ab7438..b2be1df08 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ Date: Tue, 28 Nov 2023 14:05:55 +0100 Subject: [PATCH 02/15] Remove unused imports and fix import statement --- app/Models/Server.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index 07714caa7..ca5bd9559 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,8 +2,6 @@ namespace App\Models; -use App\Actions\Server\InstallLogDrain; -use App\Actions\Server\InstallNewRelic; use App\Enums\ApplicationDeploymentStatus; use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; @@ -18,7 +16,7 @@ use Illuminate\Support\Sleep; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Illuminate\Support\Str; -use Stringable; +use Illuminate\Support\Stringable; class Server extends BaseModel { From 500ba0fab8ba567fb2fe8f043d604044c893c8a3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 14:08:42 +0100 Subject: [PATCH 03/15] fix: do not remove deployment in case compose based failed --- app/Jobs/ApplicationDeploymentJob.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 7d229e879..153404c72 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1289,9 +1289,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->execute_remote_command( ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], ["echo '{$exception->getMessage()}'", 'type' => 'err'], - ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'], - [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] ); + if ($this->application->build_pack !== 'dockercompose') { + $this->execute_remote_command( + ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'], + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] + ); + } $this->next(ApplicationDeploymentStatus::FAILED->value); } From 87062e4e226da1d6a0ee95280695adc099d5184d Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 14:23:59 +0100 Subject: [PATCH 04/15] Refactor application deployment job --- app/Jobs/ApplicationDeploymentJob.php | 39 ++------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 153404c72..dae4c0730 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -290,42 +290,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ray($e); } } - // private function deploy_docker_compose() - // { - // $dockercompose_base64 = base64_encode($this->application->dockercompose); - // $this->execute_remote_command( - // [ - // "echo 'Starting deployment of {$this->application->name}.'" - // ], - // ); - // $this->prepare_builder_image(); - // $this->execute_remote_command( - // [ - // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") - // ], - // ); - // $this->build_image_name = Str::lower("{$this->customRepository}:build"); - // $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); - // $this->save_environment_variables(); - // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); - // ray($containers); - // if ($containers->count() > 0) { - // foreach ($containers as $container) { - // $containerName = data_get($container, 'Names'); - // if ($containerName) { - // instant_remote_process( - // ["docker rm -f {$containerName}"], - // $this->application->destination->server - // ); - // } - // } - // } - - // $this->execute_remote_command( - // ["echo -n 'Starting services (could take a while)...'"], - // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], - // ); - // } private function generate_image_names() { if ($this->application->dockerfile) { @@ -589,6 +553,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->start_by_compose_file(); $this->health_check(); $this->stop_running_container(); + $this->application_deployment_queue->addLogEntry("Rolling update completed."); } } private function health_check() @@ -1204,7 +1169,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); }); - $this->application_deployment_queue->addLogEntry("Rolling update completed."); } else { $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->execute_remote_command( @@ -1226,6 +1190,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ); } + $this->application_deployment_queue->addLogEntry("New container started."); } private function generate_build_env_variables() From 4af471ee3161558b35c16b04cdc8539ad09c3c19 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 14:26:35 +0100 Subject: [PATCH 05/15] fix: no container servers --- app/Jobs/ContainerStatusJob.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 79265f7a1..e30729299 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -42,7 +42,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted if (!$this->server->isServerReady()) { return; }; - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server); + $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); + if (is_null($containers)) { + return; + } $containers = format_docker_command_output_to_json($containers); $applications = $this->server->applications(); $databases = $this->server->databases(); From 706e4b13ee30fe7eff6b162a52eb5e1150d39909 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 14:27:38 +0100 Subject: [PATCH 06/15] fix: sentry issue --- bootstrap/helpers/docker.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 030aa6aa7..d643cce6b 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -275,8 +275,11 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview return $labels->all(); } -function isDatabaseImage(string $image) +function isDatabaseImage(?string $image = null) { + if (is_null($image)) { + return false; + } $image = str($image); if ($image->contains(':')) { $image = str($image); From c505a6ce9cfd5e729cf64e11dfcdac9dc5dc497e Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 15:49:24 +0100 Subject: [PATCH 07/15] wip --- app/Actions/Proxy/StartProxy.php | 7 +- app/Http/Livewire/Boarding/Index.php | 5 +- app/Http/Livewire/Dashboard.php | 2 + app/Http/Livewire/Project/New/Select.php | 1 + app/Http/Livewire/Server/Form.php | 23 ++++-- app/Http/Livewire/Server/New/ByIp.php | 6 +- app/Models/Server.php | 82 ++++++++++++++----- app/Models/SwarmDocker.php | 44 ++++++++++ ..._28_143533_add_fields_to_swarm_dockers.php | 30 +++++++ .../views/livewire/boarding/index.blade.php | 34 ++++---- .../views/livewire/server/form.blade.php | 13 +-- .../views/livewire/server/new/by-ip.blade.php | 6 +- 12 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 6a6738177..b90508c5a 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -13,6 +13,9 @@ class StartProxy public function handle(Server $server, bool $async = true): string|Activity { try { + if ($server->isSwarm()) { + throw new \Exception("Server is part of swarm, not implemented yet."); + } $proxyType = $server->proxyType(); $commands = collect([]); $proxy_path = get_proxy_path(); @@ -46,11 +49,9 @@ class StartProxy $server->save(); return 'OK'; } - } catch(\Throwable $e) { + } catch (\Throwable $e) { ray($e); throw $e; } - - } } diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index 7f53708ad..645b0da6a 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -31,6 +31,7 @@ class Index extends Component public ?string $remoteServerHost = null; public ?int $remoteServerPort = 22; public ?string $remoteServerUser = 'root'; + public bool $isPartOfSwarm = false; public ?Server $createdServer = null; public Collection $projects; @@ -182,7 +183,9 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== 'private_key_id' => $this->createdPrivateKey->id, 'team_id' => currentTeam()->id, ]); - $this->createdServer->save(); + $this->createdServer->settings->is_part_of_swarm = $this->isPartOfSwarm; + $this->createdServer->settings->save(); + $this->createdServer->addInitialNetwork(); $this->validateServer(); } public function validateServer() diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php index b7219864d..2052ded86 100644 --- a/app/Http/Livewire/Dashboard.php +++ b/app/Http/Livewire/Dashboard.php @@ -14,6 +14,8 @@ class Dashboard extends Component public function mount() { $this->servers = Server::ownedByCurrentTeam()->get(); + ray($this->servers[1]); + ray($this->servers[1]->standaloneDockers); $this->projects = Project::ownedByCurrentTeam()->get(); } // public function getIptables() diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php index 13fbb8886..a23209768 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -126,6 +126,7 @@ class Select extends Component { $this->server_id = $server->id; $this->standaloneDockers = $server->standaloneDockers; + ray($server); $this->swarmDockers = $server->swarmDockers; $this->current_step = 'destinations'; } diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index dacb8faad..27dfaa8e2 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -17,14 +17,14 @@ class Form extends Component protected $listeners = ['serverRefresh']; protected $rules = [ - 'server.name' => 'required|min:6', + 'server.name' => 'required', 'server.description' => 'nullable', 'server.ip' => 'required', 'server.user' => 'required', 'server.port' => 'required', - 'server.settings.is_cloudflare_tunnel' => 'required', + 'server.settings.is_cloudflare_tunnel' => 'required|boolean', 'server.settings.is_reachable' => 'required', - 'server.settings.is_part_of_swarm' => 'required', + 'server.settings.is_part_of_swarm' => 'required|boolean', 'wildcard_domain' => 'nullable|url', ]; protected $validationAttributes = [ @@ -49,9 +49,14 @@ class Form extends Component } public function instantSave() { - refresh_server_connection($this->server->privateKey); - $this->validateServer(); - $this->server->settings->save(); + try { + refresh_server_connection($this->server->privateKey); + $this->validateServer(false); + $this->server->settings->save(); + $this->emit('success', 'Server updated successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function installDocker() { @@ -100,6 +105,12 @@ class Form extends Component $install && $this->installDocker(); return; } + if ($this->server->isSwarm()) { + $swarmInstalled = $this->server->validateDockerSwarm(); + if ($swarmInstalled) { + $install && $this->emit('success', 'Docker Swarm is initiated.'); + } + } } catch (\Throwable $e) { return handleError($e, $this); } finally { diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index 66750db28..139705d94 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -29,6 +29,7 @@ class ByIp extends Component 'ip' => 'required', 'user' => 'required|string', 'port' => 'required|integer', + 'is_part_of_swarm' => 'required|boolean', ]; protected $validationAttributes = [ 'name' => 'Name', @@ -36,6 +37,7 @@ class ByIp extends Component 'ip' => 'IP Address/Domain', 'user' => 'User', 'port' => 'Port', + 'is_part_of_swarm' => 'Is part of swarm', ]; public function mount() @@ -72,11 +74,11 @@ class ByIp extends Component 'proxy' => [ "type" => ProxyTypes::TRAEFIK_V2->value, "status" => ProxyStatus::EXITED->value, - ] - + ], ]); $server->settings->is_part_of_swarm = $this->is_part_of_swarm; $server->settings->save(); + $server->addInitialNetwork(); return redirect()->route('server.show', $server->uuid); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Models/Server.php b/app/Models/Server.php index ca5bd9559..9a0c4ce7d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -35,25 +35,10 @@ class Server extends BaseModel } $server->forceFill($payload); }); - static::created(function ($server) { ServerSetting::create([ 'server_id' => $server->id, ]); - if ($server->id === 0) { - StandaloneDocker::create([ - 'id' => 0, - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $server->id, - ]); - } else { - StandaloneDocker::create([ - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $server->id, - ]); - } }); static::deleting(function ($server) { $server->destinations()->each(function ($destination) { @@ -101,7 +86,39 @@ class Server extends BaseModel { return $this->hasOne(ServerSetting::class); } - + public function addInitialNetwork() { + if ($this->id === 0) { + if ($this->isSwarm()) { + SwarmDocker::create([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify-overlay', + 'server_id' => $this->id, + ]); + } else { + StandaloneDocker::create([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => $this->id, + ]); + } + } else { + if ($this->isSwarm()) { + SwarmDocker::create([ + 'name' => 'coolify', + 'network' => 'coolify-overlay', + 'server_id' => $this->id, + ]); + } else { + StandaloneDocker::create([ + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => $this->id, + ]); + } + } + } public function proxyType() { $proxyType = $this->proxy->get('type'); @@ -357,12 +374,16 @@ class Server extends BaseModel return false; } } + public function isSwarm() + { + return data_get($this, 'settings.is_part_of_swarm'); + } public function validateConnection() { + $server = Server::find($this->id); if ($this->skipServer()) { return false; } - $uptime = instant_remote_process(['uptime'], $this, false); if (!$uptime) { $this->settings()->update([ @@ -373,14 +394,14 @@ class Server extends BaseModel $this->settings()->update([ 'is_reachable' => true, ]); - $this->update([ + $server->update([ 'unreachable_count' => 0, ]); } if (data_get($this, 'unreachable_notification_sent') === true) { $this->team->notify(new Revived($this)); - $this->update(['unreachable_notification_sent' => false]); + $server->update(['unreachable_notification_sent' => false]); } return true; @@ -398,7 +419,20 @@ class Server extends BaseModel } $this->settings->is_usable = true; $this->settings->save(); - $this->validateCoolifyNetwork(); + $this->validateCoolifyNetwork(isSwarm: false); + return true; + } + public function validateDockerSwarm() + { + $swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false); + $swarmStatus = str($swarmStatus)->trim()->after(':')->trim(); + if ($swarmStatus === 'inactive') { + throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.'); + return false; + } + $this->settings->is_usable = true; + $this->settings->save(); + $this->validateCoolifyNetwork(isSwarm: true); return true; } public function validateDockerEngineVersion() @@ -415,9 +449,13 @@ class Server extends BaseModel $this->settings->save(); return true; } - public function validateCoolifyNetwork() + public function validateCoolifyNetwork($isSwarm = false) { - return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); + if ($isSwarm) { + return instant_remote_process(["docker network create --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); + } else { + return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); + } } public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null) { diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php index ea56f85bc..9f0973db5 100644 --- a/app/Models/SwarmDocker.php +++ b/app/Models/SwarmDocker.php @@ -4,13 +4,57 @@ namespace App\Models; class SwarmDocker extends BaseModel { + protected $guarded = []; + public function applications() { return $this->morphMany(Application::class, 'destination'); } + public function postgresqls() + { + return $this->morphMany(StandalonePostgresql::class, 'destination'); + } + + public function redis() + { + return $this->morphMany(StandaloneRedis::class, 'destination'); + } + public function mongodbs() + { + return $this->morphMany(StandaloneMongodb::class, 'destination'); + } + public function mysqls() + { + return $this->morphMany(StandaloneMysql::class, 'destination'); + } + public function mariadbs() + { + return $this->morphMany(StandaloneMariadb::class, 'destination'); + } + public function server() { return $this->belongsTo(Server::class); } + + public function services() + { + return $this->morphMany(Service::class, 'destination'); + } + + public function databases() + { + $postgresqls = $this->postgresqls; + $redis = $this->redis; + $mongodbs = $this->mongodbs; + $mysqls = $this->mysqls; + $mariadbs = $this->mariadbs; + return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs); + } + + public function attachedTo() + { + return $this->applications?->count() > 0 || $this->databases()->count() > 0; + } } diff --git a/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php b/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php new file mode 100644 index 000000000..7ca398b90 --- /dev/null +++ b/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php @@ -0,0 +1,30 @@ +string('network'); + + $table->unique(['server_id', 'network']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('swarm_dockers', function (Blueprint $table) { + $table->dropColumn('network'); + }); + } +}; diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 75f2441f0..389633b2d 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -131,16 +131,16 @@ @if (!$serverReachable) - This server is not reachable with the following public key. -

- Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user - 'root' or skip the boarding process and add a new private key manually to Coolify and to the - server. - - Check again - - @endif + This server is not reachable with the following public key. +

+ Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user + 'root' or skip the boarding process and add a new private key manually to Coolify and to the + server. + + Check + again + + @endif

Private Keys are used to connect to a remote server through a secure shell, called SSH.

@@ -200,14 +200,17 @@ label="Description" id="remoteServerDescription" />
- +
+
+ +
Check Connection @@ -226,7 +229,7 @@ - Let's do it! + Let's do it! @if ($dockerInstallationStarted) Validate Server & Continue @@ -234,7 +237,10 @@

This will install the latest Docker Engine on your server, configure a few things to be able - to run optimal.

Minimum Docker Engine version is: 22

To manually install Docker Engine, check this documentation.

+ to run optimal.

Minimum Docker Engine version is: 22

To manually install Docker + Engine, check this + documentation.

diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 4004dd096..0a98128cf 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -38,8 +38,7 @@ - {{-- --}} +
- @if (!$server->isLocalhost()) -
+
+ @if (!$server->isLocalhost()) -
- @endif + @endif + +
@if ($server->isFunctional()) diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index 7dfb3a5b0..9503a36a4 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -3,7 +3,7 @@ @else

Create a new Server

-
Servers are the main blocks of your infrastructure.
+
Servers are the main blocks of your infrastructure.
@@ -25,6 +25,10 @@ @endif @endforeach +
+ +
Save New Server From b4874c7df31a723d94913833124d2dd6330feaa6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 18:31:04 +0100 Subject: [PATCH 08/15] wip: swarm --- app/Actions/Application/StopApplication.php | 43 ++-- app/Http/Livewire/Dashboard.php | 2 - app/Http/Livewire/Project/New/Select.php | 1 - app/Jobs/ApplicationDeploymentJob.php | 189 ++++++++++-------- app/Models/Server.php | 10 +- bootstrap/helpers/remoteProcess.php | 3 + database/seeders/SwarmDockerSeeder.php | 8 +- .../project/application/general.blade.php | 46 +++-- 8 files changed, 172 insertions(+), 130 deletions(-) diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 1d09f0daf..d32248042 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -11,29 +11,34 @@ class StopApplication public function handle(Application $application) { $server = $application->destination->server; - $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); - if ($containers->count() > 0) { - foreach ($containers as $container) { - $containerName = data_get($container, 'Names'); - if ($containerName) { - instant_remote_process( - ["docker rm -f {$containerName}"], - $server - ); + if ($server->isSwarm()) { + instant_remote_process(["docker stack rm {$application->uuid}" ], $server); + } else { + $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); + if ($containers->count() > 0) { + foreach ($containers as $container) { + $containerName = data_get($container, 'Names'); + if ($containerName) { + instant_remote_process( + ["docker rm -f {$containerName}"], + $server + ); + } } + // TODO: make notification for application + // $application->environment->project->team->notify(new StatusChanged($application)); } - // TODO: make notification for application - // $application->environment->project->team->notify(new StatusChanged($application)); - } - // Delete Preview Deployments - $previewDeployments = $application->previews; - foreach ($previewDeployments as $previewDeployment) { - $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id); - foreach ($containers as $container) { - $name = str_replace('/', '', $container['Names']); - instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false); + // Delete Preview Deployments + $previewDeployments = $application->previews; + foreach ($previewDeployments as $previewDeployment) { + $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id); + foreach ($containers as $container) { + $name = str_replace('/', '', $container['Names']); + instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false); + } } } + } } diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php index 2052ded86..b7219864d 100644 --- a/app/Http/Livewire/Dashboard.php +++ b/app/Http/Livewire/Dashboard.php @@ -14,8 +14,6 @@ class Dashboard extends Component public function mount() { $this->servers = Server::ownedByCurrentTeam()->get(); - ray($this->servers[1]); - ray($this->servers[1]->standaloneDockers); $this->projects = Project::ownedByCurrentTeam()->get(); } // public function getIptables() diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php index a23209768..13fbb8886 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -126,7 +126,6 @@ class Select extends Component { $this->server_id = $server->id; $this->standaloneDockers = $server->standaloneDockers; - ray($server); $this->swarmDockers = $server->swarmDockers; $this->current_step = 'destinations'; } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index dae4c0730..17d9b2c9b 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -156,25 +156,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // Generate custom host<->ip mapping $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); - $allContainers = format_docker_command_output_to_json($allContainers); - $ips = collect([]); - if (count($allContainers) > 0) { - $allContainers = $allContainers[0]; - foreach ($allContainers as $container) { - $containerName = data_get($container, 'Name'); - if ($containerName === 'coolify-proxy') { - continue; - } - $containerIp = data_get($container, 'IPv4Address'); - if ($containerName && $containerIp) { - $containerIp = str($containerIp)->before('/'); - $ips->put($containerName, $containerIp->value()); + if (!is_null($allContainers)) { + $allContainers = format_docker_command_output_to_json($allContainers); + $ips = collect([]); + if (count($allContainers) > 0) { + $allContainers = $allContainers[0]; + foreach ($allContainers as $container) { + $containerName = data_get($container, 'Name'); + if ($containerName === 'coolify-proxy') { + continue; + } + $containerIp = data_get($container, 'IPv4Address'); + if ($containerName && $containerIp) { + $containerIp = str($containerIp)->before('/'); + $ips->put($containerName, $containerIp->value()); + } } } + $this->addHosts = $ips->map(function ($ip, $name) { + return "--add-host $name:$ip"; + })->implode(' '); } - $this->addHosts = $ips->map(function ($ip, $name) { - return "--add-host $name:$ip"; - })->implode(' '); if ($this->application->dockerfile_target_build) { $this->buildTarget = " --target {$this->application->dockerfile_target_build} "; @@ -214,6 +216,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') { $this->push_to_docker_registry(); + if ($this->server->isSwarm()) { + $this->application_deployment_queue->addLogEntry("Creating / updating stack."); + $this->execute_remote_command( + [ + "docker stack deploy --with-registry-auth --prune --compose-file {$this->configuration_dir}/docker-compose.yml {$this->application->uuid}" + ], + ); + } } $this->next(ApplicationDeploymentStatus::FINISHED->value); $this->application->isConfigurationChanged(true); @@ -534,75 +544,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function rolling_update() { - if (count($this->application->ports_mappings_array) > 0) { - $this->execute_remote_command( - [ - "echo '\n----------------------------------------'", - ], - ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], - ); - $this->stop_running_container(force: true); - $this->start_by_compose_file(); + if ($this->server->isSwarm()) { + // Skip this. } else { - $this->execute_remote_command( - [ - "echo '\n----------------------------------------'", - ], - ["echo -n 'Rolling update started.'"], - ); - $this->start_by_compose_file(); - $this->health_check(); - $this->stop_running_container(); - $this->application_deployment_queue->addLogEntry("Rolling update completed."); + if (count($this->application->ports_mappings_array) > 0) { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], + ); + $this->stop_running_container(force: true); + $this->start_by_compose_file(); + } else { + $this->execute_remote_command( + [ + "echo '\n----------------------------------------'", + ], + ["echo -n 'Rolling update started.'"], + ); + $this->start_by_compose_file(); + $this->health_check(); + $this->stop_running_container(); + $this->application_deployment_queue->addLogEntry("Rolling update completed."); + } } } private function health_check() { - if ($this->application->isHealthcheckDisabled()) { - $this->newVersionIsHealthy = true; - return; - } - // ray('New container name: ', $this->container_name); - if ($this->container_name) { - $counter = 1; - $this->execute_remote_command( - [ - "echo 'Waiting for healthcheck to pass on the new container.'" - ] - ); - if ($this->full_healthcheck_url) { + if ($this->server->isSwarm()) { + // Implement healthcheck for swarm + } else { + if ($this->application->isHealthcheckDisabled()) { + $this->newVersionIsHealthy = true; + return; + } + // ray('New container name: ', $this->container_name); + if ($this->container_name) { + $counter = 1; $this->execute_remote_command( [ - "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" + "echo 'Waiting for healthcheck to pass on the new container.'" ] ); - } - while ($counter < $this->application->health_check_retries) { - $this->execute_remote_command( - [ - "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", - "hidden" => true, - "save" => "health_check" - ], - - ); - $this->execute_remote_command( - [ - "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" - ], - ); - if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { - $this->newVersionIsHealthy = true; - $this->application->update(['status' => 'running']); + if ($this->full_healthcheck_url) { $this->execute_remote_command( [ - "echo 'New container is healthy.'" + "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" + ] + ); + } + while ($counter < $this->application->health_check_retries) { + $this->execute_remote_command( + [ + "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", + "hidden" => true, + "save" => "health_check" + ], + + ); + $this->execute_remote_command( + [ + "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" ], ); - break; + if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { + $this->newVersionIsHealthy = true; + $this->application->update(['status' => 'running']); + $this->execute_remote_command( + [ + "echo 'New container is healthy.'" + ], + ); + break; + } + $counter++; + sleep($this->application->health_check_interval); } - $counter++; - sleep($this->application->health_check_interval); } } } @@ -849,21 +867,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if ($this->pull_request_id !== 0) { $labels = collect(generateLabelsApplication($this->application, $this->preview)); - - // $newHostLabel = $newLabels->filter(function ($label) { - // return str($label)->contains('Host'); - // }); - // $labels = $labels->reject(function ($label) { - // return str($label)->contains('Host'); - // }); - // ray($labels,$newLabels); - // $labels = $labels->map(function ($label) { - // $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/'; - // $replacement = "$1-pr-{$this->pull_request_id}-$2-$3"; - // $newLabel = preg_replace($pattern, $replacement, $label); - // return $newLabel; - // }); - // $labels = $labels->merge($newHostLabel); } $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $docker_compose = [ @@ -906,6 +909,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ] ] ]; + if ($this->server->isSwarm()) { + data_forget($docker_compose, 'services.' . $this->container_name . '.container_name'); + data_forget($docker_compose, 'services.' . $this->container_name . '.expose'); + data_forget($docker_compose, 'services.' . $this->container_name . '.restart'); + + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit'); + data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit'); + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness'); + data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpus'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset'); + data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares'); + } else { + } if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { $docker_compose['services'][$this->container_name]['logging'] = [ 'driver' => 'fluentd', diff --git a/app/Models/Server.php b/app/Models/Server.php index 9a0c4ce7d..5e3762971 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -67,7 +67,7 @@ class Server extends BaseModel { $teamId = currentTeam()->id; $selectArray = collect($select)->concat(['id']); - return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name'); + return Server::whereTeamId($teamId)->with('settings','swarmDockers','standaloneDockers')->select($selectArray->all())->orderBy('name'); } static public function isUsable() @@ -87,6 +87,8 @@ class Server extends BaseModel return $this->hasOne(ServerSetting::class); } public function addInitialNetwork() { + ray($this->id); + if ($this->id === 0) { if ($this->isSwarm()) { SwarmDocker::create([ @@ -106,13 +108,13 @@ class Server extends BaseModel } else { if ($this->isSwarm()) { SwarmDocker::create([ - 'name' => 'coolify', + 'name' => 'coolify-overlay', 'network' => 'coolify-overlay', 'server_id' => $this->id, ]); } else { StandaloneDocker::create([ - 'name' => 'coolify', + 'name' => 'coolify-overlay', 'network' => 'coolify', 'server_id' => $this->id, ]); @@ -452,7 +454,7 @@ class Server extends BaseModel public function validateCoolifyNetwork($isSwarm = false) { if ($isSwarm) { - return instant_remote_process(["docker network create --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); + return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); } else { return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 45c8ae0e5..2fe90415e 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro } return excludeCertainErrors($process->errorOutput(), $exitCode); } + if ($output === 'null') { + $output = null; + } return $output; } function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) diff --git a/database/seeders/SwarmDockerSeeder.php b/database/seeders/SwarmDockerSeeder.php index 906e1bccc..8a204e159 100644 --- a/database/seeders/SwarmDockerSeeder.php +++ b/database/seeders/SwarmDockerSeeder.php @@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder */ public function run(): void { - SwarmDocker::create([ - 'name' => 'Swarm Docker 1', - 'server_id' => 1, - ]); + // SwarmDocker::create([ + // 'name' => 'Swarm Docker 1', + // 'server_id' => 1, + // ]); } } diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index daeac9710..835c15766 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -69,21 +69,39 @@ @endif @if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')

Docker Registry

-
Push the built image to a docker registry. More info here.
+ @if ($application->destination->server->isSwarm()) +
Docker Swarm requires the image to be available in a registry. More info here.
+ @else +
Push the built image to a docker registry. More info here.
+ @endif
@if ($application->build_pack === 'dockerimage') - - + @if ($application->destination->server->isSwarm()) + + + @else + + + @endif @else - - + @if ($application->destination->server->isSwarm()) + + + @else + + + @endif + @endif
@@ -140,8 +158,8 @@ @endif @if ($application->build_pack === 'dockercompose') Reload Compose File - + {{-- --}} @endif From c41ffd6bfbff104fbd12a24496bffde659a40f8b Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 18:42:09 +0100 Subject: [PATCH 09/15] wip: swarm --- app/Actions/Proxy/StartProxy.php | 39 ++++++++++++++++++++------------ bootstrap/helpers/proxy.php | 13 ++++++++++- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index b90508c5a..b6f657b35 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -13,9 +13,7 @@ class StartProxy public function handle(Server $server, bool $async = true): string|Activity { try { - if ($server->isSwarm()) { - throw new \Exception("Server is part of swarm, not implemented yet."); - } + $proxyType = $server->proxyType(); $commands = collect([]); $proxy_path = get_proxy_path(); @@ -27,18 +25,29 @@ class StartProxy $docker_compose_yml_base64 = base64_encode($configuration); $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); - $commands = $commands->merge([ - "mkdir -p $proxy_path && cd $proxy_path", - "echo 'Creating required Docker Compose file.'", - "echo 'Pulling docker image.'", - 'docker compose pull', - "echo 'Stopping existing coolify-proxy.'", - "docker compose down -v --remove-orphans > /dev/null 2>&1", - "echo 'Starting coolify-proxy.'", - 'docker compose up -d --remove-orphans', - "echo 'Proxy started successfully.'" - ]); - $commands = $commands->merge(connectProxyToNetworks($server)); + if ($server->isSwarm()) { + $commands = $commands->merge([ + "mkdir -p $proxy_path && cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Starting coolify-proxy.'", + // "docker stack deploy -c docker-compose.yaml coolify-proxy", + "echo 'Proxy started successfully.'" + ]); + } else { + $commands = $commands->merge([ + "mkdir -p $proxy_path && cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Pulling docker image.'", + 'docker compose pull', + "echo 'Stopping existing coolify-proxy.'", + "docker compose down -v --remove-orphans > /dev/null 2>&1", + "echo 'Starting coolify-proxy.'", + 'docker compose up -d --remove-orphans', + "echo 'Proxy started successfully.'" + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); + } + if ($async) { $activity = remote_process($commands, $server); return $activity; diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 2f595d1c6..614988eca 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -102,7 +102,6 @@ function generate_default_proxy_configuration(Server $server) "--entrypoints.https.address=:443", "--entrypoints.http.http.encodequerysemicolons=true", "--entrypoints.https.http.encodequerysemicolons=true", - "--providers.docker=true", "--providers.docker.exposedbydefault=false", "--providers.file.directory=/traefik/dynamic/", "--providers.file.watch=true", @@ -128,6 +127,18 @@ function generate_default_proxy_configuration(Server $server) $config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log"; $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; } + if ($server->isSwarm()) { + $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; + $config['services']['traefik']['deploy'] = [ + "placement" => [ + "constraints" => [ + "node.role==manager", + ], + ], + ]; + } else { + $config['services']['traefik']['command'][] = "--providers.docker=true"; + } $config = Yaml::dump($config, 4, 2); SaveConfiguration::run($server, $config); return $config; From b21add02102e5a84e7fe30de8723d1b028ac8de6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 20:09:00 +0100 Subject: [PATCH 10/15] Update Swarm cluster label to Swarm Manager --- resources/views/livewire/boarding/index.blade.php | 2 +- resources/views/livewire/server/form.blade.php | 2 +- resources/views/livewire/server/new/by-ip.blade.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 389633b2d..9f8e47961 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -209,7 +209,7 @@
+ label="Is it a Swarm Manager?" />
Check Connection
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 0a98128cf..c124a6b25 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -55,7 +55,7 @@ id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> @endif + label="Is it a Swarm Manager?" /> diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index 9503a36a4..410f27fa7 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -27,7 +27,7 @@
+ label="Is it a Swarm Manager?" />
Save New Server From 928b68043b85be34e41587c67da4f1838470898f Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 20:49:38 +0100 Subject: [PATCH 11/15] wip: swarm --- app/Jobs/ApplicationDeploymentJob.php | 36 ++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 17d9b2c9b..02473e0a5 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -877,7 +877,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted 'container_name' => $this->container_name, 'restart' => RESTART_MODE, 'environment' => $environment_variables, - 'labels' => $labels, 'expose' => $ports, 'networks' => [ $this->destination->network, @@ -921,7 +920,37 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted data_forget($docker_compose, 'services.' . $this->container_name . '.cpus'); data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset'); data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares'); + + $docker_compose['services'][$this->container_name]['deploy'] = [ + 'placement' => [ + 'constraints' => [ + 'node.role == worker' + ] + ], + 'mode' => 'replicated', + 'replicas' => 1, + 'update_config' => [ + 'order' => 'start-first' + ], + 'rollback_config' => [ + 'order' => 'start-first' + ], + 'labels' => $labels, + 'resources' => [ + 'limits' => [ + 'cpus' => $this->application->limits_cpus, + 'memory' => $this->application->limits_memory, + ], + 'reservations' => [ + 'cpus' => $this->application->limits_cpus, + 'memory' => $this->application->limits_memory, + ] + ] + ]; + } else { + $docker_compose['services'][$this->container_name]['labels'] = $labels; + } if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { $docker_compose['services'][$this->container_name]['logging'] = [ @@ -970,6 +999,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // 'dockerfile' => $this->workdir . $this->dockerfile_location, // ]; // } + + $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + + data_forget($docker_compose, 'services.' . $this->container_name); + $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); From 2d7bbbe30089b8b75aa3ae51a544aac8f6e27827 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 29 Nov 2023 10:06:52 +0100 Subject: [PATCH 12/15] wip: swarm --- app/Actions/Proxy/StartProxy.php | 2 +- app/Actions/Server/InstallDocker.php | 16 ++++++-- app/Http/Livewire/Boarding/Index.php | 4 +- app/Http/Livewire/Server/Form.php | 8 ++-- app/Http/Livewire/Server/New/ByIp.php | 8 ++-- app/Jobs/ApplicationDeploymentJob.php | 15 ++++--- app/Jobs/ContainerStatusJob.php | 5 +++ app/Models/Server.php | 2 +- bootstrap/helpers/proxy.php | 40 ++++++++++++------- ...3_11_29_075937_change_swarm_properties.php | 30 ++++++++++++++ .../components/applications/navbar.blade.php | 2 + .../views/livewire/boarding/index.blade.php | 2 +- .../views/livewire/server/form.blade.php | 4 +- .../views/livewire/server/new/by-ip.blade.php | 2 +- scripts/upgrade.sh | 3 +- 15 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 database/migrations/2023_11_29_075937_change_swarm_properties.php diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index b6f657b35..a99e47b25 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -30,7 +30,7 @@ class StartProxy "mkdir -p $proxy_path && cd $proxy_path", "echo 'Creating required Docker Compose file.'", "echo 'Starting coolify-proxy.'", - // "docker stack deploy -c docker-compose.yaml coolify-proxy", + "cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy", "echo 'Proxy started successfully.'" ]); } else { diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index a580c3473..79863d989 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -76,10 +76,20 @@ class InstallDocker "echo 'Restarting Docker Engine...'", "systemctl enable docker >/dev/null 2>&1 || true", "systemctl restart docker", - "echo 'Creating default Docker network (coolify)...'", - "docker network create --attachable coolify >/dev/null 2>&1 || true", - "echo 'Done!'" ]); + if ($server->isSwarm()) { + $command = $command->merge([ + "docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true", + ]); + } else { + $command = $command->merge([ + "docker network create --attachable coolify >/dev/null 2>&1 || true", + ]); + $command = $command->merge([ + "echo 'Done!'", + ]); + } + return remote_process($command, $server); } } diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index 645b0da6a..67221d0c6 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -31,7 +31,7 @@ class Index extends Component public ?string $remoteServerHost = null; public ?int $remoteServerPort = 22; public ?string $remoteServerUser = 'root'; - public bool $isPartOfSwarm = false; + public bool $isSwarmManager = false; public ?Server $createdServer = null; public Collection $projects; @@ -183,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== 'private_key_id' => $this->createdPrivateKey->id, 'team_id' => currentTeam()->id, ]); - $this->createdServer->settings->is_part_of_swarm = $this->isPartOfSwarm; + $this->createdServer->settings->is_swarm_manager = $this->isSwarmManager; $this->createdServer->settings->save(); $this->createdServer->addInitialNetwork(); $this->validateServer(); diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 27dfaa8e2..efca800c2 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -24,7 +24,8 @@ class Form extends Component 'server.port' => 'required', 'server.settings.is_cloudflare_tunnel' => 'required|boolean', 'server.settings.is_reachable' => 'required', - 'server.settings.is_part_of_swarm' => 'required|boolean', + 'server.settings.is_swarm_manager' => 'required|boolean', + // 'server.settings.is_swarm_worker' => 'required|boolean', 'wildcard_domain' => 'nullable|url', ]; protected $validationAttributes = [ @@ -34,8 +35,9 @@ class Form extends Component 'server.user' => 'User', 'server.port' => 'Port', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', - 'server.settings.is_reachable' => 'is reachable', - 'server.settings.is_part_of_swarm' => 'is part of swarm' + 'server.settings.is_reachable' => 'Is reachable', + 'server.settings.is_swarm_manager' => 'Swarm Manager', + // 'server.settings.is_swarm_worker' => 'Swarm Worker', ]; public function mount() diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index 139705d94..858f4ffa1 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -21,7 +21,7 @@ class ByIp extends Component public string $ip; public string $user = 'root'; public int $port = 22; - public bool $is_part_of_swarm = false; + public bool $is_swarm_manager = false; protected $rules = [ 'name' => 'required|string', @@ -29,7 +29,7 @@ class ByIp extends Component 'ip' => 'required', 'user' => 'required|string', 'port' => 'required|integer', - 'is_part_of_swarm' => 'required|boolean', + 'is_swarm_manager' => 'required|boolean', ]; protected $validationAttributes = [ 'name' => 'Name', @@ -37,7 +37,7 @@ class ByIp extends Component 'ip' => 'IP Address/Domain', 'user' => 'User', 'port' => 'Port', - 'is_part_of_swarm' => 'Is part of swarm', + 'is_swarm_manager' => 'Swarm Manager', ]; public function mount() @@ -76,7 +76,7 @@ class ByIp extends Component "status" => ProxyStatus::EXITED->value, ], ]); - $server->settings->is_part_of_swarm = $this->is_part_of_swarm; + $server->settings->is_swarm_manager = $this->is_swarm_manager; $server->settings->save(); $server->addInitialNetwork(); return redirect()->route('server.show', $server->uuid); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 02473e0a5..bd6d7436a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -453,11 +453,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->pull_request_id !== 0) { $networkId = "{$this->application->uuid}-{$this->pull_request_id}"; } - $this->execute_remote_command([ - "docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true - ], [ - "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true - ]); + if ($this->server->isSwarm()) { + // TODO + } else { + $this->execute_remote_command([ + "docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true + ], [ + "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true + ]); + } + $this->start_by_compose_file(); $this->application->loadComposeFile(isInit: false); } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index e30729299..30324baab 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -42,6 +42,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted if (!$this->server->isServerReady()) { return; }; + if ($this->server->isSwarm()) { + + } else { + + } $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); if (is_null($containers)) { return; diff --git a/app/Models/Server.php b/app/Models/Server.php index 5e3762971..408106107 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -378,7 +378,7 @@ class Server extends BaseModel } public function isSwarm() { - return data_get($this, 'settings.is_part_of_swarm'); + return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker'); } public function validateConnection() { diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 614988eca..76fa9cc5a 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -54,9 +54,15 @@ function connectProxyToNetworks(Server $server) function generate_default_proxy_configuration(Server $server) { $proxy_path = get_proxy_path(); - $networks = collect($server->standaloneDockers)->map(function ($docker) { - return $docker['network']; - })->unique(); + if ($server->isSwarm()) { + $networks = collect($server->swarmDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + } else { + $networks = collect($server->standaloneDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + } if ($networks->count() === 0) { $networks = collect(['coolify']); } @@ -66,6 +72,16 @@ function generate_default_proxy_configuration(Server $server) "external" => true, ]; }); + $labels = [ + "traefik.enable=true", + "traefik.http.routers.traefik.entrypoints=http", + "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file", + "traefik.http.routers.traefik.service=api@internal", + "traefik.http.services.traefik.loadbalancer.server.port=8080", + // Global Middlewares + "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", + "traefik.http.middlewares.gzip.compress=true", + ]; $config = [ "version" => "3.8", "networks" => $array_of_networks->toArray(), @@ -109,16 +125,7 @@ function generate_default_proxy_configuration(Server $server) "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json", "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", ], - "labels" => [ - "traefik.enable=true", - "traefik.http.routers.traefik.entrypoints=http", - "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file", - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - // Global Middlewares - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", - "traefik.http.middlewares.gzip.compress=true", - ], + "labels" => $labels, ], ], ]; @@ -128,8 +135,13 @@ function generate_default_proxy_configuration(Server $server) $config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; } if ($server->isSwarm()) { + data_forget($config, 'services.traefik.container_name'); + data_forget($config, 'services.traefik.restart'); + data_forget($config, 'services.traefik.labels'); + $config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true"; $config['services']['traefik']['deploy'] = [ + "labels" => $labels, "placement" => [ "constraints" => [ "node.role==manager", @@ -139,7 +151,7 @@ function generate_default_proxy_configuration(Server $server) } else { $config['services']['traefik']['command'][] = "--providers.docker=true"; } - $config = Yaml::dump($config, 4, 2); + $config = Yaml::dump($config, 12, 2); SaveConfiguration::run($server, $config); return $config; } diff --git a/database/migrations/2023_11_29_075937_change_swarm_properties.php b/database/migrations/2023_11_29_075937_change_swarm_properties.php new file mode 100644 index 000000000..6c0edc432 --- /dev/null +++ b/database/migrations/2023_11_29_075937_change_swarm_properties.php @@ -0,0 +1,30 @@ +renameColumn('is_part_of_swarm', 'is_swarm_manager'); + $table->boolean('is_swarm_worker')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->renameColumn('is_swarm_manager', 'is_part_of_swarm'); + $table->dropColumn('is_swarm_worker'); + }); + } +}; diff --git a/resources/views/components/applications/navbar.blade.php b/resources/views/components/applications/navbar.blade.php index d0cd565dc..9bd048e26 100644 --- a/resources/views/components/applications/navbar.blade.php +++ b/resources/views/components/applications/navbar.blade.php @@ -15,6 +15,8 @@
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
Please load a Compose file.
+ @elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty()) + Swarm Deployments requires a Docker Image in a Registry. @else @if ($application->status !== 'exited') diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 9f8e47961..fb102c7c5 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -208,7 +208,7 @@ id="remoteServerUser" />
-
Check Connection diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index c124a6b25..528fd32c5 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -54,8 +54,10 @@ helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.Coolify does not install/setup Cloudflare (cloudflared) on your server." id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> @endif - + {{-- --}} diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index 410f27fa7..62268f2fe 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -26,7 +26,7 @@ @endforeach
-
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 3c44308a0..af6463869 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -16,6 +16,7 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' > /data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env # Make sure coolify network exists -docker network create coolify 2>/dev/null +docker network create --attachable coolify 2>/dev/null +# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null docker run --pull always -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate" From f4803ad58bfb8f7cb12a7c296338767b06f3c66a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 29 Nov 2023 14:59:06 +0100 Subject: [PATCH 13/15] wip: swarm fix: gitcompose deployments --- app/Actions/Proxy/CheckProxy.php | 55 +++++++++++-------- app/Http/Livewire/Server/Proxy/Deploy.php | 26 +++++++-- app/Http/Livewire/Server/Proxy/Status.php | 2 +- app/Jobs/ApplicationDeploymentJob.php | 34 +++--------- app/Jobs/ContainerStatusJob.php | 43 +++++++++++++-- app/Models/Application.php | 49 +++++++++-------- bootstrap/helpers/docker.php | 20 ++++++- bootstrap/helpers/shared.php | 2 +- .../views/livewire/boarding/index.blade.php | 4 +- .../views/livewire/server/form.blade.php | 4 +- .../views/livewire/server/new/by-ip.blade.php | 4 +- .../livewire/server/proxy/status.blade.php | 2 +- 12 files changed, 149 insertions(+), 96 deletions(-) diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 279fac20e..32673abc9 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -17,35 +17,42 @@ class CheckProxy return false; } } - $status = getContainerStatus($server, 'coolify-proxy'); - if ($status === 'running') { - $server->proxy->set('status', 'running'); + if ($server->isSwarm()) { + $status = getContainerStatus($server, 'coolify-proxy_traefik'); + $server->proxy->set('status', $status); $server->save(); return false; - } - $ip = $server->ip; - if ($server->id === 0) { - $ip = 'host.docker.internal'; - } + } else { + $status = getContainerStatus($server, 'coolify-proxy'); + if ($status === 'running') { + $server->proxy->set('status', 'running'); + $server->save(); + return false; + } + $ip = $server->ip; + if ($server->id === 0) { + $ip = 'host.docker.internal'; + } - $connection80 = @fsockopen($ip, '80'); - $connection443 = @fsockopen($ip, '443'); - $port80 = is_resource($connection80) && fclose($connection80); - $port443 = is_resource($connection443) && fclose($connection443); - if ($port80) { - if ($fromUI) { - throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); - } else { - return false; + $connection80 = @fsockopen($ip, '80'); + $connection443 = @fsockopen($ip, '443'); + $port80 = is_resource($connection80) && fclose($connection80); + $port443 = is_resource($connection443) && fclose($connection443); + if ($port80) { + if ($fromUI) { + throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } else { + return false; + } } - } - if ($port443) { - if ($fromUI) { - throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); - } else { - return false; + if ($port443) { + if ($fromUI) { + throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } else { + return false; + } } + return true; } - return true; } } diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php index 7e828b092..9612eccc7 100644 --- a/app/Http/Livewire/Server/Proxy/Deploy.php +++ b/app/Http/Livewire/Server/Proxy/Deploy.php @@ -58,11 +58,25 @@ class Deploy extends Component public function stop() { - instant_remote_process([ - "docker rm -f coolify-proxy", - ], $this->server); - $this->server->proxy->status = 'exited'; - $this->server->save(); - $this->emit('proxyStatusUpdated'); + try { + if ($this->server->isSwarm()) { + instant_remote_process([ + "docker service rm coolify-proxy_traefik", + ], $this->server); + $this->server->proxy->status = 'exited'; + $this->server->save(); + $this->emit('proxyStatusUpdated'); + } else { + instant_remote_process([ + "docker rm -f coolify-proxy", + ], $this->server); + $this->server->proxy->status = 'exited'; + $this->server->save(); + $this->emit('proxyStatusUpdated'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } } diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php index 8df8f10cd..0c5f274b5 100644 --- a/app/Http/Livewire/Server/Proxy/Status.php +++ b/app/Http/Livewire/Server/Proxy/Status.php @@ -16,7 +16,7 @@ class Status extends Component protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; public function startProxyPolling() { - $this->polling = true; + $this->checkProxy(); } public function proxyStatusUpdated() { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index bd6d7436a..d0f18d00c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -220,8 +220,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->application_deployment_queue->addLogEntry("Creating / updating stack."); $this->execute_remote_command( [ - "docker stack deploy --with-registry-auth --prune --compose-file {$this->configuration_dir}/docker-compose.yml {$this->application->uuid}" + executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}") ], + [ + "echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'" + ] ); } } @@ -376,7 +379,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $envs->push($env->key . '=' . $env->value); } } - ray($envs); $envs_base64 = base64_encode($envs->implode("\n")); $this->execute_remote_command( [ @@ -442,17 +444,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->cleanup_git(); $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); $yaml = Yaml::dump($composeFile->toArray(), 10); + ray($composeFile); + ray($this->container_name); $this->docker_compose_base64 = base64_encode($yaml); $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yaml"), "hidden" => true + executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true ]); $this->save_environment_variables(); $this->stop_running_container(force: true); + ray($this->pull_request_id); $networkId = $this->application->uuid; if ($this->pull_request_id !== 0) { $networkId = "{$this->application->uuid}-{$this->pull_request_id}"; } + ray($networkId); if ($this->server->isSwarm()) { // TODO } else { @@ -832,26 +838,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->env_args = $this->env_args->implode(' '); } - private function modify_compose_file() - { - // ray("{$this->workdir}{$this->docker_compose_location}"); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->docker_compose_location}"), "hidden" => true, "save" => 'compose_file']); - if ($this->saved_outputs->get('compose_file')) { - $compose = $this->saved_outputs->get('compose_file'); - } - try { - $yaml = Yaml::parse($compose); - } catch (\Exception $e) { - throw new \Exception($e->getMessage()); - } - $services = data_get($yaml, 'services'); - $topLevelNetworks = collect(data_get($yaml, 'networks', [])); - $definedNetwork = collect([$this->application->uuid]); - - $services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork) { - $serviceNetworks = collect(data_get($service, 'networks', [])); - }); - } private function generate_compose_file() { $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; @@ -952,10 +938,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ] ] ]; - } else { $docker_compose['services'][$this->container_name]['labels'] = $labels; - } if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { $docker_compose['services'][$this->container_name]['logging'] = [ diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 30324baab..7063e01cd 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -43,15 +43,37 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted return; }; if ($this->server->isSwarm()) { - + $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); + $containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); } else { - + $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); + $containerReplicase = null; } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); if (is_null($containers)) { return; } $containers = format_docker_command_output_to_json($containers); + if ($containerReplicase) { + $containerReplicase = format_docker_command_output_to_json($containerReplicase); + foreach ($containerReplicase as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $containers = $containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + return $container; + }); + } + } $applications = $this->server->applications(); $databases = $this->server->databases(); $services = $this->server->services()->get(); @@ -63,10 +85,16 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $foundServices = []; foreach ($containers as $container) { + if ($this->server->isSwarm()) { + $labels = data_get($container, 'Spec.Labels'); + $uuid = data_get($labels, 'coolify.name'); + } else { + $labels = data_get($container, 'Config.Labels'); + $uuid = data_get($labels, 'com.docker.compose.service'); + } $containerStatus = data_get($container, 'State.Status'); $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); $containerStatus = "$containerStatus ($containerHealth)"; - $labels = data_get($container, 'Config.Labels'); $labels = Arr::undot(format_docker_labels_to_json($labels)); $applicationId = data_get($labels, 'coolify.applicationId'); if ($applicationId) { @@ -98,7 +126,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted } } } else { - $uuid = data_get($labels, 'com.docker.compose.service'); if ($uuid) { $database = $databases->where('uuid', $uuid)->first(); if ($database) { @@ -253,7 +280,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted // Check if proxy is running $this->server->proxyType(); $foundProxyContainer = $containers->filter(function ($value, $key) { - return data_get($value, 'Name') === '/coolify-proxy'; + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } })->first(); if (!$foundProxyContainer) { try { diff --git a/app/Models/Application.php b/app/Models/Application.php index ea7e0a930..e2d93c7a1 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -603,6 +603,7 @@ class Application extends BaseModel { if ($this->docker_compose_raw) { $mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id); + ray($this->docker_compose_pr_raw); if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) { parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true); } @@ -614,7 +615,7 @@ class Application extends BaseModel function loadComposeFile($isInit = false) { $initialDockerComposeLocation = $this->docker_compose_location; - $initialDockerComposePrLocation = $this->docker_compose_pr_location; + // $initialDockerComposePrLocation = $this->docker_compose_pr_location; if ($this->build_pack === 'dockercompose') { if ($isInit && $this->docker_compose_raw) { return; @@ -623,11 +624,11 @@ class Application extends BaseModel ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); $workdir = rtrim($this->base_directory, '/'); $composeFile = $this->docker_compose_location; - $prComposeFile = $this->docker_compose_pr_location; - $fileList = collect([".$composeFile"]); - if ($composeFile !== $prComposeFile) { - $fileList->push(".$prComposeFile"); - } + // $prComposeFile = $this->docker_compose_pr_location; + $fileList = collect([".$workdir$composeFile"]); + // if ($composeFile !== $prComposeFile) { + // $fileList->push(".$prComposeFile"); + // } $commands = collect([ "mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}", $cloneCommand, @@ -645,24 +646,24 @@ class Application extends BaseModel $this->docker_compose_raw = $composeFileContent; $this->save(); } - if ($composeFile === $prComposeFile) { - $this->docker_compose_pr_raw = $composeFileContent; - $this->save(); - } else { - $commands = collect([ - "cd /tmp/{$uuid}", - "cat .$workdir$prComposeFile", - ]); - $composePrFileContent = instant_remote_process($commands, $this->destination->server, false); - if (!$composePrFileContent) { - $this->docker_compose_pr_location = $initialDockerComposePrLocation; - $this->save(); - throw new \Exception("Could not load compose file from $workdir$prComposeFile"); - } else { - $this->docker_compose_pr_raw = $composePrFileContent; - $this->save(); - } - } + // if ($composeFile === $prComposeFile) { + // $this->docker_compose_pr_raw = $composeFileContent; + // $this->save(); + // } else { + // $commands = collect([ + // "cd /tmp/{$uuid}", + // "cat .$workdir$prComposeFile", + // ]); + // $composePrFileContent = instant_remote_process($commands, $this->destination->server, false); + // if (!$composePrFileContent) { + // $this->docker_compose_pr_location = $initialDockerComposePrLocation; + // $this->save(); + // throw new \Exception("Could not load compose file from $workdir$prComposeFile"); + // } else { + // $this->docker_compose_pr_raw = $composePrFileContent; + // $this->save(); + // } + // } $commands = collect([ "rm -rf /tmp/{$uuid}", diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index d643cce6b..c46c4d558 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -93,7 +93,11 @@ function executeInDocker(string $containerId, string $command) function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false) { - $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); + if ($server->isSwarm()) { + $container = instant_remote_process(["docker service ls --filter 'name={$container_id}' --format '{{json .}}' "], $server, $throwError); + } else { + $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); + } if (!$container) { return 'exited'; } @@ -101,7 +105,19 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data if ($all_data) { return $container[0]; } - return data_get($container[0], 'State.Status', 'exited'); + if ($server->isSwarm()) { + $replicas = data_get($container[0], 'Replicas'); + $replicas = explode('/', $replicas); + $active = (int)$replicas[0]; + $total = (int)$replicas[1]; + if ($active === $total) { + return 'running'; + } else { + return 'starting'; + } + } else { + return data_get($container[0], 'State.Status', 'exited'); + } } function generateApplicationContainerName(Application $application, $pull_request_id = 0) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 8eb1fd443..3681a6c0c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -863,7 +863,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $key = Str::of($variableName); $value = Str::of($variable); } - // TODO: here is the problem if ($key->startsWith('SERVICE_FQDN')) { if ($isNew || $savedService->fqdn === null) { $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); @@ -1145,6 +1144,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_set($service, 'volumes', $serviceVolumes->toArray()); } } else { + // TODO } // Decide if the service is a database $isDatabase = isDatabaseImage(data_get_str($service, 'image')); diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index fb102c7c5..98a2c772a 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -207,10 +207,10 @@ placeholder="Username to connect to your server. Default is root." label="Username" id="remoteServerUser" /> -
+ {{--
-
+
--}} Check Connection diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 528fd32c5..b95d0e938 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -54,8 +54,8 @@ helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.Coolify does not install/setup Cloudflare (cloudflared) on your server." id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> @endif - + {{-- --}} {{-- --}} diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index 62268f2fe..6ec358fdc 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -25,10 +25,10 @@ @endif @endforeach -
+ {{--
-
+
--}} Save New Server diff --git a/resources/views/livewire/server/proxy/status.blade.php b/resources/views/livewire/server/proxy/status.blade.php index 65eebe037..e3184d061 100644 --- a/resources/views/livewire/server/proxy/status.blade.php +++ b/resources/views/livewire/server/proxy/status.blade.php @@ -1,6 +1,6 @@
@if ($server->isFunctional()) -
+
@if (data_get($server, 'proxy.status') === 'running') @elseif (data_get($server, 'proxy.status') === 'restarting') From 0dff57e69feade4f93a9a2d9d839761b92220017 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 29 Nov 2023 15:03:21 +0100 Subject: [PATCH 14/15] Add cleanup option to app:init command --- app/Console/Commands/Init.php | 57 ++++++++++--------- .../etc/s6-overlay/s6-rc.d/init-script/up | 2 +- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 4cf0f21bf..862fb7625 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -30,6 +30,7 @@ class Init extends Command $this->alive(); $cleanup = $this->option('cleanup'); if ($cleanup) { + echo "Running cleanup\n"; $this->cleanup_stucked_resources(); $this->cleanup_ssh(); } @@ -101,14 +102,14 @@ class Init extends Command ray('Application without environment', $application->name); $application->delete(); } - if (!data_get($application, 'destination.server')) { - ray('Application without server', $application->name); - $application->delete(); - } if (!$application->destination()) { ray('Application without destination', $application->name); $application->delete(); } + if (!data_get($application, 'destination.server')) { + ray('Application without server', $application->name); + $application->delete(); + } } } catch (\Throwable $e) { echo "Error in application: {$e->getMessage()}\n"; @@ -120,14 +121,14 @@ class Init extends Command ray('Postgresql without environment', $postgresql->name); $postgresql->delete(); } - if (!data_get($postgresql, 'destination.server')) { - ray('Postgresql without server', $postgresql->name); - $postgresql->delete(); - } if (!$postgresql->destination()) { ray('Postgresql without destination', $postgresql->name); $postgresql->delete(); } + if (!data_get($postgresql, 'destination.server')) { + ray('Postgresql without server', $postgresql->name); + $postgresql->delete(); + } } } catch (\Throwable $e) { echo "Error in postgresql: {$e->getMessage()}\n"; @@ -139,14 +140,14 @@ class Init extends Command ray('Redis without environment', $redis->name); $redis->delete(); } - if (!data_get($redis, 'destination.server')) { - ray('Redis without server', $redis->name); - $redis->delete(); - } if (!$redis->destination()) { ray('Redis without destination', $redis->name); $redis->delete(); } + if (!data_get($redis, 'destination.server')) { + ray('Redis without server', $redis->name); + $redis->delete(); + } } } catch (\Throwable $e) { echo "Error in redis: {$e->getMessage()}\n"; @@ -159,14 +160,14 @@ class Init extends Command ray('Mongodb without environment', $mongodb->name); $mongodb->delete(); } - if (!data_get($mongodb, 'destination.server')) { - ray('Mongodb without server', $mongodb->name); - $mongodb->delete(); - } if (!$mongodb->destination()) { ray('Mongodb without destination', $mongodb->name); $mongodb->delete(); } + if (!data_get($mongodb, 'destination.server')) { + ray('Mongodb without server', $mongodb->name); + $mongodb->delete(); + } } } catch (\Throwable $e) { echo "Error in mongodb: {$e->getMessage()}\n"; @@ -179,14 +180,14 @@ class Init extends Command ray('Mysql without environment', $mysql->name); $mysql->delete(); } - if (!data_get($mysql, 'destination.server')) { - ray('Mysql without server', $mysql->name); - $mysql->delete(); - } if (!$mysql->destination()) { ray('Mysql without destination', $mysql->name); $mysql->delete(); } + if (!data_get($mysql, 'destination.server')) { + ray('Mysql without server', $mysql->name); + $mysql->delete(); + } } } catch (\Throwable $e) { echo "Error in mysql: {$e->getMessage()}\n"; @@ -199,14 +200,14 @@ class Init extends Command ray('Mariadb without environment', $mariadb->name); $mariadb->delete(); } - if (!data_get($mariadb, 'destination.server')) { - ray('Mariadb without server', $mariadb->name); - $mariadb->delete(); - } if (!$mariadb->destination()) { ray('Mariadb without destination', $mariadb->name); $mariadb->delete(); } + if (!data_get($mariadb, 'destination.server')) { + ray('Mariadb without server', $mariadb->name); + $mariadb->delete(); + } } } catch (\Throwable $e) { echo "Error in mariadb: {$e->getMessage()}\n"; @@ -219,14 +220,14 @@ class Init extends Command ray('Service without environment', $service->name); $service->delete(); } - if (!data_get($service, 'server')) { - ray('Service without server', $service->name); - $service->delete(); - } if (!$service->destination()) { ray('Service without destination', $service->name); $service->delete(); } + if (!data_get($service, 'server')) { + ray('Service without server', $service->name); + $service->delete(); + } } } catch (\Throwable $e) { echo "Error in service: {$e->getMessage()}\n"; diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up index 09595f708..32492f6b7 100644 --- a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up +++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up @@ -1,2 +1,2 @@ #!/command/execlineb -P -php /var/www/html/artisan app:init +php /var/www/html/artisan app:init --cleanup From 7fe5eca66113918c7a2f5a1b3a61125b8742f653 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 29 Nov 2023 15:13:03 +0100 Subject: [PATCH 15/15] Add precheck for containers --- app/Jobs/ContainerStatusJob.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 7063e01cd..b99681455 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -46,6 +46,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); $containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); } else { + // Precheck for containers + $containers = instant_remote_process(["docker container ls -q"], $this->server); + if (!$containers) { + return; + } $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); $containerReplicase = null; }