From b4874c7df31a723d94913833124d2dd6330feaa6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 28 Nov 2023 18:31:04 +0100 Subject: [PATCH] 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