From 6e7ee0ca4893820de048ae47eb7b5ee22c76d424 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 24 May 2023 14:26:50 +0200 Subject: [PATCH] =?UTF-8?q?a=20ton=20=F0=9F=91=B7=E2=80=8D=E2=99=82?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Actions/CoolifyTask/RunRemoteProcess.php | 4 +- app/Console/Kernel.php | 13 +- .../Controllers/ApplicationController.php | 2 +- app/Http/Livewire/CheckUpdate.php | 2 +- app/Http/Livewire/Destination/Form.php | 2 +- .../Destination/New/StandaloneDocker.php | 2 +- app/Http/Livewire/ForceUpgrade.php | 2 +- app/Http/Livewire/PrivateKey/Change.php | 2 +- app/Http/Livewire/Profile/Form.php | 2 +- .../Livewire/Project/Application/Danger.php | 2 +- .../Livewire/Project/Application/Deploy.php | 55 +-- ...{PollDeployment.php => DeploymentLogs.php} | 2 +- .../Project/Application/Deployments.php | 25 ++ .../Application/EnvironmentVariable/Add.php | 2 +- .../Application/EnvironmentVariable/All.php | 2 +- .../Application/EnvironmentVariable/Show.php | 2 +- .../Livewire/Project/Application/General.php | 2 +- .../Project/Application/GetDeployments.php | 19 - .../Project/Application/ResourceLimits.php | 2 +- .../Livewire/Project/Application/Rollback.php | 29 +- .../Livewire/Project/Application/Status.php | 1 + .../Project/Application/Storages/Add.php | 2 +- .../Project/Application/Storages/All.php | 2 +- .../Livewire/Project/DeleteEnvironment.php | 2 +- app/Http/Livewire/Project/DeleteProject.php | 2 +- .../Livewire/Project/New/EmptyProject.php | 2 +- .../Project/New/GithubPrivateRepository.php | 6 +- .../New/GithubPrivateRepositoryDeployKey.php | 6 +- .../Project/New/PublicGitRepository.php | 6 +- app/Http/Livewire/RunCommand.php | 2 +- app/Http/Livewire/Server/Form.php | 2 +- app/Http/Livewire/Server/New/ByIp.php | 4 +- app/Http/Livewire/Server/PrivateKey.php | 2 +- app/Http/Livewire/Server/Proxy.php | 8 +- app/Http/Livewire/Source/Create.php | 4 +- app/Http/Livewire/Source/Github/Change.php | 8 +- ...onJob.php => ApplicationDeploymentJob.php} | 215 +++++----- app/Jobs/ContainerStatusJob.php | 30 +- app/Jobs/ContainerStopJob.php | 39 ++ ...toUpdateJob.php => InstanceAutoUpdate.php} | 6 +- ...magesJob.php => InstanceDockerCleanup.php} | 2 +- ...CheckJob.php => InstanceProxyCheckJob.php} | 4 +- app/Jobs/RollbackApplicationJob.php | 379 ------------------ app/Models/Application.php | 4 +- app/Models/ApplicationDeploymentQueue.php | 24 ++ bootstrap/helpers.php | 328 --------------- bootstrap/helpers/applications.php | 32 ++ bootstrap/helpers/docker.php | 40 ++ bootstrap/helpers/github.php | 47 +++ bootstrap/helpers/proxy.php | 74 ++++ bootstrap/helpers/remoteProcess.php | 102 +++++ bootstrap/helpers/shared.php | 54 +++ bootstrap/includeHelpers.php | 5 + composer.json | 2 +- ...te_application_deployment_queues_table.php | 31 ++ resources/css/app.css | 2 +- .../project/application/deploy.blade.php | 8 +- ...nt.blade.php => deployment-logs.blade.php} | 0 .../project/application/deployments.blade.php | 29 ++ .../application/get-deployments.blade.php | 19 - .../application/configuration.blade.php | 1 - .../project/application/deployment.blade.php | 2 +- .../project/application/deployments.blade.php | 8 +- routes/web.php | 6 +- routes/webhooks.php | 10 +- scripts/run | 3 + tests/Feature/DockerCommandsTest.php | 8 +- 67 files changed, 754 insertions(+), 992 deletions(-) rename app/Http/Livewire/Project/Application/{PollDeployment.php => DeploymentLogs.php} (95%) create mode 100644 app/Http/Livewire/Project/Application/Deployments.php delete mode 100644 app/Http/Livewire/Project/Application/GetDeployments.php rename app/Jobs/{DeployApplicationJob.php => ApplicationDeploymentJob.php} (84%) create mode 100644 app/Jobs/ContainerStopJob.php rename app/Jobs/{AutoUpdateJob.php => InstanceAutoUpdate.php} (93%) rename app/Jobs/{DockerCleanupDanglingImagesJob.php => InstanceDockerCleanup.php} (93%) rename app/Jobs/{ProxyCheckJob.php => InstanceProxyCheckJob.php} (90%) delete mode 100644 app/Jobs/RollbackApplicationJob.php create mode 100644 app/Models/ApplicationDeploymentQueue.php delete mode 100644 bootstrap/helpers.php create mode 100644 bootstrap/helpers/applications.php create mode 100644 bootstrap/helpers/docker.php create mode 100644 bootstrap/helpers/github.php create mode 100644 bootstrap/helpers/proxy.php create mode 100644 bootstrap/helpers/remoteProcess.php create mode 100644 bootstrap/helpers/shared.php create mode 100644 bootstrap/includeHelpers.php create mode 100644 database/migrations/2023_05_24_083426_create_application_deployment_queues_table.php rename resources/views/livewire/project/application/{poll-deployment.blade.php => deployment-logs.blade.php} (100%) create mode 100644 resources/views/livewire/project/application/deployments.blade.php delete mode 100644 resources/views/livewire/project/application/get-deployments.blade.php diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 877e95170..a35848f68 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -4,7 +4,7 @@ use App\Enums\ActivityTypes; use App\Enums\ProcessStatus; -use App\Jobs\DeployApplicationJob; +use App\Jobs\ApplicationDeploymentJob; use Illuminate\Process\ProcessResult; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; @@ -125,7 +125,7 @@ public function encodeOutput($type, $output) 'type' => $type, 'output' => $output, 'timestamp' => hrtime(true), - 'batch' => DeployApplicationJob::$batch_counter, + 'batch' => ApplicationDeploymentJob::$batch_counter, 'order' => $this->getLatestCounter(), ]; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f6369005a..61c6a9d36 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,10 +2,9 @@ namespace App\Console; -use App\Jobs\AutoUpdateJob; -use App\Jobs\ContainerStatusJob; -use App\Jobs\DockerCleanupDanglingImagesJob; -use App\Jobs\ProxyCheckJob; +use App\Jobs\InstanceAutoUpdate; +use App\Jobs\InstanceProxyCheckJob; +use App\Jobs\InstanceDockerCleanup; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -18,9 +17,9 @@ protected function schedule(Schedule $schedule): void { $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->job(new DockerCleanupDanglingImagesJob)->everyFiveMinutes(); - $schedule->job(new AutoUpdateJob)->everyFifteenMinutes(); - $schedule->job(new ProxyCheckJob)->everyMinute(); + $schedule->job(new InstanceDockerCleanup)->everyFiveMinutes(); + $schedule->job(new InstanceAutoUpdate)->everyFifteenMinutes(); + $schedule->job(new InstanceProxyCheckJob)->everyMinute(); } /** diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 450abac73..b0c76ec5c 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -37,7 +37,7 @@ public function deployments() if (!$application) { return redirect()->route('dashboard'); } - return view('project.application.deployments', ['application' => $application, 'deployments' => $application->deployments()]); + return view('project.application.deployments', ['application' => $application]); } public function deployment() diff --git a/app/Http/Livewire/CheckUpdate.php b/app/Http/Livewire/CheckUpdate.php index 6330852c0..5c0cee39b 100644 --- a/app/Http/Livewire/CheckUpdate.php +++ b/app/Http/Livewire/CheckUpdate.php @@ -13,7 +13,7 @@ class CheckUpdate extends Component public function checkUpdate() { - $this->latestVersion = getLatestVersionOfCoolify(); + $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('version'); if ($this->latestVersion === 'latest') { $this->updateAvailable = true; diff --git a/app/Http/Livewire/Destination/Form.php b/app/Http/Livewire/Destination/Form.php index 3643e6782..9bc2369f8 100644 --- a/app/Http/Livewire/Destination/Form.php +++ b/app/Http/Livewire/Destination/Form.php @@ -31,7 +31,7 @@ public function delete() $this->destination->delete(); return redirect()->route('dashboard'); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } } diff --git a/app/Http/Livewire/Destination/New/StandaloneDocker.php b/app/Http/Livewire/Destination/New/StandaloneDocker.php index f7ce86713..1ebb3824e 100644 --- a/app/Http/Livewire/Destination/New/StandaloneDocker.php +++ b/app/Http/Livewire/Destination/New/StandaloneDocker.php @@ -33,7 +33,7 @@ public function mount() } } $this->network = new Cuid2(7); - $this->name = generateRandomName(); + $this->name = generate_random_name(); } public function submit() diff --git a/app/Http/Livewire/ForceUpgrade.php b/app/Http/Livewire/ForceUpgrade.php index 52c9a28ad..033f3ef43 100644 --- a/app/Http/Livewire/ForceUpgrade.php +++ b/app/Http/Livewire/ForceUpgrade.php @@ -23,7 +23,7 @@ public function upgrade() ], $server, ActivityTypes::INLINE->value); $this->emit('updateInitiated'); } else { - $latestVersion = getLatestVersionOfCoolify(); + $latestVersion = get_latest_version_of_coolify(); $cdn = "https://coolify-cdn.b-cdn.net/files"; $server = Server::where('ip', 'host.docker.internal')->first(); diff --git a/app/Http/Livewire/PrivateKey/Change.php b/app/Http/Livewire/PrivateKey/Change.php index d8d9e3ffd..27889feb1 100644 --- a/app/Http/Livewire/PrivateKey/Change.php +++ b/app/Http/Livewire/PrivateKey/Change.php @@ -35,7 +35,7 @@ public function changePrivateKey() $this->private_key->save(); session('currentTeam')->privateKeys = PrivateKey::where('team_id', session('currentTeam')->id)->get(); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Profile/Form.php b/app/Http/Livewire/Profile/Form.php index ceefbd4e6..4b463b4d3 100644 --- a/app/Http/Livewire/Profile/Form.php +++ b/app/Http/Livewire/Profile/Form.php @@ -29,7 +29,7 @@ public function submit() 'name' => $this->name, ]); } catch (\Throwable $error) { - return generalErrorHandler($error, $this); + return general_error_handler($error, $this); } } } diff --git a/app/Http/Livewire/Project/Application/Danger.php b/app/Http/Livewire/Project/Application/Danger.php index 55eb7e084..33e8a613b 100644 --- a/app/Http/Livewire/Project/Application/Danger.php +++ b/app/Http/Livewire/Project/Application/Danger.php @@ -12,7 +12,7 @@ class Danger extends Component public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function delete() { diff --git a/app/Http/Livewire/Project/Application/Deploy.php b/app/Http/Livewire/Project/Application/Deploy.php index 71dc5318c..a935b2ad8 100644 --- a/app/Http/Livewire/Project/Application/Deploy.php +++ b/app/Http/Livewire/Project/Application/Deploy.php @@ -2,10 +2,8 @@ namespace App\Http\Livewire\Project\Application; -use App\Jobs\ContainerStatusJob; -use App\Jobs\DeployApplicationJob; +use App\Jobs\ContainerStopJob; use App\Models\Application; -use Illuminate\Support\Facades\Route; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -24,52 +22,37 @@ class Deploy extends Component public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->application = Application::where('id', $this->applicationId)->first(); $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); - // dispatch(new ContainerStatusJob($this->application->uuid)); } - protected function setDeploymentUuid() + protected function set_deployment_uuid() { // Create Deployment ID $this->deployment_uuid = new Cuid2(7); $this->parameters['deployment_uuid'] = $this->deployment_uuid; } - protected function redirectToDeployment() + public function deploy(bool $force = false) { - return redirect()->route('project.application.deployment', $this->parameters); - } - public function start() - { - $this->setDeploymentUuid(); + $this->set_deployment_uuid(); - dispatch(new DeployApplicationJob( - deployment_uuid: $this->deployment_uuid, - application_uuid: $this->application->uuid, - force_rebuild: false, - )); - - return $this->redirectToDeployment(); - } - public function forceRebuild() - { - $this->setDeploymentUuid(); - - dispatch(new DeployApplicationJob( - deployment_uuid: $this->deployment_uuid, - application_uuid: $this->application->uuid, - force_rebuild: true, - )); - - return $this->redirectToDeployment(); + queue_application_deployment( + application: $this->application, + metadata: [ + 'deployment_uuid' => $this->deployment_uuid, + 'application_uuid' => $this->application->uuid, + 'force_rebuild' => $force, + ] + ); + return redirect()->route('project.application.deployments', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'environment_name' => $this->parameters['environment_name'], + ]); } public function stop() { - instantRemoteProcess(["docker rm -f {$this->application->uuid}"], $this->destination->server); - if ($this->application->status != 'exited') { - $this->application->status = 'exited'; - $this->application->save(); - } + dispatch(new ContainerStopJob($this->application->id, $this->destination->server)); } } diff --git a/app/Http/Livewire/Project/Application/PollDeployment.php b/app/Http/Livewire/Project/Application/DeploymentLogs.php similarity index 95% rename from app/Http/Livewire/Project/Application/PollDeployment.php rename to app/Http/Livewire/Project/Application/DeploymentLogs.php index 2e38ab292..fc7d9e7c0 100644 --- a/app/Http/Livewire/Project/Application/PollDeployment.php +++ b/app/Http/Livewire/Project/Application/DeploymentLogs.php @@ -7,7 +7,7 @@ use Livewire\Component; use Spatie\Activitylog\Models\Activity; -class PollDeployment extends Component +class DeploymentLogs extends Component { public $activity; public $isKeepAliveOn = true; diff --git a/app/Http/Livewire/Project/Application/Deployments.php b/app/Http/Livewire/Project/Application/Deployments.php new file mode 100644 index 000000000..1c16d82c5 --- /dev/null +++ b/app/Http/Livewire/Project/Application/Deployments.php @@ -0,0 +1,25 @@ +current_url = url()->current(); + } + public function reloadDeployments() + { + $this->loadDeployments(); + } + public function loadDeployments() + { + $this->deployments = Application::find($this->application_id)->deployments(); + } +} diff --git a/app/Http/Livewire/Project/Application/EnvironmentVariable/Add.php b/app/Http/Livewire/Project/Application/EnvironmentVariable/Add.php index e169aa28a..6c3eebb28 100644 --- a/app/Http/Livewire/Project/Application/EnvironmentVariable/Add.php +++ b/app/Http/Livewire/Project/Application/EnvironmentVariable/Add.php @@ -23,7 +23,7 @@ class Add extends Component ]; public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function submit() { diff --git a/app/Http/Livewire/Project/Application/EnvironmentVariable/All.php b/app/Http/Livewire/Project/Application/EnvironmentVariable/All.php index 4d7836b5f..99366e5e8 100644 --- a/app/Http/Livewire/Project/Application/EnvironmentVariable/All.php +++ b/app/Http/Livewire/Project/Application/EnvironmentVariable/All.php @@ -26,7 +26,7 @@ public function submit($data) $this->application->refresh(); $this->emit('clearAddEnv'); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/Application/EnvironmentVariable/Show.php b/app/Http/Livewire/Project/Application/EnvironmentVariable/Show.php index f09df0e49..9d58776f0 100644 --- a/app/Http/Livewire/Project/Application/EnvironmentVariable/Show.php +++ b/app/Http/Livewire/Project/Application/EnvironmentVariable/Show.php @@ -17,7 +17,7 @@ class Show extends Component ]; public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function submit() { diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index 8e3a9afd2..bf24024e6 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -112,7 +112,7 @@ public function submit() $this->application->fqdn = $domains->implode(','); $this->application->save(); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/Application/GetDeployments.php b/app/Http/Livewire/Project/Application/GetDeployments.php deleted file mode 100644 index 2df15d0c4..000000000 --- a/app/Http/Livewire/Project/Application/GetDeployments.php +++ /dev/null @@ -1,19 +0,0 @@ -type_uuid', '=', $this->deployment_uuid)->first(); - $this->created_at = $activity->created_at; - $this->status = data_get($activity, 'properties.status'); - } -} diff --git a/app/Http/Livewire/Project/Application/ResourceLimits.php b/app/Http/Livewire/Project/Application/ResourceLimits.php index d3f1096f8..e0377b75e 100644 --- a/app/Http/Livewire/Project/Application/ResourceLimits.php +++ b/app/Http/Livewire/Project/Application/ResourceLimits.php @@ -45,7 +45,7 @@ public function submit() $this->validate(); $this->application->save(); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/Application/Rollback.php b/app/Http/Livewire/Project/Application/Rollback.php index ee131133a..1ee01a746 100644 --- a/app/Http/Livewire/Project/Application/Rollback.php +++ b/app/Http/Livewire/Project/Application/Rollback.php @@ -2,8 +2,6 @@ namespace App\Http\Livewire\Project\Application; -use App\Jobs\DeployApplicationJob; -use App\Jobs\RollbackApplicationJob; use App\Models\Application; use Livewire\Component; use Illuminate\Support\Str; @@ -18,20 +16,27 @@ class Rollback extends Component public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function rollbackImage($tag) { $deployment_uuid = new Cuid2(7); - dispatch(new RollbackApplicationJob( - deployment_uuid: $deployment_uuid, - application_uuid: $this->application->uuid, - commit: $tag, - )); + queue_application_deployment( + application: $this->application, + metadata: [ + 'deployment_uuid' => $deployment_uuid, + 'application_uuid' => $this->application->uuid, + 'force_rebuild' => false, + 'commit' => $tag, + ] + ); - $this->parameters['deployment_uuid'] = $deployment_uuid; - return redirect()->route('project.application.deployment', $this->parameters); + return redirect()->route('project.application.deployments', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'environment_name' => $this->parameters['environment_name'], + ]); } public function loadImages() { @@ -51,7 +56,7 @@ public function loadImages() })->map(function ($item) { $item = Str::of($item)->explode('#'); if ($item[1] === $this->current) { - $is_current = true; + // $is_current = true; } return [ 'tag' => $item[1], @@ -60,7 +65,7 @@ public function loadImages() ]; })->toArray(); } catch (\Throwable $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/Application/Status.php b/app/Http/Livewire/Project/Application/Status.php index 3f68dc35c..47008f305 100644 --- a/app/Http/Livewire/Project/Application/Status.php +++ b/app/Http/Livewire/Project/Application/Status.php @@ -8,6 +8,7 @@ class Status extends Component { public Application $application; + public function pollingStatus() { $this->application->refresh(); diff --git a/app/Http/Livewire/Project/Application/Storages/Add.php b/app/Http/Livewire/Project/Application/Storages/Add.php index f0d66780c..6bb7557f1 100644 --- a/app/Http/Livewire/Project/Application/Storages/Add.php +++ b/app/Http/Livewire/Project/Application/Storages/Add.php @@ -23,7 +23,7 @@ class Add extends Component ]; public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function submit() { diff --git a/app/Http/Livewire/Project/Application/Storages/All.php b/app/Http/Livewire/Project/Application/Storages/All.php index a8606c175..0d1d2966b 100644 --- a/app/Http/Livewire/Project/Application/Storages/All.php +++ b/app/Http/Livewire/Project/Application/Storages/All.php @@ -27,7 +27,7 @@ public function submit($data) $this->application->refresh(); $this->emit('clearAddStorage'); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/DeleteEnvironment.php b/app/Http/Livewire/Project/DeleteEnvironment.php index a6cb3716c..a009b268e 100644 --- a/app/Http/Livewire/Project/DeleteEnvironment.php +++ b/app/Http/Livewire/Project/DeleteEnvironment.php @@ -14,7 +14,7 @@ class DeleteEnvironment extends Component public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function delete() { diff --git a/app/Http/Livewire/Project/DeleteProject.php b/app/Http/Livewire/Project/DeleteProject.php index 8e4910b6d..89bd7bc5e 100644 --- a/app/Http/Livewire/Project/DeleteProject.php +++ b/app/Http/Livewire/Project/DeleteProject.php @@ -13,7 +13,7 @@ class DeleteProject extends Component public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); } public function delete() { diff --git a/app/Http/Livewire/Project/New/EmptyProject.php b/app/Http/Livewire/Project/New/EmptyProject.php index c5a4c36ea..931348a22 100644 --- a/app/Http/Livewire/Project/New/EmptyProject.php +++ b/app/Http/Livewire/Project/New/EmptyProject.php @@ -10,7 +10,7 @@ class EmptyProject extends Component public function createEmptyProject() { $project = Project::create([ - 'name' => generateRandomName(), + 'name' => generate_random_name(), 'team_id' => session('currentTeam')->id, ]); return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']); diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepository.php b/app/Http/Livewire/Project/New/GithubPrivateRepository.php index c5cfabedd..1ca8cade6 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepository.php @@ -110,7 +110,7 @@ public function submit() $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $application = Application::create([ - 'name' => generateRandomName(), + 'name' => generate_random_name(), 'repository_project_id' => $this->selected_repository_id, 'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}", 'git_branch' => $this->selected_branch_name, @@ -129,12 +129,12 @@ public function submit() 'environment_name' => $environment->name ]); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->query = request()->query(); $this->repositories = $this->branches = collect(); $this->github_apps = GithubApp::private(); diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index d7d643dca..71860358c 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -34,7 +34,7 @@ public function mount() if (config('app.env') === 'local') { $this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify'; } - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->query = request()->query(); $this->private_keys = PrivateKey::where('team_id', session('currentTeam')->id)->get(); } @@ -74,7 +74,7 @@ public function submit() $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $application_init = [ - 'name' => generateRandomName(), + 'name' => generate_random_name(), 'git_repository' => $git_repository, 'git_branch' => $git_branch, 'git_full_url' => "git@$git_host:$git_repository.git", @@ -96,7 +96,7 @@ public function submit() 'application_uuid' => $application->uuid, ]); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Project/New/PublicGitRepository.php b/app/Http/Livewire/Project/New/PublicGitRepository.php index c5a9f160e..782c75998 100644 --- a/app/Http/Livewire/Project/New/PublicGitRepository.php +++ b/app/Http/Livewire/Project/New/PublicGitRepository.php @@ -37,7 +37,7 @@ public function mount() $this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify'; $this->port = 3000; } - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->query = request()->query(); } @@ -78,7 +78,7 @@ public function submit() $application_init = [ - 'name' => generateRandomName(), + 'name' => generate_random_name(), 'git_repository' => $git_repository, 'git_branch' => $git_branch, 'build_pack' => 'nixpacks', @@ -106,7 +106,7 @@ public function submit() 'application_uuid' => $application->uuid, ]); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } } diff --git a/app/Http/Livewire/RunCommand.php b/app/Http/Livewire/RunCommand.php index 55be8fe23..2db18ab84 100755 --- a/app/Http/Livewire/RunCommand.php +++ b/app/Http/Livewire/RunCommand.php @@ -29,7 +29,7 @@ public function runCommand() $activity = remoteProcess([$this->command], Server::where('uuid', $this->server)->first(), ActivityTypes::INLINE->value); $this->emit('newMonitorActivity', $activity->id); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } } diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index d14bbcd21..2e7522ecd 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -56,7 +56,7 @@ public function validateServer() $this->dockerComposeVersion = 'Not installed.'; } } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } public function delete() diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index d2a4f1b3c..632ae89ef 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -30,7 +30,7 @@ class ByIp extends Component ]; public function mount() { - $this->name = generateRandomName(); + $this->name = generate_random_name(); $this->private_key_id = $this->private_keys->first()->id; } public function setPrivateKey(string $private_key_id) @@ -61,7 +61,7 @@ public function submit() $server->settings->save(); return redirect()->route('server.show', $server->uuid); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } } diff --git a/app/Http/Livewire/Server/PrivateKey.php b/app/Http/Livewire/Server/PrivateKey.php index e9d6cf072..bafaa89c2 100644 --- a/app/Http/Livewire/Server/PrivateKey.php +++ b/app/Http/Livewire/Server/PrivateKey.php @@ -25,7 +25,7 @@ public function setPrivateKey($private_key_id) } public function mount() { - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->private_keys = ModelsPrivateKey::where('team_id', session('currentTeam')->id)->get(); } } diff --git a/app/Http/Livewire/Server/Proxy.php b/app/Http/Livewire/Server/Proxy.php index 078597382..dc5ba8adf 100644 --- a/app/Http/Livewire/Server/Proxy.php +++ b/app/Http/Livewire/Server/Proxy.php @@ -39,7 +39,7 @@ public function installProxy() public function proxyStatus() { - $this->server->extra_attributes->proxy_status = checkContainerStatus(server: $this->server, container_id: 'coolify-proxy'); + $this->server->extra_attributes->proxy_status = get_container_status(server: $this->server, container_id: 'coolify-proxy'); $this->server->save(); $this->server->refresh(); } @@ -69,7 +69,7 @@ public function saveConfiguration() "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", ], $this->server); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } public function resetProxy() @@ -77,7 +77,7 @@ public function resetProxy() try { $this->proxy_settings = resolve(CheckProxySettingsInSync::class)($this->server, true); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } public function checkProxySettingsInSync() @@ -85,7 +85,7 @@ public function checkProxySettingsInSync() try { $this->proxy_settings = resolve(CheckProxySettingsInSync::class)($this->server); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } } } diff --git a/app/Http/Livewire/Source/Create.php b/app/Http/Livewire/Source/Create.php index f7f09bba7..97948ee20 100644 --- a/app/Http/Livewire/Source/Create.php +++ b/app/Http/Livewire/Source/Create.php @@ -17,7 +17,7 @@ class Create extends Component public function mount() { - $this->name = generateRandomName(); + $this->name = generate_random_name(); } public function createGitHubApp() { @@ -43,7 +43,7 @@ public function createGitHubApp() ]); redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Http/Livewire/Source/Github/Change.php b/app/Http/Livewire/Source/Github/Change.php index a01a0a1a4..ee06e1748 100644 --- a/app/Http/Livewire/Source/Github/Change.php +++ b/app/Http/Livewire/Source/Github/Change.php @@ -35,7 +35,7 @@ public function submit() $this->validate(); $this->github_app->save(); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } public function instantSave() @@ -45,7 +45,7 @@ public function instantSave() $this->github_app->save(); $this->emit('saved', 'GitHub settings updated!'); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } public function mount() @@ -54,7 +54,7 @@ public function mount() if ($settings->fqdn) { $this->host = $settings->fqdn; } - $this->parameters = getParameters(); + $this->parameters = get_parameters(); $this->is_system_wide = $this->github_app->is_system_wide; } public function delete() @@ -63,7 +63,7 @@ public function delete() $this->github_app->delete(); redirect()->route('dashboard'); } catch (\Exception $e) { - return generalErrorHandler($e, $this); + return general_error_handler($e, $this); } } } diff --git a/app/Jobs/DeployApplicationJob.php b/app/Jobs/ApplicationDeploymentJob.php similarity index 84% rename from app/Jobs/DeployApplicationJob.php rename to app/Jobs/ApplicationDeploymentJob.php index a683d2b4a..f01ea0b35 100644 --- a/app/Jobs/DeployApplicationJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -7,6 +7,7 @@ use App\Enums\ActivityTypes; use App\Enums\ProcessStatus; use App\Models\Application; +use App\Models\ApplicationDeploymentQueue; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -18,36 +19,41 @@ use Symfony\Component\Yaml\Yaml; use Illuminate\Support\Str; use Spatie\Url\Url; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Throwable; -class DeployApplicationJob implements ShouldQueue +class ApplicationDeploymentJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - protected Application $application; - protected $destination; - protected $source; - protected Activity $activity; - protected string $git_commit; - protected string $workdir; - protected string $docker_compose; - protected $build_args; - protected $env_args; - public static int $batch_counter = 0; - public $timeout = 3600; - public $tries = 60; + private Application $application; + private ApplicationDeploymentQueue $application_deployment_queue; + private $destination; + private $source; + private Activity $activity; - public function middleware(): array - { - return [(new WithoutOverlapping($this->application->uuid))->releaseAfter(10)]; - } + private string|null $git_commit = null; + private string $workdir; + private string $docker_compose; + private $build_args; + private $env_args; + + public static int $batch_counter = 0; + public $timeout = 10200; public function __construct( + public int $application_deployment_queue_id, public string $deployment_uuid, public string $application_uuid, public bool $force_rebuild = false, + public string|null $commit = null, ) { + $this->application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id); + $this->application_deployment_queue->update([ + 'status' => ProcessStatus::IN_PROGRESS->value, + ]); + if ($this->commit) { + $this->git_commit = $this->commit; + } $this->application = Application::query() ->where('uuid', $this->application_uuid) ->firstOrFail(); @@ -73,35 +79,7 @@ public function __construct( ->event(ActivityTypes::DEPLOYMENT->value) ->log("[]"); } - protected function stopRunningContainer() - { - $this->executeNow([ - "echo -n 'Removing old instance... '", - $this->execute_in_builder("docker rm -f {$this->application->uuid} >/dev/null 2>&1"), - "echo 'Done.'", - "echo -n 'Starting your application... '", - ]); - } - protected function startByComposeFile() - { - $this->executeNow([ - $this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), - ], isDebuggable: true); - $this->executeNow([ - "echo 'Done. 🎉'", - ], isFinished: true); - } - protected function generateComposeFile() - { - $this->docker_compose = $this->generate_docker_compose(); - $docker_compose_base64 = base64_encode($this->docker_compose); - $this->executeNow([ - $this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml") - ], hideFromOutput: true); - } - /** - * Execute the job. - */ + public function handle(): void { try { @@ -112,69 +90,74 @@ public function handle(): void $this->workdir = "/artifacts/{$this->deployment_uuid}"; // Pull builder image - $this->executeNow([ + $this->execute_now([ "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}...'", "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder)... '", ]); - $this->executeNow([ + $this->execute_now([ "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder", ], isDebuggable: true); - // Import git repository - $this->executeNow([ - "echo 'Done.'", - "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}... '" - ]); - - $this->executeNow([ - ...$this->gitImport(), - ], 'importing_git_repository'); - - $this->executeNow([ + $this->execute_now([ "echo 'Done.'" ]); - // Get git commit - $this->executeNow([$this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD")], 'commit_sha', hideFromOutput: true); - $this->git_commit = $this->activity->properties->get('commit_sha'); + if (is_null($this->git_commit)) { + // Import git repository + $this->execute_now([ + "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}... '" + ]); + + $this->execute_now([ + ...$this->gitImport(), + ], 'importing_git_repository'); + + $this->execute_now([ + "echo 'Done.'" + ]); + // Get git commit + $this->execute_now([$this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD")], 'commit_sha', hideFromOutput: true); + $this->git_commit = $this->activity->properties->get('commit_sha'); + } if (!$this->force_rebuild) { - $this->executeNow([ + $this->execute_now([ "docker images -q {$this->application->uuid}:{$this->git_commit} 2>/dev/null", ], 'local_image_found', hideFromOutput: true, ignoreErrors: true); $image_found = Str::of($this->activity->properties->get('local_image_found'))->trim()->isNotEmpty(); if ($image_found) { - $this->executeNow([ + $this->execute_now([ "echo 'Docker Image found locally with the same Git Commit SHA. Build skipped...'" ]); // Generate docker-compose.yml - $this->generateComposeFile(); + $this->generate_compose_file(); // Stop running container - $this->stopRunningContainer(); + $this->stop_running_container(); // Start application - $this->startByComposeFile(); + $this->start_by_compose_file(); + $this->next(ProcessStatus::FINISHED->value); return; } } - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("rm -fr {$this->workdir}/.git") ], hideFromOutput: true); - $this->executeNow([ + $this->execute_now([ "echo -n 'Generating nixpacks configuration... '", ]); - $this->executeNow([ + $this->execute_now([ $this->nixpacks_build_cmd(), $this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile"), $this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile"), ], isDebuggable: true); // Generate docker-compose.yml - $this->generateComposeFile(); - $this->executeNow([ + $this->generate_compose_file(); + $this->execute_now([ "echo 'Done.'", "echo -n 'Building image... '", ]); @@ -183,7 +166,7 @@ public function handle(): void $this->add_build_env_variables_to_dockerfile(); if ($this->application->settings->is_static) { - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->application->uuid}:{$this->git_commit}-build {$this->workdir}"), ], isDebuggable: true); @@ -193,55 +176,65 @@ public function handle(): void COPY --from={$this->application->uuid}:{$this->git_commit}-build /app/{$this->application->publish_directory} ."; $docker_file = base64_encode($dockerfile); - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("echo '{$docker_file}' | base64 -d > {$this->workdir}/Dockerfile-prod"), $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t {$this->application->uuid}:{$this->git_commit} {$this->workdir}"), ], hideFromOutput: true); } else { - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->application->uuid}:{$this->git_commit} {$this->workdir}"), ], isDebuggable: true); } - $this->executeNow([ + $this->execute_now([ "echo 'Done.'", ]); // Stop running container - $this->stopRunningContainer(); + $this->stop_running_container(); // Start application - $this->startByComposeFile(); + $this->start_by_compose_file(); + + $this->next(ProcessStatus::FINISHED->value); } catch (\Exception $e) { - $this->executeNow([ + $this->execute_now([ "echo '\nOops something is not okay, are you okay? 😢'", "echo '\n\n{$e->getMessage()}'", ]); - $this->fail($e->getMessage()); + $this->fail(); } finally { - // Saving docker-compose.yml if (isset($this->docker_compose)) { Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose); } - $this->executeNow(["docker rm -f {$this->deployment_uuid} >/dev/null 2>&1"], hideFromOutput: true); - // dispatch(new ContainerStatusJob($this->application_uuid)); + $this->execute_now(["docker rm -f {$this->deployment_uuid} >/dev/null 2>&1"], hideFromOutput: true); } } - public function failed(Throwable $exception): void + public function failed(): void { - $outputStack[] = [ - 'type' => 'out', - 'output' => $exception->getMessage(), - 'timestamp' => hrtime(true), - 'batch' => DeployApplicationJob::$batch_counter, - 'order' => 0, - ]; - $this->activity->description = json_encode($outputStack); $this->activity->properties = $this->activity->properties->merge([ - 'exitCode' => 0, + 'exitCode' => 1, 'status' => ProcessStatus::ERROR->value, ]); $this->activity->save(); - $this->fail($exception); + $this->next(ProcessStatus::ERROR->value); + $this->fail(); + } + + private function next(string $status) + { + dispatch(new ContainerStatusJob($this->application->id)); + $this->application_deployment_queue->update([ + 'status' => $status, + ]); + $next_found = ApplicationDeploymentQueue::where('application_id', $this->application->id)->where('status', 'queued')->first(); + if ($next_found) { + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next_found->id, + deployment_uuid: $next_found->metadata['deployment_uuid'], + application_uuid: $next_found->metadata['application_uuid'], + force_rebuild: $next_found->metadata['force_rebuild'], + )); + } } private function execute_in_builder(string $command) { @@ -278,7 +271,7 @@ private function generate_build_env_variables() } private function add_build_env_variables_to_dockerfile() { - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("cat {$this->workdir}/Dockerfile") ], propertyName: 'dockerfile', hideFromOutput: true); $dockerfile = collect(Str::of($this->activity->properties->get('dockerfile'))->trim()->explode("\n")); @@ -287,7 +280,7 @@ private function add_build_env_variables_to_dockerfile() $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->executeNow([ + $this->execute_now([ $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile") ], hideFromOutput: true); } @@ -437,7 +430,7 @@ private function set_labels_for_applications() return $labels; } - private function executeNow( + private function execute_now( array|Collection $command, string $propertyName = null, bool $isFinished = false, @@ -544,4 +537,30 @@ private function nixpacks_build_cmd() $nixpacks_command .= " {$this->workdir}"; return $this->execute_in_builder($nixpacks_command); } + private function stop_running_container() + { + $this->execute_now([ + "echo -n 'Removing old instance... '", + $this->execute_in_builder("docker rm -f {$this->application->uuid} >/dev/null 2>&1"), + "echo 'Done.'", + "echo -n 'Starting your application... '", + ]); + } + private function start_by_compose_file() + { + $this->execute_now([ + $this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), + ], isDebuggable: true); + $this->execute_now([ + "echo 'Done. 🎉'", + ], isFinished: true); + } + private function generate_compose_file() + { + $this->docker_compose = $this->generate_docker_compose(); + $docker_compose_base64 = base64_encode($this->docker_compose); + $this->execute_now([ + $this->execute_in_builder("mkdir -p {$this->workdir} && echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml") + ], hideFromOutput: true); + } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 85be298c9..2b3209ffc 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -16,27 +16,31 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeUnique { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + private Application $application; public function __construct( - public string|null $container_id = null, + public string|null $application_id = null, ) { + if ($this->application_id) { + $this->application = Application::find($this->application_id)->first(); + } } public function uniqueId(): string { - return $this->container_id; + return $this->application_id; } public function handle(): void { try { - if ($this->container_id) { - $this->checkContainerStatus(); + if ($this->application->uuid) { + $this->check_container_status(); } else { - $this->checkAllServers(); + $this->check_all_servers(); } } catch (\Exception $e) { Log::error($e->getMessage()); } } - protected function checkAllServers() + protected function check_all_servers() { $servers = Server::all()->reject(fn (Server $server) => $server->settings->is_build_server); $applications = Application::all(); @@ -44,7 +48,7 @@ protected function checkAllServers() $containers = collect(); foreach ($servers as $server) { $output = instantRemoteProcess(['docker ps -a -q --format \'{{json .}}\''], $server); - $containers = $containers->concat(formatDockerCmdOutputToJson($output)); + $containers = $containers->concat(format_docker_command_output_to_json($output)); } foreach ($containers as $container) { $found_application = $applications->filter(function ($value, $key) use ($container) { @@ -65,15 +69,11 @@ protected function checkAllServers() Log::info('Not found application: ' . $not_found_application->uuid . '. Set status to: ' . $not_found_application->status); } } - protected function checkContainerStatus() + protected function check_container_status() { - $application = Application::where('uuid', $this->container_id)->firstOrFail(); - if (!$application) { - return; - } - if ($application->destination->server) { - $application->status = checkContainerStatus(server: $application->destination->server, container_id: $this->container_id); - $application->save(); + if ($this->application->destination->server) { + $this->application->status = get_container_status(server: $this->application->destination->server, container_id: $this->application->uuid); + $this->application->save(); } } } diff --git a/app/Jobs/ContainerStopJob.php b/app/Jobs/ContainerStopJob.php new file mode 100644 index 000000000..d1cdb18a2 --- /dev/null +++ b/app/Jobs/ContainerStopJob.php @@ -0,0 +1,39 @@ +application_id; + } + public function handle(): void + { + try { + $application = Application::find($this->application_id)->first(); + instantRemoteProcess(["docker rm -f {$application->uuid}"], $this->server); + $application->status = get_container_status(server: $application->destination->server, container_id: $application->uuid); + $application->save(); + } catch (\Exception $e) { + Log::error($e->getMessage()); + } + } +} diff --git a/app/Jobs/AutoUpdateJob.php b/app/Jobs/InstanceAutoUpdate.php similarity index 93% rename from app/Jobs/AutoUpdateJob.php rename to app/Jobs/InstanceAutoUpdate.php index dc1927efd..2955772b2 100644 --- a/app/Jobs/AutoUpdateJob.php +++ b/app/Jobs/InstanceAutoUpdate.php @@ -12,7 +12,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class AutoUpdateJob implements ShouldQueue +class InstanceAutoUpdate implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -33,7 +33,7 @@ public function __construct() public function handle(): void { if (config('app.env') === 'local') { - $latest_version = getLatestVersionOfCoolify(); + $latest_version = get_latest_version_of_coolify(); $current_version = config('version'); if ($latest_version === $current_version) { return; @@ -53,7 +53,7 @@ public function handle(): void "sleep 10" ], $server, ActivityTypes::INLINE->value); } else { - $latest_version = getLatestVersionOfCoolify(); + $latest_version = get_latest_version_of_coolify(); $current_version = config('version'); if ($latest_version === $current_version) { return; diff --git a/app/Jobs/DockerCleanupDanglingImagesJob.php b/app/Jobs/InstanceDockerCleanup.php similarity index 93% rename from app/Jobs/DockerCleanupDanglingImagesJob.php rename to app/Jobs/InstanceDockerCleanup.php index f0aeea9f0..253fac815 100644 --- a/app/Jobs/DockerCleanupDanglingImagesJob.php +++ b/app/Jobs/InstanceDockerCleanup.php @@ -11,7 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class DockerCleanupDanglingImagesJob implements ShouldQueue +class InstanceDockerCleanup implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 500; diff --git a/app/Jobs/ProxyCheckJob.php b/app/Jobs/InstanceProxyCheckJob.php similarity index 90% rename from app/Jobs/ProxyCheckJob.php rename to app/Jobs/InstanceProxyCheckJob.php index 4c7ce12e6..f42d7cff2 100755 --- a/app/Jobs/ProxyCheckJob.php +++ b/app/Jobs/InstanceProxyCheckJob.php @@ -10,7 +10,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class ProxyCheckJob implements ShouldQueue +class InstanceProxyCheckJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -32,7 +32,7 @@ public function handle() $servers = Server::whereRelation('settings', 'is_validated', true)->get(); foreach ($servers as $server) { - $status = checkContainerStatus(server: $server, container_id: $container_name); + $status = get_container_status(server: $server, container_id: $container_name); if ($status === 'running') { continue; } diff --git a/app/Jobs/RollbackApplicationJob.php b/app/Jobs/RollbackApplicationJob.php deleted file mode 100644 index 6be5124dd..000000000 --- a/app/Jobs/RollbackApplicationJob.php +++ /dev/null @@ -1,379 +0,0 @@ -application = Application::query() - ->where('uuid', $this->application_uuid) - ->firstOrFail(); - $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); - $server = $this->destination->server; - - $private_key_location = savePrivateKeyForServer($server); - - $this->git_commit = $commit; - - $remoteProcessArgs = new CoolifyTaskArgs( - server_ip: $server->ip, - private_key_location: $private_key_location, - command: 'overwritten-later', - port: $server->port, - user: $server->user, - type: ActivityTypes::DEPLOYMENT->value, - type_uuid: $this->deployment_uuid, - ); - - $this->activity = activity() - ->performedOn($this->application) - ->withProperties($remoteProcessArgs->toArray()) - ->event(ActivityTypes::DEPLOYMENT->value) - ->log("[]"); - } - protected function stopRunningContainer() - { - $this->executeNow([ - "echo -n 'Removing old instance... '", - $this->execute_in_builder("docker rm -f {$this->application->uuid} >/dev/null 2>&1"), - "echo 'Done.'", - "echo -n 'Starting your application... '", - ]); - } - protected function startByComposeFile() - { - $this->executeNow([ - $this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), - ], isDebuggable: true); - $this->executeNow([ - "echo 'Done. 🎉'", - ], isFinished: true); - } - protected function generateComposeFile() - { - $this->docker_compose = $this->generate_docker_compose(); - $docker_compose_base64 = base64_encode($this->docker_compose); - $this->executeNow([ - $this->execute_in_builder("mkdir -p {$this->workdir}"), - $this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml") - ], hideFromOutput: true); - } - /** - * Execute the job. - */ - public function handle(): void - { - try { - $this->workdir = "/artifacts/{$this->deployment_uuid}"; - $this->executeNow([ - "echo 'Starting rollback of {$this->application->git_repository}:{$this->application->git_branch} to {$this->git_commit}...'", - "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder)... '", - ]); - - $this->executeNow([ - "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder", - ], isDebuggable: true); - - $this->executeNow([ - "echo 'Done.'", - "echo -n 'Checking if the image is available... '", - ]); - - $this->executeNow([ - "docker images -q {$this->application->uuid}:{$this->git_commit} 2>/dev/null", - ], 'local_image_found', hideFromOutput: true, ignoreErrors: true); - $image_found = Str::of($this->activity->properties->get('local_image_found'))->trim()->isNotEmpty(); - if ($image_found) { - $this->executeNow([ - "echo 'Yes, it is available.'", - ]); - // Generate docker-compose.yml - $this->generateComposeFile(); - - // Stop running container - $this->stopRunningContainer(); - - // Start application - $this->startByComposeFile(); - - $this->application->git_commit_sha = $this->git_commit; - $this->application->settings->is_auto_deploy = false; - $this->application->settings->save(); - $this->application->save(); - - $this->executeNow([ - "echo 'Auto deployments are disabled for this application to prevent overwritten automatically.'", - "echo 'Commit SHA set to {$this->git_commit} in the Source menu.'", - ]); - return; - } - throw new \Exception('Docker Image not found locally with the same Git Commit SHA.'); - } catch (\Exception $e) { - $this->executeNow([ - "echo '\nOops something is not okay, are you okay? 😢'", - "echo '\n\n{$e->getMessage()}'", - ]); - $this->fail($e->getMessage()); - } finally { - // Saving docker-compose.yml - if (isset($this->docker_compose)) { - Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose); - } - $this->executeNow(["docker rm -f {$this->deployment_uuid} >/dev/null 2>&1"], hideFromOutput: true); - dispatch(new ContainerStatusJob($this->application_uuid)); - } - } - - private function execute_in_builder(string $command) - { - return "docker exec {$this->deployment_uuid} bash -c '{$command}'"; - } - private function generate_environment_variables($ports) - { - $environment_variables = collect(); - - foreach ($this->application->runtime_environment_variables as $env) { - $environment_variables->push("$env->key=$env->value"); - } - // Add PORT if not exists, use the first port as default - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { - $environment_variables->push("PORT={$ports[0]}"); - } - return $environment_variables->all(); - } - private function generate_env_variables() - { - $this->env_args = collect([]); - foreach ($this->application->nixpacks_environment_variables as $env) { - $this->env_args->push("--env {$env->key}={$env->value}"); - } - $this->env_args = $this->env_args->implode(' '); - } - private function generate_build_env_variables() - { - $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->git_commit}"]); - foreach ($this->application->build_environment_variables as $env) { - $this->build_args->push("--build-arg {$env->key}={$env->value}"); - } - $this->build_args = $this->build_args->implode(' '); - } - private function add_build_env_variables_to_dockerfile() - { - $this->executeNow([ - $this->execute_in_builder("cat {$this->workdir}/Dockerfile") - ], propertyName: 'dockerfile', hideFromOutput: true); - $dockerfile = collect(Str::of($this->activity->properties->get('dockerfile'))->trim()->explode("\n")); - - foreach ($this->application->build_environment_variables as $env) { - $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); - } - $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->executeNow([ - $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile") - ], hideFromOutput: true); - } - private function generate_docker_compose() - { - $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; - $persistentStorages = $this->generate_local_persistent_volumes(); - $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); - $environment_variables = $this->generate_environment_variables($ports); - $docker_compose = [ - 'version' => '3.8', - 'services' => [ - $this->application->uuid => [ - 'image' => "{$this->application->uuid}:$this->git_commit", - 'container_name' => $this->application->uuid, - 'restart' => 'always', - 'environment' => $environment_variables, - 'labels' => $this->set_labels_for_applications(), - 'expose' => $ports, - 'networks' => [ - $this->destination->network, - ], - 'healthcheck' => [ - 'test' => [ - 'CMD-SHELL', - $this->generate_healthcheck_commands() - ], - 'interval' => $this->application->health_check_interval . 's', - 'timeout' => $this->application->health_check_timeout . 's', - 'retries' => $this->application->health_check_retries, - 'start_period' => $this->application->health_check_start_period . 's' - ], - 'mem_limit' => $this->application->limits_memory, - 'memswap_limit' => $this->application->limits_memory_swap, - 'mem_swappiness' => $this->application->limits_memory_swappiness, - 'mem_reservation' => $this->application->limits_memory_reservation, - 'oom_kill_disable' => $this->application->limits_memory_oom_kill, - 'cpus' => $this->application->limits_cpus, - 'cpuset' => $this->application->limits_cpuset, - 'cpu_shares' => $this->application->limits_cpu_shares, - ] - ], - 'networks' => [ - $this->destination->network => [ - 'external' => false, - 'name' => $this->destination->network, - 'attachable' => true, - ] - ] - ]; - if (count($this->application->ports_mappings_array) > 0) { - $docker_compose['services'][$this->application->uuid]['ports'] = $this->application->ports_mappings_array; - } - if (count($persistentStorages) > 0) { - $docker_compose['services'][$this->application->uuid]['volumes'] = $persistentStorages; - } - if (count($volume_names) > 0) { - $docker_compose['volumes'] = $volume_names; - } - return Yaml::dump($docker_compose, 10); - } - private function generate_local_persistent_volumes() - { - foreach ($this->application->persistentStorages as $persistentStorage) { - $volume_name = $persistentStorage->host_path ?? $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; - } - return $local_persistent_volumes ?? []; - } - - private function generate_local_persistent_volumes_only_volume_names() - { - foreach ($this->application->persistentStorages as $persistentStorage) { - if ($persistentStorage->host_path) { - continue; - } - $local_persistent_volumes_names[$persistentStorage->name] = [ - 'name' => $persistentStorage->name, - 'external' => false, - ]; - } - return $local_persistent_volumes_names ?? []; - } - private function generate_healthcheck_commands() - { - if (!$this->application->health_check_port) { - $this->application->health_check_port = $this->application->ports_exposes_array[0]; - } - if ($this->application->health_check_path) { - $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null" - ]; - } else { - $generated_healthchecks_commands = [ - "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/" - ]; - } - return implode(' ', $generated_healthchecks_commands); - } - - private function set_labels_for_applications() - { - $labels = []; - $labels[] = 'coolify.managed=true'; - $labels[] = 'coolify.version=' . config('version'); - $labels[] = 'coolify.applicationId=' . $this->application->id; - $labels[] = 'coolify.type=application'; - $labels[] = 'coolify.name=' . $this->application->name; - if ($this->application->fqdn) { - $domains = Str::of($this->application->fqdn)->explode(','); - $labels[] = 'traefik.enable=true'; - foreach ($domains as $domain) { - $url = Url::fromString($domain); - $host = $url->getHost(); - $path = $url->getPath(); - $slug = Str::slug($url); - $label_id = "{$this->application->uuid}-{$slug}"; - if ($path === '/') { - $labels[] = "traefik.http.routers.{$label_id}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; - } else { - $labels[] = "traefik.http.routers.{$label_id}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)"; - $labels[] = "traefik.http.routers.{$label_id}.middlewares={$label_id}-stripprefix"; - $labels[] = "traefik.http.middlewares.{$label_id}-stripprefix.stripprefix.prefixes={$path}"; - } - } - } - return $labels; - } - - private function executeNow( - array|Collection $command, - string $propertyName = null, - bool $isFinished = false, - bool $hideFromOutput = false, - bool $isDebuggable = false, - bool $ignoreErrors = false - ) { - static::$batch_counter++; - - if ($command instanceof Collection) { - $commandText = $command->implode("\n"); - } else { - $commandText = collect($command)->implode("\n"); - } - - $this->activity->properties = $this->activity->properties->merge([ - 'command' => $commandText, - ]); - $this->activity->save(); - if ($isDebuggable && !$this->application->settings->is_debug) { - $hideFromOutput = true; - } - $remoteProcess = resolve(RunRemoteProcess::class, [ - 'activity' => $this->activity, - 'hideFromOutput' => $hideFromOutput, - 'isFinished' => $isFinished, - 'ignoreErrors' => $ignoreErrors, - ]); - $result = $remoteProcess(); - if ($propertyName) { - $this->activity->properties = $this->activity->properties->merge([ - $propertyName => trim($result->output()), - ]); - $this->activity->save(); - } - - if ($result->exitCode() != 0 && $result->errorOutput() && !$ignoreErrors) { - throw new \RuntimeException($result->errorOutput()); - } - } -} diff --git a/app/Models/Application.php b/app/Models/Application.php index 6170b670c..ba3f56ac4 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -150,7 +150,9 @@ public function persistentStorages() public function deployments() { - return Activity::where('subject_id', $this->id)->where('properties->type', '=', 'deployment')->orderBy('created_at', 'desc')->get(); + $asd = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc')->get(); + return $asd; + // return Activity::where('subject_id', $this->id)->where('properties->type', '=', 'deployment')->orderBy('created_at', 'desc')->get(); } public function get_deployment(string $deployment_uuid) { diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php new file mode 100644 index 000000000..bc4d756eb --- /dev/null +++ b/app/Models/ApplicationDeploymentQueue.php @@ -0,0 +1,24 @@ + SchemalessAttributes::class, + ]; + public function scopeWithExtraAttributes(): Builder + { + return $this->metadata->modelScope(); + } +} diff --git a/bootstrap/helpers.php b/bootstrap/helpers.php deleted file mode 100644 index 72b8912e8..000000000 --- a/bootstrap/helpers.php +++ /dev/null @@ -1,328 +0,0 @@ -errorInfo[0] === '23505') { - throw new \Exception('Duplicate entry found.', '23505'); - } else if (count($e->errorInfo) === 4) { - throw new \Exception($e->errorInfo[3]); - } else { - throw new \Exception($e->errorInfo[2]); - } - } else { - throw new \Exception($e->getMessage()); - } - } catch (\Throwable $error) { - if ($that) { - return $that->emit('error', $error->getMessage()); - } elseif ($isJson) { - return response()->json([ - 'code' => $error->getCode(), - 'error' => $error->getMessage(), - ]); - } else { - // dump($error); - } - } - } -} -if (!function_exists('remoteProcess')) { - /** - * Run a Remote Process, which SSH's asynchronously into a machine to run the command(s). - * @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo - * - */ - function remoteProcess( - array $command, - Server $server, - string $type, - ?string $type_uuid = null, - ?Model $model = null, - ): Activity { - - $command_string = implode("\n", $command); - - // @TODO: Check if the user has access to this server - // checkTeam($server->team_id); - - $private_key_location = savePrivateKeyForServer($server); - - return resolve(PrepareCoolifyTask::class, [ - 'remoteProcessArgs' => new CoolifyTaskArgs( - server_ip: $server->ip, - private_key_location: $private_key_location, - command: <<port, - user: $server->user, - type: $type, - type_uuid: $type_uuid, - model: $model, - ), - ])(); - } -} - -// function checkTeam(string $team_id) -// { -// $found_team = auth()->user()->teams->pluck('id')->contains($team_id); -// if (!$found_team) { -// throw new \RuntimeException('You do not have access to this server.'); -// } -// } - -if (!function_exists('savePrivateKeyForServer')) { - function savePrivateKeyForServer(Server $server) - { - $temp_file = "id.root@{$server->ip}"; - Storage::disk('ssh-keys')->put($temp_file, $server->privateKey->private_key); - return '/var/www/html/storage/app/ssh-keys/' . $temp_file; - } -} - -if (!function_exists('generateSshCommand')) { - function generateSshCommand(string $private_key_location, string $server_ip, string $user, string $port, string $command, bool $isMux = true) - { - Storage::disk('local')->makeDirectory('.ssh'); - - $delimiter = 'EOF-COOLIFY-SSH'; - $ssh_command = "ssh "; - - if ($isMux && config('coolify.mux_enabled')) { - $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/.ssh/ssh_mux_%h_%p_%r '; - } - $ssh_command .= "-i {$private_key_location} " - . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' - . '-o PasswordAuthentication=no ' - . '-o ConnectTimeout=3600 ' - . '-o ServerAliveInterval=20 ' - . '-o RequestTTY=no ' - . '-o LogLevel=ERROR ' - . "-p {$port} " - . "{$user}@{$server_ip} " - . " 'bash -se' << \\$delimiter" . PHP_EOL - . $command . PHP_EOL - . $delimiter; - - return $ssh_command; - } -} -if (!function_exists('formatDockerCmdOutputToJson')) { - function formatDockerCmdOutputToJson($rawOutput): Collection - { - $outputLines = explode(PHP_EOL, $rawOutput); - - return collect($outputLines) - ->reject(fn ($line) => empty($line)) - ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); - } -} -if (!function_exists('formatDockerLabelsToJson')) { - function formatDockerLabelsToJson($rawOutput): Collection - { - $outputLines = explode(PHP_EOL, $rawOutput); - - return collect($outputLines) - ->reject(fn ($line) => empty($line)) - ->map(function ($outputLine) { - $outputArray = explode(',', $outputLine); - return collect($outputArray) - ->map(function ($outputLine) { - return explode('=', $outputLine); - }) - ->mapWithKeys(function ($outputLine) { - return [$outputLine[0] => $outputLine[1]]; - }); - })[0]; - } -} -if (!function_exists('instantRemoteProcess')) { - function instantRemoteProcess(array $command, Server $server, $throwError = true) - { - $command_string = implode("\n", $command); - $private_key_location = savePrivateKeyForServer($server); - $ssh_command = generateSshCommand($private_key_location, $server->ip, $server->user, $server->port, $command_string); - $process = Process::run($ssh_command); - $output = trim($process->output()); - $exitCode = $process->exitCode(); - if ($exitCode !== 0) { - if (!$throwError) { - return null; - } - throw new \RuntimeException($process->errorOutput()); - } - return $output; - } -} - -if (!function_exists('getLatestVersionOfCoolify')) { - function getLatestVersionOfCoolify() - { - $response = Http::get('https://coolify-cdn.b-cdn.net/versions.json'); - $versions = $response->json(); - return data_get($versions, 'coolify.v4.version'); - } -} -if (!function_exists('generateRandomName')) { - function generateRandomName() - { - $generator = \Nubs\RandomNameGenerator\All::create(); - $cuid = new Cuid2(7); - return Str::kebab("{$generator->getName()}-{$cuid}"); - } -} - -use Lcobucci\JWT\Encoding\ChainedFormatter; -use Lcobucci\JWT\Encoding\JoseEncoder; -use Lcobucci\JWT\Signer\Key\InMemory; -use Lcobucci\JWT\Signer\Rsa\Sha256; -use Lcobucci\JWT\Token\Builder; -use Symfony\Component\Yaml\Yaml; - -if (!function_exists('generate_github_installation_token')) { - function generate_github_installation_token(GithubApp $source) - { - $signingKey = InMemory::plainText($source->privateKey->private_key); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); - $now = $now->setTime($now->format('H'), $now->format('i')); - $issuedToken = $tokenBuilder - ->issuedBy($source->app_id) - ->issuedAt($now) - ->expiresAt($now->modify('+10 minutes')) - ->getToken($algorithm, $signingKey) - ->toString(); - $token = Http::withHeaders([ - 'Authorization' => "Bearer $issuedToken", - 'Accept' => 'application/vnd.github.machine-man-preview+json' - ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens"); - if ($token->failed()) { - throw new \Exception("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']); - } - return $token->json()['token']; - } -} -if (!function_exists('generate_github_jwt_token')) { - function generate_github_jwt_token(GithubApp $source) - { - $signingKey = InMemory::plainText($source->privateKey->private_key); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); - $now = $now->setTime($now->format('H'), $now->format('i')); - $issuedToken = $tokenBuilder - ->issuedBy($source->app_id) - ->issuedAt($now->modify('-1 minute')) - ->expiresAt($now->modify('+10 minutes')) - ->getToken($algorithm, $signingKey) - ->toString(); - return $issuedToken; - } -} -if (!function_exists('getParameters')) { - function getParameters() - { - return Route::current()->parameters(); - } -} -if (!function_exists('checkContainerStatus')) { - function checkContainerStatus(Server $server, string $container_id, bool $throwError = false) - { - $container = instantRemoteProcess(["docker inspect --format '{{json .State}}' {$container_id}"], $server, $throwError); - if (!$container) { - return 'exited'; - } - $container = formatDockerCmdOutputToJson($container); - return $container[0]['Status']; - } -} -if (!function_exists('getProxyConfiguration')) { - function getProxyConfiguration(Server $server) - { - $proxy_path = config('coolify.proxy_config_path'); - $networks = collect($server->standaloneDockers)->map(function ($docker) { - return $docker['network']; - })->unique(); - if ($networks->count() === 0) { - $networks = collect(['coolify']); - } - $array_of_networks = collect([]); - $networks->map(function ($network) use ($array_of_networks) { - $array_of_networks[$network] = [ - "external" => true, - ]; - }); - $config = [ - "version" => "3.8", - "networks" => $array_of_networks->toArray(), - "services" => [ - "traefik" => [ - "container_name" => "coolify-proxy", - "image" => "traefik:v2.10", - "restart" => "always", - "extra_hosts" => [ - "host.docker.internal:host-gateway", - ], - "networks" => $networks->toArray(), - "ports" => [ - "80:80", - "443:443", - "8080:8080", - ], - "volumes" => [ - "/var/run/docker.sock:/var/run/docker.sock:ro", - "{$proxy_path}:/traefik", - ], - "command" => [ - "--api.dashboard=true", - "--api.insecure=true", - "--entrypoints.http.address=:80", - "--entrypoints.https.address=:443", - "--providers.docker=true", - "--providers.docker.exposedbydefault=false", - "--providers.file.directory=/traefik/dynamic-conf/", - "--providers.file.watch=true", - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", - "--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.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)', - "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file", - "traefik.http.routers.traefik.service=api@internal", - "traefik.http.services.traefik.loadbalancer.server.port=8080", - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", - "traefik.http.middlewares.gzip.compress=true", - ], - ], - ], - ]; - if (config('app.env') === 'local') { - $config['services']['traefik']['command'][] = "--log.level=debug"; - } - return Yaml::dump($config, 4, 2); - } -} diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php new file mode 100644 index 000000000..72172a6db --- /dev/null +++ b/bootstrap/helpers/applications.php @@ -0,0 +1,32 @@ + $application->id, + 'metadata' => $metadata, + ]); + $queued_deployments = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->get()->sortByDesc('created_at'); + $running_deployments = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'in_progress')->get()->sortByDesc('created_at'); + if ($queued_deployments->count() > 1) { + $queued_deployments = $queued_deployments->skip(1); + $queued_deployments->each(function ($queued_deployment, $key) { + $queued_deployment->status = 'cancelled by system'; + $queued_deployment->save(); + }); + } + if ($running_deployments->count() > 0) { + return; + } + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $deployment->id, + deployment_uuid: $metadata['deployment_uuid'], + application_uuid: $metadata['application_uuid'], + force_rebuild: $metadata['force_rebuild'], + commit: $metadata['commit'] ?? null, + )); +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php new file mode 100644 index 000000000..f4c28f673 --- /dev/null +++ b/bootstrap/helpers/docker.php @@ -0,0 +1,40 @@ +reject(fn ($line) => empty($line)) + ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); +} +function format_docker_labels_to_json($rawOutput): Collection +{ + $outputLines = explode(PHP_EOL, $rawOutput); + + return collect($outputLines) + ->reject(fn ($line) => empty($line)) + ->map(function ($outputLine) { + $outputArray = explode(',', $outputLine); + return collect($outputArray) + ->map(function ($outputLine) { + return explode('=', $outputLine); + }) + ->mapWithKeys(function ($outputLine) { + return [$outputLine[0] => $outputLine[1]]; + }); + })[0]; +} + +function get_container_status(Server $server, string $container_id, bool $throwError = false) +{ + $container = instantRemoteProcess(["docker inspect --format '{{json .State}}' {$container_id}"], $server, $throwError); + if (!$container) { + return 'exited'; + } + $container = format_docker_command_output_to_json($container); + return $container[0]['Status']; +} diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php new file mode 100644 index 000000000..ee1810ae6 --- /dev/null +++ b/bootstrap/helpers/github.php @@ -0,0 +1,47 @@ +privateKey->private_key); + $algorithm = new Sha256(); + $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); + $now = new DateTimeImmutable(); + $now = $now->setTime($now->format('H'), $now->format('i')); + $issuedToken = $tokenBuilder + ->issuedBy($source->app_id) + ->issuedAt($now) + ->expiresAt($now->modify('+10 minutes')) + ->getToken($algorithm, $signingKey) + ->toString(); + $token = Http::withHeaders([ + 'Authorization' => "Bearer $issuedToken", + 'Accept' => 'application/vnd.github.machine-man-preview+json' + ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens"); + if ($token->failed()) { + throw new \Exception("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']); + } + return $token->json()['token']; +} +function generate_github_jwt_token(GithubApp $source) +{ + $signingKey = InMemory::plainText($source->privateKey->private_key); + $algorithm = new Sha256(); + $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); + $now = new DateTimeImmutable(); + $now = $now->setTime($now->format('H'), $now->format('i')); + $issuedToken = $tokenBuilder + ->issuedBy($source->app_id) + ->issuedAt($now->modify('-1 minute')) + ->expiresAt($now->modify('+10 minutes')) + ->getToken($algorithm, $signingKey) + ->toString(); + return $issuedToken; +} diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php new file mode 100644 index 000000000..f58ba0d67 --- /dev/null +++ b/bootstrap/helpers/proxy.php @@ -0,0 +1,74 @@ +standaloneDockers)->map(function ($docker) { + return $docker['network']; + })->unique(); + if ($networks->count() === 0) { + $networks = collect(['coolify']); + } + $array_of_networks = collect([]); + $networks->map(function ($network) use ($array_of_networks) { + $array_of_networks[$network] = [ + "external" => true, + ]; + }); + $config = [ + "version" => "3.8", + "networks" => $array_of_networks->toArray(), + "services" => [ + "traefik" => [ + "container_name" => "coolify-proxy", + "image" => "traefik:v2.10", + "restart" => "always", + "extra_hosts" => [ + "host.docker.internal:host-gateway", + ], + "networks" => $networks->toArray(), + "ports" => [ + "80:80", + "443:443", + "8080:8080", + ], + "volumes" => [ + "/var/run/docker.sock:/var/run/docker.sock:ro", + "{$proxy_path}:/traefik", + ], + "command" => [ + "--api.dashboard=true", + "--api.insecure=true", + "--entrypoints.http.address=:80", + "--entrypoints.https.address=:443", + "--providers.docker=true", + "--providers.docker.exposedbydefault=false", + "--providers.file.directory=/traefik/dynamic-conf/", + "--providers.file.watch=true", + "--certificatesresolvers.letsencrypt.acme.httpchallenge=true", + "--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.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)', + "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file", + "traefik.http.routers.traefik.service=api@internal", + "traefik.http.services.traefik.loadbalancer.server.port=8080", + "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https", + "traefik.http.middlewares.gzip.compress=true", + ], + ], + ], + ]; + if (config('app.env') === 'local') { + $config['services']['traefik']['command'][] = "--log.level=debug"; + } + return Yaml::dump($config, 4, 2); + } +} diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php new file mode 100644 index 000000000..ade752b41 --- /dev/null +++ b/bootstrap/helpers/remoteProcess.php @@ -0,0 +1,102 @@ +team_id); + + $private_key_location = savePrivateKeyForServer($server); + + return resolve(PrepareCoolifyTask::class, [ + 'remoteProcessArgs' => new CoolifyTaskArgs( + server_ip: $server->ip, + private_key_location: $private_key_location, + command: <<port, + user: $server->user, + type: $type, + type_uuid: $type_uuid, + model: $model, + ), + ])(); + } +} + +if (!function_exists('savePrivateKeyForServer')) { + function savePrivateKeyForServer(Server $server) + { + $temp_file = "id.root@{$server->ip}"; + Storage::disk('ssh-keys')->put($temp_file, $server->privateKey->private_key); + return '/var/www/html/storage/app/ssh-keys/' . $temp_file; + } +} + +if (!function_exists('generateSshCommand')) { + function generateSshCommand(string $private_key_location, string $server_ip, string $user, string $port, string $command, bool $isMux = true) + { + Storage::disk('local')->makeDirectory('.ssh'); + + $delimiter = 'EOF-COOLIFY-SSH'; + $ssh_command = "ssh "; + + if ($isMux && config('coolify.mux_enabled')) { + $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/.ssh/ssh_mux_%h_%p_%r '; + } + $ssh_command .= "-i {$private_key_location} " + . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' + . '-o PasswordAuthentication=no ' + . '-o ConnectTimeout=3600 ' + . '-o ServerAliveInterval=20 ' + . '-o RequestTTY=no ' + . '-o LogLevel=ERROR ' + . "-p {$port} " + . "{$user}@{$server_ip} " + . " 'bash -se' << \\$delimiter" . PHP_EOL + . $command . PHP_EOL + . $delimiter; + + return $ssh_command; + } +} + +if (!function_exists('instantRemoteProcess')) { + function instantRemoteProcess(array $command, Server $server, $throwError = true) + { + $command_string = implode("\n", $command); + $private_key_location = savePrivateKeyForServer($server); + $ssh_command = generateSshCommand($private_key_location, $server->ip, $server->user, $server->port, $command_string); + $process = Process::run($ssh_command); + $output = trim($process->output()); + $exitCode = $process->exitCode(); + if ($exitCode !== 0) { + if (!$throwError) { + return null; + } + throw new \RuntimeException($process->errorOutput()); + } + return $output; + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php new file mode 100644 index 000000000..471ce2471 --- /dev/null +++ b/bootstrap/helpers/shared.php @@ -0,0 +1,54 @@ +errorInfo[0] === '23505') { + throw new \Exception('Duplicate entry found.', '23505'); + } else if (count($e->errorInfo) === 4) { + throw new \Exception($e->errorInfo[3]); + } else { + throw new \Exception($e->errorInfo[2]); + } + } else { + throw new \Exception($e->getMessage()); + } + } catch (\Throwable $error) { + if ($that) { + return $that->emit('error', $error->getMessage()); + } elseif ($isJson) { + return response()->json([ + 'code' => $error->getCode(), + 'error' => $error->getMessage(), + ]); + } else { + // dump($error); + } + } +} + +function get_parameters() +{ + return Route::current()->parameters(); +} + +function get_latest_version_of_coolify() +{ + $response = Http::get('https://coolify-cdn.b-cdn.net/versions.json'); + $versions = $response->json(); + return data_get($versions, 'coolify.v4.version'); +} + +function generate_random_name() +{ + $generator = \Nubs\RandomNameGenerator\All::create(); + $cuid = new Cuid2(7); + return Str::kebab("{$generator->getName()}-{$cuid}"); +} diff --git a/bootstrap/includeHelpers.php b/bootstrap/includeHelpers.php new file mode 100644 index 000000000..cc272b2c0 --- /dev/null +++ b/bootstrap/includeHelpers.php @@ -0,0 +1,5 @@ +id(); + $table->string('application_id'); + $table->integer('pull_request_id')->default(0); + $table->schemalessAttributes('metadata'); + $table->string('status')->default('queued'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('application_deployment_queues'); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index a55c30aba..5636881f0 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -4,7 +4,7 @@ * { //outline: solid 0.25rem hsla(210, 100%, 100%, 0.5); - @apply scrollbar-thumb-yellow-400 scrollbar-track-coolgray-200 scrollbar-w-2; + @apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-1; } html { @apply h-full min-h-full bg-coolgray-100; diff --git a/resources/views/livewire/project/application/deploy.blade.php b/resources/views/livewire/project/application/deploy.blade.php index a070c6982..e2e465812 100644 --- a/resources/views/livewire/project/application/deploy.blade.php +++ b/resources/views/livewire/project/application/deploy.blade.php @@ -9,10 +9,10 @@ class="flex items-center justify-center h-full text-white normal-case rounded-no - + diff --git a/resources/views/project/application/deployments.blade.php b/resources/views/project/application/deployments.blade.php index 9592dc620..99a89fae7 100644 --- a/resources/views/project/application/deployments.blade.php +++ b/resources/views/project/application/deployments.blade.php @@ -12,11 +12,5 @@ -
- @forelse ($deployments as $deployment) - - @empty -

No deployments found.

- @endforelse -
+ diff --git a/routes/web.php b/routes/web.php index 2a9badbea..c610c53fa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -81,7 +81,7 @@ if ($is_new_project) { $project = Project::create([ - 'name' => request()->query('name') ?? generateRandomName(), + 'name' => request()->query('name') ?? generate_random_name(), 'team_id' => $id, ]); return response()->json([ @@ -92,7 +92,7 @@ $environment = Project::where('uuid', request()->query('project'))->first()->environments->where('name', request()->query('name'))->first(); if (!$environment) { $environment = Environment::create([ - 'name' => request()->query('name') ?? generateRandomName(), + 'name' => request()->query('name') ?? generate_random_name(), 'project_id' => Project::where('uuid', request()->query('project'))->first()->id, ]); } @@ -104,7 +104,7 @@ 'magic' => true, ]); } catch (\Throwable $e) { - return generalErrorHandler($e, isJson: true); + return general_error_handler($e, isJson: true); } }); Route::get('/', function () { diff --git a/routes/webhooks.php b/routes/webhooks.php index 75aa63cac..e698cb12b 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -1,6 +1,6 @@ save(); return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } }); @@ -52,7 +52,7 @@ } return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } }); Route::post('/source/github/events', function () { @@ -97,7 +97,7 @@ foreach ($applications as $application) { if ($application->isDeployable()) { $deployment_uuid = new Cuid2(7); - dispatch(new DeployApplicationJob( + dispatch(new ApplicationDeploymentJob( deployment_uuid: $deployment_uuid, application_uuid: $application->uuid, force_rebuild: false, @@ -105,6 +105,6 @@ } } } catch (\Exception $e) { - return generalErrorHandler($e); + return general_error_handler($e); } }); diff --git a/scripts/run b/scripts/run index c0a3fc069..7e4bb2411 100755 --- a/scripts/run +++ b/scripts/run @@ -50,6 +50,9 @@ function coolify { function coolify-root { bash vendor/bin/spin exec coolify bash } +function redis { + docker exec -ti coolify-redis redis-cli +} function vite { bash vendor/bin/spin exec vite bash } diff --git a/tests/Feature/DockerCommandsTest.php b/tests/Feature/DockerCommandsTest.php index 392e955b6..b29d6191d 100644 --- a/tests/Feature/DockerCommandsTest.php +++ b/tests/Feature/DockerCommandsTest.php @@ -20,7 +20,7 @@ test()->actingAs(User::factory([ 'uuid' => Str::uuid(), - 'email' => Str::uuid().'@example.com', + 'email' => Str::uuid() . '@example.com', ])->create()); $coolifyNamePrefix = 'coolify_test_'; @@ -40,7 +40,7 @@ // Assert there's no containers start with coolify_test_* $activity = remoteProcess([$areThereCoolifyTestContainers], $host); $tidyOutput = RunRemoteProcess::decodeOutput($activity); - $containers = formatDockerCmdOutputToJson($tidyOutput); + $containers = format_docker_command_output_to_json($tidyOutput); expect($containers)->toBeEmpty(); // start a container nginx -d --name = $containerName @@ -50,13 +50,13 @@ // docker ps name = $container $activity = remoteProcess([$areThereCoolifyTestContainers], $host); $tidyOutput = RunRemoteProcess::decodeOutput($activity); - $containers = formatDockerCmdOutputToJson($tidyOutput); + $containers = format_docker_command_output_to_json($tidyOutput); expect($containers->where('Names', $containerName)->count())->toBe(1); // Stop testing containers $activity = remoteProcess([ "docker ps --filter='name={$coolifyNamePrefix}*' -aq && " . - "docker rm -f $(docker ps --filter='name={$coolifyNamePrefix}*' -aq)" + "docker rm -f $(docker ps --filter='name={$coolifyNamePrefix}*' -aq)" ], $host); expect($activity->getExtraProperty('exitCode'))->toBe(0); });