Merge branch 'coollabsio:main' into add-debugbar

This commit is contained in:
peaklabs-dev
2024-09-27 16:59:21 +02:00
committed by GitHub
187 changed files with 3453 additions and 2428 deletions

View File

@@ -6,7 +6,7 @@ APP_KEY=
APP_URL=http://localhost APP_URL=http://localhost
APP_PORT=8000 APP_PORT=8000
APP_DEBUG=true APP_DEBUG=true
SSH_MUX_ENABLED=false SSH_MUX_ENABLED=true
# PostgreSQL Database Configuration # PostgreSQL Database Configuration
DB_DATABASE=coolify DB_DATABASE=coolify
@@ -19,11 +19,7 @@ DB_PORT=5432
# Set to true to enable Ray # Set to true to enable Ray
RAY_ENABLED=false RAY_ENABLED=false
# Set custom ray port # Set custom ray port
RAY_PORT= # RAY_PORT=
# Clockwork Configuration
CLOCKWORK_ENABLED=false
CLOCKWORK_QUEUE_COLLECT=true
# Enable Laravel Telescope for debugging # Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false TELESCOPE_ENABLED=false

View File

@@ -1 +1,13 @@
> Always use `next` branch as destination branch for PRs, not `main` ## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
- [ ] I have listed all changes in the `Changes` section.
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
- [ ] I have tested my changes.
- [ ] I have considered backwards compatibility.
- [ ] I have removed this checklist and any unused sections.
## Changes
-
## Issues
- fix #

View File

@@ -65,11 +65,14 @@ jobs:
} }
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({ const pr = context.payload.pull_request;
q: `repo:${owner}/${repo} is:issue is:closed linked:${context.payload.pull_request.number}`, if (pr.body) {
per_page: 100 const issueReferences = pr.body.match(/#(\d+)/g);
}); if (issueReferences) {
for (const issue of closedIssues.items) { for (const reference of issueReferences) {
await processIssue(issue.number); const issueNumber = parseInt(reference.substring(1));
await processIssue(issueNumber);
}
}
} }
} }

View File

@@ -6,15 +6,19 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis
## Table of Contents ## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment) - [Contributing to Coolify](#contributing-to-coolify)
2. [Verify Installation](#2-verify-installation-optional) - [Table of Contents](#table-of-contents)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) - [1. Setup Development Environment](#1-setup-development-environment)
4. [Set up Environment Variables](#4-set-up-environment-variables) - [2. Verify Installation (Optional)](#2-verify-installation-optional)
5. [Start Coolify](#5-start-coolify) - [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
6. [Start Development](#6-start-development) - [4. Set up Environment Variables](#4-set-up-environment-variables)
7. [Development Notes](#7-development-notes) - [5. Start Coolify](#5-start-coolify)
8. [Create a Pull Request](#8-create-a-pull-request) - [6. Start Development](#6-start-development)
9. [Additional Contribution Guidelines](#additional-contribution-guidelines) - [7. Development Notes](#7-development-notes)
- [8. Create a Pull Request](#8-create-a-pull-request)
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing a New Service](#contributing-a-new-service)
- [Contributing to Documentation](#contributing-to-documentation)
## 1. Setup Development Environment ## 1. Setup Development Environment

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Application; namespace App\Actions\Application;
use App\Actions\Server\CleanupDocker;
use App\Models\Application; use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,44 +10,35 @@ class StopApplication
{ {
use AsAction; use AsAction;
public function handle(Application $application, bool $previewDeployments = false) public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{ {
if ($application->destination->server->isSwarm()) { try {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); $server = $application->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping application: '.$application->name);
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
return; return;
} }
$servers = collect([]); $containersToStop = $application->getContainersToStop($previewDeployments);
$servers->push($application->destination->server); $application->stopContainers($containersToStop, $server);
$application->additional_servers->map(function ($server) use ($servers) {
$servers->push($server);
});
foreach ($servers as $server) {
if (! $server->isFunctional()) {
return 'Server is not functional';
}
if ($previewDeployments) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
} 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(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
}
}
}
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
// remove network $application->delete_connected_networks($application->uuid);
$uuid = $application->uuid; }
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false); if ($dockerCleanup) {
} CleanupDocker::dispatch($server, true);
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
} }
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes; use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Process\ProcessResult; use Illuminate\Process\ProcessResult;
@@ -137,7 +138,7 @@ class RunRemoteProcess
$command = $this->activity->getExtraProperty('command'); $command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail(); $server = Server::whereUuid($server_uuid)->firstOrFail();
return generateSshCommand($server, $command); return SshMultiplexingHelper::generateSshCommand($server, $command);
} }
protected function handleOutput(string $type, string $output) protected function handleOutput(string $type, string $output)

View File

@@ -37,7 +37,6 @@ class StartPostgresql
$this->generate_init_scripts(); $this->generate_init_scripts();
$this->add_custom_conf(); $this->add_custom_conf();
$docker_compose = [ $docker_compose = [
'services' => [ 'services' => [
$container_name => [ $container_name => [

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse; use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly; use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb; use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase class StopDatabase
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{ {
$server = $database->destination->server; $server = $database->destination->server;
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
return 'Server is not functional'; return 'Server is not functional';
} }
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false); $this->stopContainer($database, $database->uuid, 300);
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false); if (! $isDeleteOperation) {
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false); if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
if ($database->is_public) { if ($database->is_public) {
StopDatabaseProxy::run($database); StopDatabaseProxy::run($database);
} }
return 'Database stopped successfully';
}
private function stopContainer($database, string $containerName, int $timeout = 300): void
{
$server = $database->destination->server;
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
}
private function forceStopContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
}
private function removeContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
private function deleteConnectedNetworks($uuid, $server)
{
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
} }
} }

View File

@@ -543,7 +543,7 @@ class GetContainersStatus
} }
} }
} }
$exitedServices = $exitedServices->unique('id'); $exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) { foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) { if (str($exitedService->status)->startsWith('exited')) {
continue; continue;

View File

@@ -26,7 +26,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false; return false;
} }
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(); ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
if (! $uptime) { if (! $uptime) {
throw new \Exception($error); throw new \Exception($error);
} }

View File

@@ -47,7 +47,8 @@ class StartProxy
"echo 'Pulling docker image.'", "echo 'Pulling docker image.'",
'docker compose pull', 'docker compose pull',
"echo 'Stopping existing coolify-proxy.'", "echo 'Stopping existing coolify-proxy.'",
'docker compose down -v --remove-orphans > /dev/null 2>&1', 'docker stop -t 10 coolify-proxy || true',
'docker rm coolify-proxy || true',
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans', 'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'", "echo 'Proxy started successfully.'",

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e; throw $e;
} finally { } finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([ $commands = collect([
'rm -fr /tmp/cloudflared', 'rm -fr /tmp/cloudflared',
]); ]);
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service; namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service; use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,11 +10,11 @@ class DeleteService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{ {
try { try {
$server = data_get($service, 'server'); $server = data_get($service, 'server');
if ($server->isFunctional()) { if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
$service->environment_variables()->delete(); $service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) { foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name"; $commands[] = "docker volume rm -f $storage->name";
} }
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false); // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
if ($result !== 0) {
ray("Failed to execute: $command");
} }
}
}
}
if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid);
}
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} finally { } finally {
if ($deleteConfigurations) {
$service->delete_configurations();
}
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
$application->forceDelete(); $application->forceDelete();
} }
@@ -50,6 +67,11 @@ class DeleteService
$task->delete(); $task->delete();
} }
$service->tags()->detach(); $service->tags()->detach();
$service->forceDelete();
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service; namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service; use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,40 +10,27 @@ class StopService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{ {
try { try {
$server = $service->destination->server; $server = $service->destination->server;
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
return 'Server is not functional'; return 'Server is not functional';
} }
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
if ($applications->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
}
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
if ($dbs->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false); $containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
if (! $isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
} }
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$db->update(['status' => 'exited']);
} }
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) { } catch (\Exception $e) {
ray($e->getMessage()); ray($e->getMessage());
return $e->getMessage(); return $e->getMessage();
} }
} }
} }

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupQueue extends Command
{
protected $signature = 'cleanup:queue';
protected $description = 'Cleanup Queue';
public function handle()
{
echo "Running queue cleanup...\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command
{
protected $signature = 'cleanup:redis';
protected $description = 'Cleanup Redis';
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
collect($keys)->each(function ($key) use ($prefix) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
});
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
}
}

View File

@@ -2,10 +2,12 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command
private function cleanup_stucked_resources() private function cleanup_stucked_resources()
{ {
try {
$servers = Server::all()->filter(function ($server) {
return $server->isFunctional();
});
foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server);
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
try { try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); $applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) { foreach ($applications as $application) {

View File

@@ -5,7 +5,6 @@ namespace App\Console\Commands;
use App\Actions\Server\StopSentinel; use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes; use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment; use App\Models\Environment;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
class Init extends Command class Init extends Command
{ {
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}'; protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs'; protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +25,63 @@ class Init extends Command
public function handle() public function handle()
{ {
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
$this->servers = Server::all(); $this->servers = Server::all();
$this->alive(); if (isCloud()) {
} else {
$this->send_alive_signal();
get_public_ips(); get_public_ips();
}
// Backward compatibility
$this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
//
$this->update_traefik_labels();
if (! isCloud() || $this->option('force-cloud')) {
$this->cleanup_unused_network_from_coolify_proxy();
}
if (isCloud()) {
$this->cleanup_unnecessary_dynamic_proxy_configuration();
} else {
$this->cleanup_in_progress_application_deployments();
}
$this->call('cleanup:redis');
$this->call('cleanup:stucked-resources');
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
} else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
}
}
private function disable_metrics()
{
if (version_compare('4.0.0-beta.312', config('version'), '<=')) { if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) { if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +92,6 @@ class Init extends Command
} }
} }
} }
$full_cleanup = $this->option('full-cleanup');
$cleanup_deployments = $this->option('cleanup-deployments');
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
$this->replace_slash_in_environment_name();
if ($cleanup_deployments) {
echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments();
return;
}
if ($cleanup_proxy_networks) {
echo "Running cleanup proxy networks.\n";
$this->cleanup_unused_network_from_coolify_proxy();
return;
}
if ($full_cleanup) {
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
$this->update_traefik_labels();
$this->cleanup_unused_network_from_coolify_proxy();
$this->cleanup_unnecessary_dynamic_proxy_configuration();
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:queue');
$this->call('cleanup:stucked-resources');
if (! isCloud()) {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
return;
}
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:stucked-resources');
} }
private function update_traefik_labels() private function update_traefik_labels()
@@ -108,7 +105,6 @@ class Init extends Command
private function cleanup_unnecessary_dynamic_proxy_configuration() private function cleanup_unnecessary_dynamic_proxy_configuration()
{ {
if (isCloud()) {
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
try { try {
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
@@ -128,13 +124,9 @@ class Init extends Command
} }
} }
}
private function cleanup_unused_network_from_coolify_proxy() private function cleanup_unused_network_from_coolify_proxy()
{ {
if (isCloud()) {
return;
}
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
continue; continue;
@@ -175,6 +167,7 @@ class Init extends Command
private function restore_coolify_db_backup() private function restore_coolify_db_backup()
{ {
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
try { try {
$database = StandalonePostgresql::withTrashed()->find(0); $database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) { if ($database && $database->trashed()) {
@@ -197,17 +190,9 @@ class Init extends Command
echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
} }
} }
private function cleanup_stucked_helper_containers()
{
foreach ($this->servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
} }
private function alive() private function send_alive_signal()
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('version'); $version = config('version');
@@ -225,23 +210,7 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n"; echo "Error in alive: {$e->getMessage()}\n";
} }
} }
// private function cleanup_ssh()
// {
// TODO: it will cleanup id.root@host.docker.internal
// try {
// $files = Storage::allFiles('ssh/keys');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// $files = Storage::allFiles('ssh/mux');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// } catch (\Throwable $e) {
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments() private function cleanup_in_progress_application_deployments()
{ {
// Cleanup any failed deployments // Cleanup any failed deployments
@@ -263,6 +232,7 @@ class Init extends Command
private function replace_slash_in_environment_name() private function replace_slash_in_environment_name()
{ {
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
$environments = Environment::all(); $environments = Environment::all();
foreach ($environments as $environment) { foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) { if (str_contains($environment->name, '/')) {
@@ -272,3 +242,4 @@ class Init extends Command
} }
} }
} }
}

View File

@@ -43,6 +43,8 @@ class Kernel extends ConsoleKernel
$schedule->command('uploads:clear')->everyTwoMinutes(); $schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily(); $schedule->command('telescope:prune')->daily();
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -77,12 +79,12 @@ class Kernel extends ConsoleKernel
} }
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
} }
$schedule->job(new PullHelperImageJob($server)) }
$schedule->job(new PullHelperImageJob)
->cron($settings->update_check_frequency) ->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone) ->timezone($settings->instance_timezone)
->onOneServer(); ->onOneServer();
} }
}
private function schedule_updates($schedule) private function schedule_updates($schedule)
{ {

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Helpers;
use App\Models\PrivateKey;
use App\Models\Server;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Process;
class SshMultiplexingHelper
{
public static function serverSshConfiguration(Server $server)
{
$privateKey = PrivateKey::findOrFail($server->private_key_id);
$sshKeyLocation = $privateKey->getKeyLocation();
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
return [
'sshKeyLocation' => $sshKeyLocation,
'muxFilename' => $muxFilename,
];
}
public static function ensureMultiplexedConnection(Server $server)
{
if (! self::isMultiplexingEnabled()) {
return;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$checkCommand .= "{$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
self::establishNewMultiplexedConnection($server);
}
}
public static function establishNewMultiplexedConnection(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
}
public static function removeMuxFile(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$closeCommand .= "{$server->user}@{$server->ip}";
Process::run($closeCommand);
}
public static function generateScpCommand(Server $server, string $source, string $dest)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
return $scp_command;
}
public static function generateSshCommand(Server $server, string $command)
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command = "timeout $timeout ssh ";
if (self::isMultiplexingEnabled()) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
}
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
}
private static function validateSshKey(string $sshKeyLocation): void
{
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
}
}
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
{
$options = "-i {$sshKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR ';
// Bruh
if ($isScp) {
$options .= "-P {$server->port} ";
} else {
$options .= "-p {$server->port} ";
}
return $options;
}
}

View File

@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
} }
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
private function validateDataApplications(Request $request, Server $server) private function validateDataApplications(Request $request, Server $server)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();

View File

@@ -27,6 +27,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
@@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
ray('New container name: ', $this->container_name)->green(); ray('New container name: ', $this->container_name)->green();
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
// Set preview fqdn // Set preview fqdn
@@ -1456,10 +1456,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
], ],
[ [
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true, 'hidden' => true,
'save' => 'git_commit_sha', 'save' => 'git_commit_sha',
], ]
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
@@ -2211,20 +2211,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.'); $this->application_deployment_queue->addLogEntry('Building docker image completed.');
} }
/** private function graceful_shutdown_container(string $containerName, int $timeout = 300)
* @param int $timeout in seconds
*/
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
{ {
try { try {
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->execute_remote_command( $this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
); );
} catch (\Exception $error) { break;
// report error if needed }
usleep(100000);
} }
$isRunning = $this->execute_remote_command(
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
) === 'true';
if ($isRunning) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (\Exception $error) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
$this->remove_container($containerName);
}
private function remove_container(string $containerName)
{
$this->execute_remote_command( $this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
); );

View File

@@ -9,8 +9,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{ {

View File

@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
{ {
try { try {
ray('Cleaning up helper containers on '.$this->server->name); ray('Cleaning up helper containers on '.$this->server->name);
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false); $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containers = format_docker_command_output_to_json($containers); $containerIds = collect(json_decode($containers))->pluck('ID');
if ($containers->count() > 0) { if ($containerIds->count() > 0) {
foreach ($containers as $container) { foreach ($containerIds as $containerId) {
$containerId = data_get($container, 'ID');
ray('Removing container '.$containerId); ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
} }

View File

@@ -3,12 +3,14 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
class CleanupStaleMultiplexedConnections implements ShouldQueue class CleanupStaleMultiplexedConnections implements ShouldQueue
{ {
@@ -16,22 +18,65 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
public function handle() public function handle()
{ {
Server::chunk(100, function ($servers) { $this->cleanupStaleConnections();
foreach ($servers as $server) { $this->cleanupNonExistentServerConnections();
$this->cleanupStaleConnection($server);
}
});
} }
private function cleanupStaleConnection(Server $server) private function cleanupStaleConnections()
{ {
$muxSocket = "/tmp/mux_{$server->id}"; $muxFiles = Storage::disk('ssh-mux')->files();
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
$server = Server::where('uuid', $serverUuid)->first();
if (! $server) {
$this->removeMultiplexFile($muxFile);
continue;
}
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
$checkProcess = Process::run($checkCommand); $checkProcess = Process::run($checkCommand);
if ($checkProcess->exitCode() !== 0) { if ($checkProcess->exitCode() !== 0) {
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null"; $this->removeMultiplexFile($muxFile);
} else {
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
$establishedAt = Carbon::parse(substr($muxContent, 37));
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
if (Carbon::now()->isAfter($expirationTime)) {
$this->removeMultiplexFile($muxFile);
}
}
}
}
private function cleanupNonExistentServerConnections()
{
$muxFiles = Storage::disk('ssh-mux')->files();
$existingServerUuids = Server::pluck('uuid')->toArray();
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
if (! in_array($serverUuid, $existingServerUuids)) {
$this->removeMultiplexFile($muxFile);
}
}
}
private function extractServerUuidFromMuxFile($muxFile)
{
return substr($muxFile, 4);
}
private function removeMultiplexFile($muxFile)
{
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
Process::run($closeCommand); Process::run($closeCommand);
} Storage::disk('ssh-mux')->delete($muxFile);
} }
} }

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {} public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle() public function handle()
{ {
GetContainersStatus::run($this->server); GetContainersStatus::run($this->server);

View File

@@ -23,7 +23,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -80,16 +79,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
public function middleware(): array
{
return [new WithoutOverlapping($this->backup->id)];
}
public function uniqueId(): int
{
return $this->backup->id;
}
public function handle(): void public function handle(): void
{ {
try { try {

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct() {}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService; use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Models\Application; use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct( public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false, public bool $deleteConfigurations,
public bool $deleteVolumes = false) {} public bool $deleteVolumes,
public bool $dockerCleanup,
public bool $deleteConnectedNetworks
) {}
public function handle() public function handle()
{ {
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly': case 'standalone-dragonfly':
case 'standalone-clickhouse': case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get(); $persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource); StopDatabase::run($this->resource, true);
break; break;
case 'service': case 'service':
StopService::run($this->resource); StopService::run($this->resource, true);
DeleteService::run($this->resource); DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break; break;
} }
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) { if ($this->deleteConfigurations) {
$this->resource?->delete_configurations(); $this->resource?->delete_configurations();
} }
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::dispatch($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
$this->resource->forceDelete(); $this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
Artisan::queue('cleanup:stucked-resources'); Artisan::queue('cleanup:stucked-resources');
} }
} }

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {} public function __construct(public Server $server) {}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->id)];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void public function handle(): void
{ {
try { try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public GithubApp $github_app) {} public function __construct(public GithubApp $github_app) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->github_app->uuid))];
}
public function uniqueId(): int
{
return $this->github_app->uuid;
}
public function handle() public function handle()
{ {
try { try {

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000; public $timeout = 1000;
public function middleware(): array public function __construct() {}
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function handle(): void public function handle(): void
{ {

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000; public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {} public function __construct(public Server $server) {}
public function handle(): void public function handle(): void

View File

@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue class ScheduledTaskJob implements ShouldQueue
@@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue
{ {
if ($this->resource instanceof Application) { if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone; $timezone = $this->resource->destination->server->settings->server_timezone;
return $timezone; return $timezone;
} elseif ($this->resource instanceof Service) { } elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone; $timezone = $this->resource->server->settings->server_timezone;
return $timezone; return $timezone;
} }
return 'UTC'; return 'UTC';
} }
public function middleware(): array
{
return [new WithoutOverlapping($this->task->id)];
}
public function uniqueId(): int
{
return $this->task->id;
}
public function handle(): void public function handle(): void
{ {

View File

@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3; public $tries = 1;
public $timeout = 60; public $timeout = 60;
@@ -45,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {} public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle() public function handle()
{ {
try { try {
@@ -93,7 +82,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
private function serverStatus() private function serverStatus()
{ {
['uptime' => $uptime] = $this->server->validateConnection(); ['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) { if ($uptime) {
if ($this->server->unreachable_notification_sent === true) { if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]); $this->server->update(['unreachable_notification_sent' => false]);

View File

@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Team $team) {} public function __construct(public Team $team) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle() public function handle()
{ {
try { try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {} public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle() public function handle()
{ {
if (! $this->server->isServerReady($this->tries)) { if (! $this->server->isServerReady($this->tries)) {

View File

@@ -141,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
if (! $this->createdServer) { if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
} }
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') { } elseif ($this->selectedServerType === 'remote') {
@@ -175,7 +175,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return; return;
} }
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails(); $this->updateServerDetails();
$this->currentState = 'validate-server'; $this->currentState = 'validate-server';
} }
@@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function savePrivateKey() public function savePrivateKey()
{ {
$this->validate([ $this->validate([
'privateKeyName' => 'required', 'privateKeyName' => 'required|string|max:255',
'privateKey' => 'required', 'privateKeyDescription' => 'nullable|string|max:255',
'privateKey' => 'required|string',
]); ]);
$this->createdPrivateKey = PrivateKey::create([
try {
$privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName, 'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription, 'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey, 'private_key' => $this->privateKey,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
$this->createdPrivateKey->save();
$this->createdPrivateKey = $privateKey;
$this->currentState = 'create-server'; $this->currentState = 'create-server';
} catch (\Exception $e) {
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
}
} }
public function saveServer() public function saveServer()

View File

@@ -30,7 +30,6 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [ Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);

View File

@@ -38,7 +38,7 @@ class Form extends Component
} }
$this->destination->delete(); $this->destination->delete();
return redirect()->route('dashboard'); return redirect()->route('destination.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,13 +2,28 @@
namespace App\Livewire; namespace App\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class NavbarDeleteTeam extends Component class NavbarDeleteTeam extends Component
{ {
public function delete() public $team;
public function mount()
{ {
$this->team = currentTeam()->name;
}
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$currentTeam = currentTeam(); $currentTeam = currentTeam();
$currentTeam->delete(); $currentTeam->delete();

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component

View File

@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid; protected string $deploymentUuid;
public bool $docker_cleanup = true;
public function getListeners() public function getListeners()
{ {
$teamId = auth()->user()->currentTeam()->id; $teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop() public function stop()
{ {
StopApplication::run($this->application); StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited'; $this->application->status = 'exited';
$this->application->save(); $this->application->save();
if ($this->application->additional_servers->count() > 0) { if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
]); ]);
} }
public function render()
{
return view('livewire.project.application.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
} }

View File

@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -184,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) { if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
foreach ($containers as $container) { $this->stopContainers($containers, $server, $timeout);
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
} }
}
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high'); GetContainersStatus::run($server);
$this->dispatch('reloadWindow'); $this->application->refresh();
$this->dispatch('containerStatusUpdated');
$this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -203,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id) public function delete(int $pull_request_id)
{ {
try { try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) { if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
foreach ($containers as $container) { $this->stopContainers($containers, $server, $timeout);
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
} }
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete(); ApplicationPreview::where('application_id', $this->application->id)
->where('pull_request_id', $pull_request_id)
->first()
->delete();
$this->application->refresh(); $this->application->refresh();
$this->dispatch('update_links'); $this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.'); $this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ class Previews extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function stopContainers(array $containers, $server, int $timeout)
{
$processes = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
} }

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -12,6 +14,12 @@ class BackupEdit extends Component
public $s3s; public $s3s;
public bool $delete_associated_backups_locally = false;
public bool $delete_associated_backups_s3 = false;
public bool $delete_associated_backups_sftp = false;
public ?string $status = null; public ?string $status = null;
public array $parameters; public array $parameters;
@@ -46,10 +54,24 @@ class BackupEdit extends Component
} }
} }
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
if ($this->delete_associated_backups_locally) {
$this->deleteAssociatedBackupsLocally();
}
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
}
$this->backup->delete(); $this->backup->delete();
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous(); $previousUrl = url()->previous();
$url = Url::fromString($previousUrl); $url = Url::fromString($previousUrl);
@@ -104,4 +126,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage()); $this->dispatch('error', $e->getMessage());
} }
} }
public function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (! $backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if ($backupFolder) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
public function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
public function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],
]);
}
} }

View File

@@ -3,18 +3,28 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
public ?ScheduledDatabaseBackup $backup = null; public ?ScheduledDatabaseBackup $backup = null;
public $database; public $database;
public $executions = []; public $executions = [];
public $setDeletableBackup; public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_sftp = true;
public function getListeners() public function getListeners()
{ {
$userId = auth()->user()->id; $userId = Auth::id();
return [ return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +41,36 @@ class BackupExecutions extends Component
} }
} }
public function deleteBackup($exeuctionId) #[On('deleteBackup')]
public function deleteBackup($executionId, $password)
{ {
$execution = $this->backup->executions()->where('id', $exeuctionId)->first(); if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) { if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.'); $this->dispatch('error', 'Backup execution not found.');
return; return;
} }
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else { } else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
} }
if ($this->delete_backup_s3) {
// Add logic to delete from S3
}
if ($this->delete_backup_sftp) {
// Add logic to delete from SFTP
}
$execution->delete(); $execution->delete();
$this->dispatch('success', 'Backup deleted.'); $this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions(); $this->refreshBackupExecutions();
@@ -82,6 +109,7 @@ class BackupExecutions extends Component
return $server; return $server;
} }
} }
return null; return null;
} }
@@ -92,6 +120,7 @@ class BackupExecutions extends Component
return 'UTC'; return 'UTC';
} }
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;
return $serverTimezone; return $serverTimezone;
} }
@@ -104,6 +133,17 @@ class BackupExecutions extends Component
} catch (\Exception $e) { } catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC')); $dateObj->setTimezone(new \DateTimeZone('UTC'));
} }
return $dateObj->format('Y-m-d H:i:s T'); return $dateObj->format('Y-m-d H:i:s T');
} }
public function render()
{
return view('livewire.project.database.backup-executions', [
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
],
]);
}
} }

View File

@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters; public array $parameters;
public $docker_cleanup = true;
public function getListeners() public function getListeners()
{ {
$userId = auth()->user()->id; $userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop() public function stop()
{ {
StopDatabase::run($this->database); StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited'; $this->database->status = 'exited';
$this->database->save(); $this->database->save();
$this->check_status(); $this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database); $activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function render()
{
return view('livewire.project.database.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
],
]);
}
} }

View File

@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false; public bool $disabled = false;
public string $environmentName = '';
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
} }
public function delete() public function delete()

View File

@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false; public bool $disabled = false;
public string $projectName = '';
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->projectName = Project::findOrFail($this->project_id)->name;
} }
public function delete() public function delete()

View File

@@ -52,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id); $application = $this->service->applications->find($id);
if ($application) { if ($application) {
$application->restart(); $application->restart();
$this->dispatch('success', 'Application restarted successfully.'); $this->dispatch('success', 'Service application restarted successfully.');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -65,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id); $database = $this->service->databases->find($id);
if ($database) { if ($database) {
$database->restart(); $database->restart();
$this->dispatch('success', 'Database restarted successfully.'); $this->dispatch('success', 'Service database restarted successfully.');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class FileStorage extends Component class FileStorage extends Component
@@ -83,8 +85,14 @@ class FileStorage extends Component
} }
} }
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$message = 'File deleted.'; $message = 'File deleted.';
if ($this->fileStorage->is_directory) { if ($this->fileStorage->is_directory) {
@@ -129,6 +137,13 @@ class FileStorage extends Component
public function render() public function render()
{ {
return view('livewire.project.service.file-storage'); return view('livewire.project.service.file-storage', [
'directoryDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
],
'fileDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
],
]);
} }
} }

View File

@@ -20,6 +20,8 @@ class Navbar extends Component
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public $docker_cleanup = true;
public $title = 'Configuration'; public $title = 'Configuration';
public function mount() public function mount()
@@ -42,7 +44,7 @@ class Navbar extends Component
public function serviceStarted() public function serviceStarted()
{ {
$this->dispatch('success', 'Service status changed.'); // $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) { if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true); $this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
@@ -62,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.'); $this->dispatch('success', 'Service status updated.');
} }
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments() public function checkDeployments()
{ {
try { try {
@@ -97,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function stop(bool $forceCleanup = false) public function stop()
{ {
StopService::run($this->service); StopService::run($this->service, false, $this->docker_cleanup);
if ($forceCleanup) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
ServiceStatusChanged::dispatch(); ServiceStatusChanged::dispatch();
} }
@@ -123,4 +115,13 @@ class Navbar extends Component
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function render()
{
return view('livewire.project.service.navbar', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
} }

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class ServiceApplicationView extends Component class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters; public $parameters;
public $docker_cleanup = true;
public $delete_volumes = true;
protected $rules = [ protected $rules = [
'application.human_name' => 'nullable', 'application.human_name' => 'nullable',
'application.description' => 'nullable', 'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean', 'application.is_stripprefix_enabled' => 'nullable|boolean',
]; ];
public function render()
{
return view('livewire.project.service.service-application-view');
}
public function updatedApplicationFqdn() public function updatedApplicationFqdn()
{ {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} }
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$this->application->delete(); $this->application->delete();
$this->dispatch('success', 'Application deleted.'); $this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose'); $this->dispatch('generateDockerCompose');
} }
} }
public function render()
{
return view('livewire.project.service.service-application-view', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
} }

View File

@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key'); $key = data_get($field, 'key');
$value = data_get($field, 'value'); $value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable'); $rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword'); $isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [ $this->fields->put($key, [
'serviceName' => $serviceName, 'serviceName' => $serviceName,
'key' => $key, 'key' => $key,
@@ -47,7 +47,15 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey; $this->validationAttributes["fields.$key.value"] = $fieldKey;
} }
} }
$this->fields = $this->fields->sortBy('name'); $this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
return data_get($field, 'isPassword') ? 1 : 0;
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
})->flatMap(function ($group) {
return $group;
});
} }
public function saveCompose($raw) public function saveCompose($raw)

View File

@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob; use App\Jobs\DeleteResourceJob;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{ {
public $resource; public $resource;
public $resourceName;
public $projectUuid; public $projectUuid;
public $environmentName; public $environmentName;
@@ -18,22 +25,93 @@ class Danger extends Component
public bool $delete_volumes = true; public bool $delete_volumes = true;
public bool $docker_cleanup = true;
public bool $delete_connected_networks = true;
public ?string $modalId = null; public ?string $modalId = null;
public string $resourceDomain = '';
public function mount() public function mount()
{ {
$this->modalId = new Cuid2;
$parameters = get_route_parameters(); $parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid'); $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name'); $this->environmentName = data_get($parameters, 'environment_name');
if ($this->resource === null) {
if (isset($parameters['service_uuid'])) {
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
} elseif (isset($parameters['stack_service_uuid'])) {
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
}
} }
public function delete() if ($this->resource === null) {
$this->resourceName = 'Unknown Resource';
return;
}
if (! method_exists($this->resource, 'type')) {
$this->resourceName = 'Unknown Resource';
return;
}
switch ($this->resource->type()) {
case 'application':
$this->resourceName = $this->resource->name ?? 'Application';
break;
case 'standalone-postgresql':
case 'standalone-redis':
case 'standalone-mongodb':
case 'standalone-mysql':
case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$this->resourceName = $this->resource->name ?? 'Database';
break;
case 'service':
$this->resourceName = $this->resource->name ?? 'Service';
break;
case 'service-application':
$this->resourceName = $this->resource->name ?? 'Service Application';
break;
case 'service-database':
$this->resourceName = $this->resource->name ?? 'Service Database';
break;
default:
$this->resourceName = 'Unknown Resource';
}
}
public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! $this->resource) {
$this->addError('resource', 'Resource not found.');
return;
}
try { try {
// $this->authorize('delete', $this->resource);
$this->resource->delete(); $this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes); DeleteResourceJob::dispatch(
$this->resource,
$this->delete_configurations,
$this->delete_volumes,
$this->docker_cleanup,
$this->delete_connected_networks
);
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,
@@ -43,4 +121,19 @@ class Danger extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function render()
{
return view('livewire.project.shared.danger', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
} }

View File

@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -115,8 +117,14 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
} }
public function removeServer(int $network_id, int $server_id) public function removeServer(int $network_id, int $server_id, $password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
@@ -108,14 +109,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server); $command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0]; $command = $command[0];
} }
$sshCommand = generateSshCommand($this->server, $command); $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else { } else {
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}"; $command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
if ($this->server->isNonRoot()) { if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server); $command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0]; $command = $command[0];
} }
$sshCommand = generateSshCommand($this->server, $command); $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} }
} else { } else {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
@@ -124,14 +125,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server); $command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0]; $command = $command[0];
} }
$sshCommand = generateSshCommand($this->server, $command); $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else { } else {
$command = "docker logs -n {$this->numberOfLines} {$this->container}"; $command = "docker logs -n {$this->numberOfLines} {$this->container}";
if ($this->server->isNonRoot()) { if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server); $command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0]; $command = $command[0];
} }
$sshCommand = generateSshCommand($this->server, $command); $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} }
} }
if ($refresh) { if ($refresh) {

View File

@@ -7,7 +7,9 @@ use Livewire\Component;
class Executions extends Component class Executions extends Component
{ {
public $executions = []; public $executions = [];
public $selectedKey; public $selectedKey;
public $task; public $task;
public function getListeners() public function getListeners()
@@ -42,6 +44,7 @@ class Executions extends Component
return $this->task->service->destination->server; return $this->task->service->destination->server;
} }
} }
return null; return null;
} }
@@ -52,6 +55,7 @@ class Executions extends Component
return 'UTC'; return 'UTC';
} }
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;
return $serverTimezone; return $serverTimezone;
} }
@@ -64,6 +68,7 @@ class Executions extends Component
} catch (\Exception $e) { } catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC')); $dateObj->setTimezone(new \DateTimeZone('UTC'));
} }
return $dateObj->format('Y-m-d H:i:s T'); return $dateObj->format('Y-m-d H:i:s T');
} }
} }

View File

@@ -20,6 +20,8 @@ class Show extends Component
public string $type; public string $type;
public string $scheduledTaskName;
protected $rules = [ protected $rules = [
'task.enabled' => 'required|boolean', 'task.enabled' => 'required|boolean',
'task.name' => 'required|string', 'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2; $this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
$this->scheduledTaskName = $this->task->name;
} }
public function instantSave() public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete(); $this->task->delete();
if ($this->type == 'application') { if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters); return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else { } else {
return redirect()->route('project.service.configuration', $this->parameters); return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e); return handleError($e);

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages; namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume; use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@@ -36,8 +38,14 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully'); $this->dispatch('success', 'Storage updated successfully');
} }
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$this->storage->delete(); $this->storage->delete();
$this->dispatch('refreshStorages'); $this->dispatch('refreshStorages');
} }

View File

@@ -2,12 +2,27 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server; use App\Models\Server;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class Terminal extends Component class Terminal extends Component
{ {
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
];
}
public function closeTerminal()
{
$this->dispatch('reloadWindow');
}
#[On('send-terminal-command')] #[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid) public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{ {
@@ -19,9 +34,9 @@ class Terminal extends Component
if ($status !== 'running') { if ($status !== 'running') {
return; return;
} }
$command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else { } else {
$command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} }
// ssh command is sent back to frontend then to websocket // ssh command is sent back to frontend then to websocket

View File

@@ -3,17 +3,13 @@
namespace App\Livewire\Security\PrivateKey; namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component; use Livewire\Component;
use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component class Create extends Component
{ {
use WithRateLimiting; public string $name = '';
public string $name; public string $value = '';
public string $value;
public ?string $from = null; public ?string $from = null;
@@ -26,72 +22,69 @@ class Create extends Component
'value' => 'required|string', 'value' => 'required|string',
]; ];
protected $validationAttributes = [
'name' => 'name',
'value' => 'private Key',
];
public function generateNewRSAKey() public function generateNewRSAKey()
{ {
try { $this->generateNewKey('rsa');
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function generateNewEDKey() public function generateNewEDKey()
{ {
try { $this->generateNewKey('ed25519');
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function updated($updateProperty) private function generateNewKey($type)
{ {
if ($updateProperty === 'value') { $keyData = PrivateKey::generateNewKeyPair($type);
try { $this->setKeyData($keyData);
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
if ($this->$updateProperty === '') {
$this->publicKey = '';
} else {
$this->publicKey = 'Invalid private key';
} }
public function updated($property)
{
if ($property === 'value') {
$this->validatePrivateKey();
} }
} }
$this->validateOnly($updateProperty);
}
public function createPrivateKey() public function createPrivateKey()
{ {
$this->validate(); $this->validate();
try { try {
$this->value = trim($this->value); $privateKey = PrivateKey::createAndStore([
if (! str_ends_with($this->value, "\n")) {
$this->value .= "\n";
}
$private_key = PrivateKey::create([
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->description,
'private_key' => $this->value, 'private_key' => trim($this->value)."\n",
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
if ($this->from === 'server') {
return redirect()->route('dashboard');
}
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]); return $this->redirectAfterCreation($privateKey);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function setKeyData(array $keyData)
{
$this->name = $keyData['name'];
$this->description = $keyData['description'];
$this->value = $keyData['private_key'];
$this->publicKey = $keyData['public_key'];
}
private function validatePrivateKey()
{
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
$this->publicKey = $validationResult['publicKey'];
if (! $validationResult['isValid']) {
$this->addError('value', 'Invalid private key');
}
}
private function redirectAfterCreation(PrivateKey $privateKey)
{
return $this->from === 'server'
? redirect()->route('dashboard')
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
}
} }

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use Livewire\Component;
class Index extends Component
{
public function render()
{
$privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
return view('livewire.security.private-key.index', [
'privateKeys' => $privateKeys,
])->layout('components.layout');
}
public function cleanupUnusedKeys()
{
PrivateKey::cleanupUnusedKeys();
$this->dispatch('success', 'Unused keys have been cleaned up.');
}
}

View File

@@ -29,25 +29,27 @@ class Show extends Component
try { try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); abort(404);
} }
} }
public function loadPublicKey() public function loadPublicKey()
{ {
$this->public_key = $this->private_key->publicKey(); $this->public_key = $this->private_key->getPublicKey();
if ($this->public_key === 'Error loading private key') {
$this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
}
} }
public function delete() public function delete()
{ {
try { try {
if ($this->private_key->isEmpty()) { $this->private_key->safeDelete();
$this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('security.private-key.index'); return redirect()->route('security.private-key.index');
} } catch (\Exception $e) {
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.'); $this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -56,8 +58,9 @@ class Show extends Component
public function changePrivateKey() public function changePrivateKey()
{ {
try { try {
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key); $this->private_key->updatePrivateKey([
$this->private_key->save(); 'private_key' => formatPrivateKey($this->private_key->private_key),
]);
refresh_server_connection($this->private_key); refresh_server_connection($this->private_key);
$this->dispatch('success', 'Private key updated.'); $this->dispatch('success', 'Private key updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
{ {
try { try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token); ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true; $server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain; $server->ip = $this->ssh_domain;
$server->save(); $server->save();
$server->settings->save(); $server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); $this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class Delete extends Component class Delete extends Component
@@ -11,8 +13,13 @@ class Delete extends Component
public $server; public $server;
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$this->authorize('delete', $this->server); $this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) { if ($this->server->hasDefinedResources()) {

View File

@@ -24,11 +24,16 @@ class Form extends Component
public $timezones; public $timezones;
protected $listeners = [ public function getListeners()
'serverInstalled', {
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
'refreshServerShow' => 'serverInstalled', 'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh', 'revalidate' => '$refresh',
]; ];
}
protected $rules = [ protected $rules = [
'server.name' => 'required', 'server.name' => 'required',
@@ -96,6 +101,12 @@ class Form extends Component
} }
} }
public function cloudflareTunnelConfigured()
{
$this->serverInstalled();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled() public function serverInstalled()
{ {
$this->server->refresh(); $this->server->refresh();
@@ -238,4 +249,12 @@ class Form extends Component
$this->server->settings->save(); $this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.'); $this->dispatch('success', 'Server timezone updated.');
} }
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
} }

View File

@@ -39,6 +39,7 @@ class Proxy extends Component
{ {
$this->server->proxy = null; $this->server->proxy = null;
$this->server->save(); $this->server->save();
$this->dispatch('proxyChanged');
} }
public function selectProxy($proxy_type) public function selectProxy($proxy_type)
@@ -47,7 +48,7 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type); $this->server->proxy->set('type', $proxy_type);
$this->server->save(); $this->server->save();
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
if ($this->selectedProxy !== 'NONE') { if ($this->server->proxySet()) {
StartProxy::run($this->server, false); StartProxy::run($this->server, false);
} }
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');

View File

@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged; use App\Events\ProxyStatusChanged;
use App\Models\Server; use App\Models\Server;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Livewire\Component; use Livewire\Component;
class Deploy extends Component class Deploy extends Component
@@ -29,6 +31,7 @@ class Deploy extends Component
'serverRefresh' => 'proxyStatusUpdated', 'serverRefresh' => 'proxyStatusUpdated',
'checkProxy', 'checkProxy',
'startProxy', 'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
]; ];
} }
@@ -94,21 +97,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true) public function stop(bool $forceStop = true)
{ {
try { try {
if ($this->server->isSwarm()) { $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
instant_remote_process([ $timeout = 30;
'docker service rm coolify-proxy_traefik',
], $this->server); $process = $this->stopContainer($containerName, $timeout);
} else {
instant_remote_process([ $startTime = time();
'docker rm -f coolify-proxy', while ($process->running()) {
], $this->server); if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
} }
$this->server->proxy->status = 'exited'; usleep(100000);
$this->server->proxy->force_stop = $forceStop; }
$this->server->save();
$this->dispatch('proxyStatusUpdated'); $this->removeContainer($containerName);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->server->proxy->force_stop = $forceStop;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->dispatch('proxyStatusUpdated');
} }
} }
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName)
{
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
}
private function removeContainer(string $containerName)
{
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
}
} }

View File

@@ -11,7 +11,7 @@ class Show extends Component
public $parameters = []; public $parameters = [];
protected $listeners = ['proxyStatusUpdated']; protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
public $parameters; public $parameters;
public function setPrivateKey($newPrivateKeyId) public function setPrivateKey($privateKeyId)
{ {
try { try {
$oldPrivateKeyId = $this->server->private_key_id; $privateKey = PrivateKey::findOrFail($privateKeyId);
refresh_server_connection($this->server->privateKey); $this->server->update(['private_key_id' => $privateKey->id]);
$this->server->update([
'private_key_id' => $newPrivateKeyId,
]);
$this->server->refresh(); $this->server->refresh();
refresh_server_connection($this->server->privateKey); $this->dispatch('success', 'Private key updated successfully.');
$this->checkConnection(); } catch (\Exception $e) {
} catch (\Throwable $e) { $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
$this->server->update([
'private_key_id' => $oldPrivateKeyId,
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
return handleError($e, $this);
} }
} }
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } else {
ray($error); ray($error);
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
return; return;
} }

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class AdminView extends Component class AdminView extends Component
@@ -73,8 +75,13 @@ class AdminView extends Component
$team->delete(); $team->delete();
} }
public function delete($id) public function delete($id, $password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! auth()->user()->isInstanceAdmin()) { if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users'); return $this->dispatch('error', 'You are not authorized to delete users');
} }

View File

@@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use RuntimeException; use RuntimeException;
@@ -149,12 +151,64 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
} }
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
: getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
return $containers->pluck('Names')->toArray();
}
public function stopContainers(array $containerNames, $server, int $timeout = 600)
{
$processes = [];
foreach ($containerNames as $containerName) {
$processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach ($finishedProcesses as $containerName => $process) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
public function removeContainer(string $containerName, $server)
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
public function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
public function delete_configurations() public function delete_configurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) { if (str($workdir)->endsWith($this->uuid)) {
ray('Deleting workdir');
instant_remote_process(['rm -rf '.$this->workdir()], $server, false); instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
} }
} }
@@ -176,6 +230,13 @@ class Application extends BaseModel
} }
} }
public function delete_connected_networks($uuid)
{
$server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function additional_servers() public function additional_servers()
{ {
return $this->belongsToMany(Server::class, 'additional_destinations') return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -1034,6 +1095,7 @@ class Application extends BaseModel
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} }
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$commands = collect([]); $commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) { $services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', [])); $serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -1166,7 +1228,6 @@ class Application extends BaseModel
} else { } else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name."); throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
} }
} }
public function parseContainerLabels(?ApplicationPreview $preview = null) public function parseContainerLabels(?ApplicationPreview $preview = null)

View File

@@ -2,6 +2,9 @@
namespace App\Models; namespace App\Models;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Crypt\PublicKeyLoader;
@@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader;
)] )]
class PrivateKey extends BaseModel class PrivateKey extends BaseModel
{ {
use WithRateLimiting;
protected $fillable = [ protected $fillable = [
'name', 'name',
'description', 'description',
'private_key', 'private_key',
'is_git_related', 'is_git_related',
'team_id', 'team_id',
'fingerprint',
];
protected $casts = [
'private_key' => 'encrypted',
]; ];
protected static function booted() protected static function booted()
{ {
static::saving(function ($key) { static::saving(function ($key) {
$privateKey = data_get($key, 'private_key'); $key->private_key = formatPrivateKey($key->private_key);
if (substr($privateKey, -1) !== "\n") {
$key->private_key = $privateKey."\n"; if (! self::validatePrivateKey($key->private_key)) {
throw ValidationException::withMessages([
'private_key' => ['The private key is invalid.'],
]);
}
$key->fingerprint = self::generateFingerprint($key->private_key);
if (self::fingerprintExists($key->fingerprint, $key->id)) {
throw ValidationException::withMessages([
'private_key' => ['This private key already exists.'],
]);
} }
}); });
static::deleted(function ($key) {
self::deleteFromStorage($key);
});
}
public function getPublicKey()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
} }
public static function ownedByCurrentTeam(array $select = ['*']) public static function ownedByCurrentTeam(array $select = ['*'])
{ {
$selectArray = collect($select)->concat(['id']); $selectArray = collect($select)->concat(['id']);
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all()); return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
} }
public function publicKey() public static function validatePrivateKey($privateKey)
{ {
try { try {
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']); PublicKeyLoader::load($privateKey);
} catch (\Throwable $e) {
return 'Error loading private key';
}
}
public function isEmpty()
{
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
return true; return true;
} catch (\Throwable $e) {
return false;
}
} }
return false; public static function createAndStore(array $data)
{
$privateKey = new self($data);
$privateKey->save();
$privateKey->storeInFileSystem();
return $privateKey;
}
public static function generateNewKeyPair($type = 'rsa')
{
try {
$instance = new self;
$instance->rateLimit(10);
$name = generate_random_name();
$description = 'Created by Coolify';
$keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
return [
'name' => $name,
'description' => $description,
'private_key' => $keyPair['private'],
'public_key' => $keyPair['public'],
];
} catch (\Throwable $e) {
throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
}
}
public static function extractPublicKeyFromPrivate($privateKey)
{
try {
$key = PublicKeyLoader::load($privateKey);
return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
return null;
}
}
public static function validateAndExtractPublicKey($privateKey)
{
$isValid = self::validatePrivateKey($privateKey);
$publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
return [
'isValid' => $isValid,
'publicKey' => $publicKey,
];
}
public function storeInFileSystem()
{
$filename = "ssh_key@{$this->uuid}";
Storage::disk('ssh-keys')->put($filename, $this->private_key);
return "/var/www/html/storage/app/ssh/keys/{$filename}";
}
public static function deleteFromStorage(self $privateKey)
{
$filename = "ssh_key@{$privateKey->uuid}";
Storage::disk('ssh-keys')->delete($filename);
}
public function getKeyLocation()
{
return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
}
public function updatePrivateKey(array $data)
{
$this->update($data);
$this->storeInFileSystem();
return $this;
} }
public function servers() public function servers()
@@ -85,4 +184,53 @@ class PrivateKey extends BaseModel
{ {
return $this->hasMany(GitlabApp::class); return $this->hasMany(GitlabApp::class);
} }
public function isInUse()
{
return $this->servers()->exists()
|| $this->applications()->exists()
|| $this->githubApps()->exists()
|| $this->gitlabApps()->exists();
}
public function safeDelete()
{
if (! $this->isInUse()) {
$this->delete();
return true;
}
return false;
}
public static function generateFingerprint($privateKey)
{
try {
$key = PublicKeyLoader::load($privateKey);
$publicKey = $key->getPublicKey();
return $publicKey->getFingerprint('sha256');
} catch (\Throwable $e) {
return null;
}
}
private static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::where('fingerprint', $fingerprint);
if (! is_null($excludeId)) {
$query->where('id', '!=', $excludeId);
}
return $query->exists();
}
public static function cleanupUnusedKeys()
{
self::ownedByCurrentTeam()->each(function ($privateKey) {
$privateKey->safeDelete();
});
}
} }

View File

@@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel
{ {
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
} }
public function server() public function server()
{ {
if ($this->database) { if ($this->database) {
if ($this->database->destination && $this->database->destination->server) { if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server; $server = $this->database->destination->server;
return $server; return $server;
} }
} }
return null; return null;
} }
} }

View File

@@ -4,8 +4,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use App\Models\Service;
use App\Models\Application;
class ScheduledTask extends BaseModel class ScheduledTask extends BaseModel
{ {
@@ -37,19 +35,23 @@ class ScheduledTask extends BaseModel
if ($this->application) { if ($this->application) {
if ($this->application->destination && $this->application->destination->server) { if ($this->application->destination && $this->application->destination->server) {
$server = $this->application->destination->server; $server = $this->application->destination->server;
return $server; return $server;
} }
} elseif ($this->service) { } elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) { if ($this->service->destination && $this->service->destination->server) {
$server = $this->service->destination->server; $server = $this->service->destination->server;
return $server; return $server;
} }
} elseif ($this->database) { } elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) { if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server; $server = $this->database->destination->server;
return $server; return $server;
} }
} }
return null; return null;
} }
} }

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use App\Notifications\Server\Revived;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -156,6 +155,11 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
} }
public function proxySet()
{
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
public function setupDefault404Redirect() public function setupDefault404Redirect()
{ {
$dynamic_conf_path = $this->proxyPath().'/dynamic'; $dynamic_conf_path = $this->proxyPath().'/dynamic';
@@ -163,11 +167,11 @@ class Server extends BaseModel
$redirect_url = $this->proxy->redirect_url; $redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) { if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === 'CADDY') { } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
} }
if (empty($redirect_url)) { if (empty($redirect_url)) {
if ($proxy_type === 'CADDY') { if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 { $conf = ':80, :443 {
respond 404 respond 404
}'; }';
@@ -237,7 +241,7 @@ respond 404
$conf; $conf;
$base64 = base64_encode($conf); $base64 = base64_encode($conf);
} elseif ($proxy_type === 'CADDY') { } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 { $conf = ":80, :443 {
redir $redirect_url redir $redirect_url
}"; }";
@@ -253,9 +257,6 @@ respond 404
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this); ], $this);
if (config('app.env') == 'local') {
ray($conf);
}
if ($proxy_type === 'CADDY') { if ($proxy_type === 'CADDY') {
$this->reloadCaddy(); $this->reloadCaddy();
} }
@@ -833,9 +834,9 @@ $schema://$host {
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
})->filter(function ($item) { })->flatten()->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db'; return data_get($item, 'name') !== 'coolify-db';
})->flatten(); });
} }
public function applications() public function applications()
@@ -879,6 +880,35 @@ $schema://$host {
return $this->hasMany(Service::class); return $this->hasMany(Service::class);
} }
public function port(): Attribute
{
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9]/', '', $value);
}
);
}
public function user(): Attribute
{
return Attribute::make(
get: function ($value) {
$sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
return $sanitizedValue;
}
);
}
public function ip(): Attribute
{
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
}
);
}
public function getIp(): Attribute public function getIp(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -951,10 +981,9 @@ $schema://$host {
public function isFunctional() public function isFunctional()
{ {
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
if (! $isFunctional) { if (! $isFunctional) {
Storage::disk('ssh-keys')->delete($private_key_filename); Storage::disk('ssh-mux')->delete($this->muxFilename());
Storage::disk('ssh-mux')->delete($mux_filename);
} }
return $isFunctional; return $isFunctional;
@@ -1006,9 +1035,10 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker'); return data_get($this, 'settings.is_swarm_worker');
} }
public function validateConnection() public function validateConnection($isManualCheck = true)
{ {
config()->set('constants.ssh.mux_enabled', false); config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
// ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
$server = Server::find($this->id); $server = Server::find($this->id);
if (! $server) { if (! $server) {
@@ -1018,7 +1048,6 @@ $schema://$host {
return ['uptime' => false, 'error' => 'Server skipped.']; return ['uptime' => false, 'error' => 'Server skipped.'];
} }
try { try {
// EC2 does not have `uptime` command, lol
instant_remote_process(['ls /'], $server); instant_remote_process(['ls /'], $server);
$server->settings()->update([ $server->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
@@ -1027,7 +1056,6 @@ $schema://$host {
'unreachable_count' => 0, 'unreachable_count' => 0,
]); ]);
if (data_get($server, 'unreachable_notification_sent') === true) { if (data_get($server, 'unreachable_notification_sent') === true) {
// $server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]); $server->update(['unreachable_notification_sent' => false]);
} }
@@ -1156,4 +1184,24 @@ $schema://$host {
{ {
return $this->settings->is_build_server; return $this->settings->is_build_server;
} }
public static function createWithPrivateKey(array $data, PrivateKey $privateKey)
{
$server = new self($data);
$server->privateKey()->associate($privateKey);
$server->save();
return $server;
}
public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null)
{
$this->update($data);
if ($privateKey) {
$this->privateKey()->associate($privateKey);
$this->save();
}
return $this;
}
} }

View File

@@ -6,7 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -131,15 +133,81 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function getContainersToStop(): array
{
$containersToStop = [];
$applications = $this->applications()->get();
foreach ($applications as $application) {
$containersToStop[] = "{$application->name}-{$this->uuid}";
}
$dbs = $this->databases()->get();
foreach ($dbs as $db) {
$containersToStop[] = "{$db->name}-{$this->uuid}";
}
return $containersToStop;
}
public function stopContainers(array $containerNames, $server, int $timeout = 300)
{
$processes = [];
foreach ($containerNames as $containerName) {
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
public function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
public function removeContainer(string $containerName, $server)
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
public function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
public function delete_configurations() public function delete_configurations()
{ {
$server = data_get($this, 'server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) { if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(['rm -rf '.$this->workdir()], $server, false); instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
} }
} }
public function delete_connected_networks($uuid)
{
$server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function status() public function status()
{ {
$applications = $this->applications; $applications = $this->applications;

View File

@@ -52,7 +52,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)."; $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
return $message; return $message;
} }
@@ -60,7 +60,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toTelegram(): array public function toTelegram(): array
{ {
return [ return [
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).", 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
]; ];
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Traits; namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -42,7 +43,7 @@ trait ExecuteRemoteCommand
$command = parseLineForSudo($command, $this->server); $command = parseLineForSudo($command, $this->server);
} }
} }
$remote_command = generateSshCommand($this->server, $command); $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = str($output)->trim(); $output = str($output)->trim();
if ($output->startsWith('╔')) { if ($output->startsWith('╔')) {

View File

@@ -134,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return 'exited'; return 'exited';
} }
$container = format_docker_command_output_to_json($container); $container = format_docker_command_output_to_json($container);
if ($container->isEmpty()) {
return 'exited';
}
if ($all_data) { if ($all_data) {
return $container[0]; return $container[0];
} }

View File

@@ -3,6 +3,7 @@
use App\Actions\CoolifyTask\PrepareCoolifyTask; use App\Actions\CoolifyTask\PrepareCoolifyTask;
use App\Data\CoolifyTaskArgs; use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes; use App\Enums\ActivityTypes;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey; use App\Models\PrivateKey;
@@ -10,9 +11,8 @@ use App\Models\Server;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Activitylog\Contracts\Activity; use Spatie\Activitylog\Contracts\Activity;
@@ -26,29 +26,28 @@ function remote_process(
$callEventOnFinish = null, $callEventOnFinish = null,
$callEventData = null $callEventData = null
): Activity { ): Activity {
if (is_null($type)) { $type = $type ?? ActivityTypes::INLINE->value;
$type = ActivityTypes::INLINE->value; $command = $command instanceof Collection ? $command->toArray() : $command;
}
if ($command instanceof Collection) {
$command = $command->toArray();
}
if ($server->isNonRoot()) { if ($server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $server); $command = parseCommandsByLineForSudo(collect($command), $server);
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
if (auth()->user()) {
$teams = auth()->user()->teams->pluck('id'); if (Auth::check()) {
$teams = Auth::user()->teams->pluck('id');
if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
throw new \Exception('User is not part of the team that owns this server'); throw new \Exception('User is not part of the team that owns this server');
} }
} }
SshMultiplexingHelper::ensureMultiplexedConnection($server);
return resolve(PrepareCoolifyTask::class, [ return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs( 'remoteProcessArgs' => new CoolifyTaskArgs(
server_uuid: $server->uuid, server_uuid: $server->uuid,
command: <<<EOT command: $command_string,
{$command_string}
EOT,
type: $type, type: $type,
type_uuid: $type_uuid, type_uuid: $type_uuid,
model: $model, model: $model,
@@ -58,313 +57,67 @@ function remote_process(
), ),
])(); ])();
} }
function server_ssh_configuration(Server $server)
{
$uuid = data_get($server, 'uuid');
if (is_null($uuid)) {
throw new \Exception('Server does not have a uuid');
}
$private_key_filename = "id.root@{$server->uuid}";
$location = '/var/www/html/storage/app/ssh/keys/'.$private_key_filename;
$mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename();
return [
'location' => $location,
'mux_filename' => $mux_filename,
'private_key_filename' => $private_key_filename,
];
}
function savePrivateKeyToFs(Server $server)
{
if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key");
}
['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
Storage::disk('ssh-keys')->makeDirectory('.');
Storage::disk('ssh-mux')->makeDirectory('.');
Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
return $location;
}
function generateScpCommand(Server $server, string $source, string $dest)
{
$user = $server->user;
$port = $server->port;
$privateKeyLocation = savePrivateKeyToFs($server);
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
$muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false;
// ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
ensureMultiplexedConnection($server);
// ray('Using SSH Multiplexing')->green();
} else {
// ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$scp_command .= "-i {$privateKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-P {$port} "
."{$source} "
."{$user}@{$server->ip}:{$dest}";
return $scp_command;
}
function instant_scp(string $source, string $dest, Server $server, $throwError = true) function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{ {
$timeout = config('constants.ssh.command_timeout'); $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
$scp_command = generateScpCommand($server, $source, $dest); $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
$process = Process::timeout($timeout)->run($scp_command);
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();
if ($exitCode !== 0) { if ($exitCode !== 0) {
if (! $throwError) { return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
return null;
} }
return excludeCertainErrors($process->errorOutput(), $exitCode); return $output === 'null' ? null : $output;
}
if ($output === 'null') {
$output = null;
}
return $output;
}
function generateSshCommand(Server $server, string $command)
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$user = $server->user;
$port = $server->port;
$privateKeyLocation = savePrivateKeyToFs($server);
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command = "timeout $timeout ssh ";
$muxEnabled = config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false;
// ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
// Always use multiplexing when enabled
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
ensureMultiplexedConnection($server);
// ray('Using SSH Multiplexing')->green();
} else {
// ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "-i {$privateKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-p {$port} "
."{$user}@{$server->ip} "
." 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
function ensureMultiplexedConnection(Server $server)
{
if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
return;
}
static $ensuredConnections = [];
if (isset($ensuredConnections[$server->id])) {
if (! shouldResetMultiplexedConnection($server)) {
// ray('Using Existing Multiplexed Connection')->green();
return;
}
}
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$checkCommand .= " {$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() === 0) {
// ray('Existing Multiplexed Connection is Valid')->green();
$ensuredConnections[$server->id] = [
'timestamp' => now(),
'muxSocket' => $muxSocket,
];
return;
}
// ray('Establishing New Multiplexed Connection')->orange();
$privateKeyLocation = savePrivateKeyToFs($server);
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$establishCommand .= "-i {$privateKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-p {$server->port} "
."{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
$ensuredConnections[$server->id] = [
'timestamp' => now(),
'muxSocket' => $muxSocket,
];
// ray('Established New Multiplexed Connection')->green();
}
function shouldResetMultiplexedConnection(Server $server)
{
if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
return false;
}
static $ensuredConnections = [];
if (! isset($ensuredConnections[$server->id])) {
return true;
}
$lastEnsured = $ensuredConnections[$server->id]['timestamp'];
$muxPersistTime = config('constants.ssh.mux_persist_time');
$resetInterval = strtotime($muxPersistTime) - time();
return $lastEnsured->addSeconds($resetInterval)->isPast();
}
function resetMultiplexedConnection(Server $server)
{
if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
return;
}
static $ensuredConnections = [];
if (isset($ensuredConnections[$server->id])) {
$muxSocket = $ensuredConnections[$server->id]['muxSocket'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
Process::run($closeCommand);
unset($ensuredConnections[$server->id]);
}
} }
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
{ {
static $processCount = 0; $command = $command instanceof Collection ? $command->toArray() : $command;
$processCount++;
$timeout = config('constants.ssh.command_timeout');
if ($command instanceof Collection) {
$command = $command->toArray();
}
if ($server->isNonRoot() && ! $no_sudo) { if ($server->isNonRoot() && ! $no_sudo) {
$command = parseCommandsByLineForSudo(collect($command), $server); $command = parseCommandsByLineForSudo(collect($command), $server);
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$start_time = microtime(true); // $start_time = microtime(true);
$sshCommand = generateSshCommand($server, $command_string); $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
$process = Process::timeout($timeout)->run($sshCommand); $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
$end_time = microtime(true); // $end_time = microtime(true);
$execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
// ray('SSH command execution time:', $execution_time.' ms')->orange(); // ray('SSH command execution time:', $execution_time.' ms')->orange();
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();
if ($exitCode !== 0) { if ($exitCode !== 0) {
if (! $throwError) { return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
return null;
} }
return excludeCertainErrors($process->errorOutput(), $exitCode); return $output === 'null' ? null : $output;
}
if ($output === 'null') {
$output = null;
} }
return $output;
}
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
{ {
$ignoredErrors = collect([ $ignoredErrors = collect([
'Permission denied (publickey', 'Permission denied (publickey',
'Could not resolve hostname', 'Could not resolve hostname',
]); ]);
$ignored = false; $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error));
foreach ($ignoredErrors as $ignoredError) {
if (Str::contains($errorOutput, $ignoredError)) {
$ignored = true;
break;
}
}
if ($ignored) { if ($ignored) {
// TODO: Create new exception and disable in sentry // TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorOutput, $exitCode); throw new \RuntimeException($errorOutput, $exitCode);
} }
throw new \RuntimeException($errorOutput, $exitCode); throw new \RuntimeException($errorOutput, $exitCode);
} }
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
{ {
$application = Application::find(data_get($application_deployment_queue, 'application_id'));
$is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
if (is_null($application_deployment_queue)) { if (is_null($application_deployment_queue)) {
return collect([]); return collect([]);
} }
$application = Application::find(data_get($application_deployment_queue, 'application_id'));
$is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
try { try {
$decoded = json_decode( $decoded = json_decode(
data_get($application_deployment_queue, 'logs'), data_get($application_deployment_queue, 'logs'),
@@ -379,7 +132,8 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (! $is_debug_enabled) { if (! $is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
} }
$formatted = $formatted
return $formatted
->sortBy(fn ($i) => data_get($i, 'order')) ->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) { ->map(function ($i) {
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u')); data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
@@ -421,36 +175,22 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
return $deploymentLogLines; return $deploymentLogLines;
}, collect()); }, collect());
return $formatted;
} }
function remove_iip($text) function remove_iip($text)
{ {
$text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text); $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text); return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
} }
function remove_mux_and_private_key(Server $server)
{
$muxFilename = $server->muxFilename();
$privateKeyLocation = savePrivateKeyToFs($server);
$closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFilename);
Storage::disk('ssh-keys')->delete($privateKeyLocation);
}
function refresh_server_connection(?PrivateKey $private_key = null) function refresh_server_connection(?PrivateKey $private_key = null)
{ {
if (is_null($private_key)) { if (is_null($private_key)) {
return; return;
} }
foreach ($private_key->servers as $server) { foreach ($private_key->servers as $server) {
$muxFilename = $server->muxFilename(); SshMultiplexingHelper::removeMuxFile($server);
$closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFilename);
} }
} }
@@ -468,9 +208,8 @@ function checkRequiredCommands(Server $server)
break; break;
} }
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) { if (! $commandFound) {
continue;
}
break; break;
} }
} }
}

View File

@@ -505,6 +505,12 @@ function sslip(Server $server)
return "http://$baseIp.sslip.io"; return "http://$baseIp.sslip.io";
} }
// ipv6
if (str($server->ip)->contains(':')) {
$ipv6 = str($server->ip)->replace(':', '-');
return "http://{$ipv6}.sslip.io";
}
return "http://{$server->ip}.sslip.io"; return "http://{$server->ip}.sslip.io";
} }
@@ -1247,6 +1253,10 @@ function get_public_ips()
} }
$settings->update(['public_ipv4' => $ipv4]); $settings->update(['public_ipv4' => $ipv4]);
} }
} catch (\Exception $e) {
echo "Error: {$e->getMessage()}\n";
}
try {
$ipv6 = $second->output(); $ipv6 = $second->output();
if ($ipv6) { if ($ipv6) {
$ipv6 = trim($ipv6); $ipv6 = trim($ipv6);
@@ -2924,10 +2934,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
$parsedServices = collect([]); $parsedServices = collect([]);
ray()->clearAll(); // ray()->clearAll();
$allMagicEnvironments = collect([]); $allMagicEnvironments = collect([]);
foreach ($services as $serviceName => $service) { foreach ($services as $serviceName => $service) {
$predefinedPort = null;
$magicEnvironments = collect([]); $magicEnvironments = collect([]);
$image = data_get_str($service, 'image'); $image = data_get_str($service, 'image');
$environment = collect(data_get($service, 'environment', [])); $environment = collect(data_get($service, 'environment', []));
@@ -2936,6 +2947,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$isDatabase = isDatabaseImage(data_get_str($service, 'image')); $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
if ($isService) { if ($isService) {
$containerName = "$serviceName-{$resource->uuid}";
if ($serviceName === 'registry') {
$tempServiceName = 'docker-registry';
} else {
$tempServiceName = $serviceName;
}
if (str(data_get($service, 'image'))->contains('glitchtip')) {
$tempServiceName = 'glitchtip';
}
if ($serviceName === 'supabase-kong') {
$tempServiceName = 'supabase';
}
$serviceDefinition = data_get($allServices, $tempServiceName);
$predefinedPort = data_get($serviceDefinition, 'port');
if ($serviceName === 'plausible') {
$predefinedPort = '8000';
}
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -2987,8 +3016,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
if (substr_count(str($key)->value(), '_') === 3) { if (substr_count(str($key)->value(), '_') === 3) {
$fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
$port = $key->afterLast('_')->value();
} else { } else {
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
$port = null;
} }
if ($isApplication) { if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid"); $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
@@ -2999,19 +3030,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
} }
} }
if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) { if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
$path = $value->value(); $path = $value->value();
if ($path !== '/') { if ($path !== '/') {
$fqdn = "$fqdn$path"; $fqdn = "$fqdn$path";
} }
} }
$fqdnWithPort = $fqdn;
if ($port) {
$fqdnWithPort = "$fqdn:$port";
}
if ($isApplication && is_null($resource->fqdn)) { if ($isApplication && is_null($resource->fqdn)) {
data_forget($resource, 'environment_variables'); data_forget($resource, 'environment_variables');
data_forget($resource, 'environment_variables_preview'); data_forget($resource, 'environment_variables_preview');
$resource->fqdn = $fqdn; $resource->fqdn = $fqdnWithPort;
$resource->save(); $resource->save();
} elseif ($isService && is_null($savedService->fqdn)) { } elseif ($isService && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdnWithPort;
$savedService->save(); $savedService->save();
} }
@@ -3040,7 +3076,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
$allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
if ($magicEnvironments->count() > 0) { if ($magicEnvironments->count() > 0) {
foreach ($magicEnvironments as $key => $value) { foreach ($magicEnvironments as $key => $value) {
$key = str($key); $key = str($key);
@@ -3455,6 +3490,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$value = $value->after('?'); $value = $value->after('?');
} }
if ($originalValue->value() === $value->value()) { if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
$parsedKeyValue = replaceVariables($value);
$resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([
'key' => $parsedKeyValue,
$nameOfId => $resource->id,
], [
'is_build_time' => false,
'is_preview' => false,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
continue; continue;
} }
$resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3547,6 +3594,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) { if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) { $environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_'); return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
if (str($value)->isEmpty()) {
if ($resource->environment_variables()->where('key', $key)->exists()) {
$value = $resource->environment_variables()->where('key', $key)->first()->value;
} else {
$value = null;
}
}
return $value;
}); });
} }
$serviceLabels = $labels->merge($defaultLabels); $serviceLabels = $labels->merge($defaultLabels);
@@ -3631,6 +3689,14 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc'); data_forget($service, 'exclude_from_hc');
$volumesParsed = $volumesParsed->map(function ($volume) {
data_forget($volume, 'content');
data_forget($volume, 'is_directory');
data_forget($volume, 'isDirectory');
return $volume;
});
$payload = collect($service)->merge([ $payload = collect($service)->merge([
'container_name' => $containerName, 'container_name' => $containerName,
'restart' => $restart->value(), 'restart' => $restart->value(),
@@ -3661,6 +3727,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$parsedServices->put($serviceName, $payload); $parsedServices->put($serviceName, $payload);
} }
$topLevel->put('services', $parsedServices); $topLevel->put('services', $parsedServices);
$customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
$topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {

View File

@@ -85,7 +85,11 @@
"@php artisan vendor:publish --tag=laravel-assets --ansi --force", "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate" "Illuminate\\Foundation\\ComposerScripts::postUpdate"
], ],
"post-install-cmd": [], "post-install-cmd": [
"cp -r 'hooks/' '.git/hooks/'",
"php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
"php -r \"chmod('.git/hooks/pre-commit', 0777);\""
],
"post-root-package-install": [ "post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
], ],

View File

@@ -1,424 +0,0 @@
<?php
return [
/*
|------------------------------------------------------------------------------------------------------------------
| Enable Clockwork
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or
| disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.
| Unless explicitly enabled, Clockwork only runs on localhost, *.local, *.test and *.wip domains.
|
*/
'enable' => env('CLOCKWORK_ENABLE', null),
/*
|------------------------------------------------------------------------------------------------------------------
| Features
|------------------------------------------------------------------------------------------------------------------
|
| You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
| threshold for database queries).
|
*/
'features' => [
// Cache usage stats and cache queries including results
'cache' => [
'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
// Collect cache queries
'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
// Collect values from cache queries (high performance impact with a very high number of queries)
'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false)
],
// Database usage stats and queries
'database' => [
'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
// Collect database queries (high performance impact with a very high number of queries)
'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
// Collect details of models updates (high performance impact with a lot of model updates)
'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
// Collect details of retrieved models (very high performance impact with a lot of models retrieved)
'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
// Query execution time threshold in milliseconds after which the query will be marked as slow
'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
// Collect only slow database queries
'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
// Detect and report duplicate queries
'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false)
],
// Dispatched events
'events' => [
'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
// Ignored events (framework events are ignored by default)
'ignored_events' => [
// App\Events\UserRegistered::class,
// 'user.registered'
],
],
// Laravel log (you can still log directly to Clockwork with laravel log disabled)
'log' => [
'enabled' => env('CLOCKWORK_LOG_ENABLED', true)
],
// Sent notifications
'notifications' => [
'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
],
// Performance metrics
'performance' => [
// Allow collecting of client metrics. Requires separate clockwork-browser npm package.
'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true)
],
// Dispatched queue jobs
'queue' => [
'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true)
],
// Redis commands
'redis' => [
'enabled' => env('CLOCKWORK_REDIS_ENABLED', true)
],
// Routes list
'routes' => [
'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
// Collect only routes from particular namespaces (only application routes by default)
'only_namespaces' => [ 'App' ]
],
// Rendered views
'views' => [
'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
// Collect views including view data (high performance impact with a high number of views)
'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
// Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
// not support collecting view data)
'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false)
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable web UI
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
| feature. You can also set a custom path for the web UI.
|
*/
'web' => env('CLOCKWORK_WEB', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Enable toolbar
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
| Requires a separate clockwork-browser npm library.
| For installation instructions see https://underground.works/clockwork/#docs-viewing-data
|
*/
'toolbar' => env('CLOCKWORK_TOOLBAR', true),
/*
|------------------------------------------------------------------------------------------------------------------
| HTTP requests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
|
*/
'requests' => [
// With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
// manually pass a "clockwork-profile" cookie or get/post data key.
// Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
// Collect only errors (requests with HTTP 4xx and 5xx responses)
'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
// Response time threshold in milliseconds after which the request will be marked as slow
'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
// Collect only slow requests
'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
// Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
// List of URIs that should not be collected
'except' => [
'/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests
'/_tt/.*', // Laravel Telescope toolbar
'/_debugbar/.*', // Laravel DebugBar requests
],
// List of URIs that should be collected, any other URI will not be collected if not empty
'only' => [
// '/api/.*'
],
// Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Artisan commands collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
| should be collected.
|
*/
'artisan' => [
// Enable or disable collection of executed Artisan commands
'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
// List of commands that should not be collected (built-in commands are not collected by default)
'except' => [
// 'inspire'
],
// List of commands that should be collected, any other command will not be collected if not empty
'only' => [
// 'inspire'
],
// Enable or disable collection of command output
'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
// Enable or disable collection of built-in Laravel commands
'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true)
],
/*
|------------------------------------------------------------------------------------------------------------------
| Queue jobs collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
| be collected.
|
*/
'queue' => [
// Enable or disable collection of executed queue jobs
'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
// List of queue jobs that should not be collected
'except' => [
// App\Jobs\ExpensiveJob::class
],
// List of queue jobs that should be collected, any other queue job will not be collected if not empty
'only' => [
// App\Jobs\BuggyJob::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Tests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
| collected.
|
*/
'tests' => [
// Enable or disable collection of ran tests
'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
// List of tests that should not be collected
'except' => [
// Tests\Unit\ExampleTest::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable data collection when Clockwork is disabled
|------------------------------------------------------------------------------------------------------------------
|
| You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
|
*/
'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
/*
|------------------------------------------------------------------------------------------------------------------
| Metadata storage
|------------------------------------------------------------------------------------------------------------------
|
| Configure how is the metadata collected by Clockwork stored. Three options are available:
| - files - A simple fast storage implementation storing data in one-per-request files.
| - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
| - redis - Stores requests in redis. Requires phpredis.
*/
'storage' => env('CLOCKWORK_STORAGE', 'files'),
// Path where the Clockwork metadata is stored
'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
// Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
// SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
// SQL table name to use, the table is automatically created and updated when needed
'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
// Redis connection, name of redis connection or cluster configured in database.php
'storage_redis' => env('CLOCKWORK_STORAGE_REDIS', 'default'),
// Redis prefix for Clockwork keys ("clockwork" if not set)
'storage_redis_prefix' => env('CLOCKWORK_STORAGE_REDIS_PREFIX', 'clockwork'),
// Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
/*
|------------------------------------------------------------------------------------------------------------------
| Authentication
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can be configured to require authentication before allowing access to the collected data. This might be
| useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
| pre-configured password. You can also pass a class name of a custom implementation.
|
*/
'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
// Password for the simple authentication
'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
/*
|------------------------------------------------------------------------------------------------------------------
| Stack traces collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
| whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
| long stack traces considerably increases metadata size.
|
*/
'stack_traces' => [
// Enable or disable collecting of stack traces
'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
// Limit the number of frames to be collected
'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
// List of vendor names to skip when determining caller, common vendors are automatically added
'skip_vendors' => [
// 'phpunit'
],
// List of namespaces to skip when determining caller
'skip_namespaces' => [
// 'Laravel'
],
// List of class names to skip when determining caller
'skip_classes' => [
// App\CustomLog::class
]
],
/*
|------------------------------------------------------------------------------------------------------------------
| Serialization
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
| of serialization. Serialization has a large effect on the cpu time and memory usage.
|
*/
// Maximum depth of serialized multi-level arrays and objects
'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
// A list of classes that will never be serialized (e.g. a common service container class)
'serialization_blackbox' => [
\Illuminate\Container\Container::class,
\Illuminate\Foundation\Application::class,
\Laravel\Lumen\Application::class
],
/*
|------------------------------------------------------------------------------------------------------------------
| Register helpers
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
| access the Clockwork instance.
|
*/
'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Send headers for AJAX request
|------------------------------------------------------------------------------------------------------------------
|
| When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
| API might require a version number using Accept headers to route the HTTP request to the correct codebase.
|
*/
'headers' => [
// 'Accept' => 'application/vnd.com.whatever.v1+json',
],
/*
|------------------------------------------------------------------------------------------------------------------
| Server timing
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
| in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
| Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
| will disable the feature.
|
*/
'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10)
];

View File

@@ -6,9 +6,8 @@ return [
'contact' => 'https://coolify.io/docs/contact', 'contact' => 'https://coolify.io/docs/contact',
], ],
'ssh' => [ 'ssh' => [
// Using MUX 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true), true), 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),
'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1h'),
'connection_timeout' => 10, 'connection_timeout' => 10,
'server_interval' => 20, 'server_interval' => 20,
'command_timeout' => 7200, 'command_timeout' => 7200,

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.341', 'release' => '4.0.0-beta.346',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.341'; return '4.0.0-beta.346';

View File

@@ -2,8 +2,8 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class UpdateServerSettingsDefaultTimezone extends Migration class UpdateServerSettingsDefaultTimezone extends Migration
{ {

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
class EncryptExistingPrivateKeys extends Migration
{
public function up()
{
try {
DB::table('private_keys')->chunkById(100, function ($keys) {
foreach ($keys as $key) {
DB::table('private_keys')
->where('id', $key->id)
->update(['private_key' => Crypt::encryptString($key->private_key)]);
}
});
} catch (\Exception $e) {
echo 'Encrypting private keys failed.';
echo $e->getMessage();
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
use App\Models\PrivateKey;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddSshKeyFingerprintToPrivateKeysTable extends Migration
{
public function up()
{
Schema::table('private_keys', function (Blueprint $table) {
$table->string('fingerprint')->after('private_key')->nullable();
});
try {
DB::table('private_keys')->chunkById(100, function ($keys) {
foreach ($keys as $key) {
$fingerprint = PrivateKey::generateFingerprint($key->private_key);
if ($fingerprint) {
$key->fingerprint = $fingerprint;
$key->save();
}
}
});
} catch (\Exception $e) {
echo 'Generating fingerprints failed.';
echo $e->getMessage();
}
}
public function down()
{
Schema::table('private_keys', function (Blueprint $table) {
$table->dropColumn('fingerprint');
});
}
}

View File

@@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder
UserSeeder::class, UserSeeder::class,
TeamSeeder::class, TeamSeeder::class,
PrivateKeySeeder::class, PrivateKeySeeder::class,
PopulateSshKeysDirectorySeeder::class,
ServerSeeder::class, ServerSeeder::class,
ServerSettingSeeder::class, ServerSettingSeeder::class,
ProjectSeeder::class, ProjectSeeder::class,

View File

@@ -31,7 +31,7 @@ class GithubAppSeeder extends Seeder
'client_id' => 'Iv1.220e564d2b0abd8c', 'client_id' => 'Iv1.220e564d2b0abd8c',
'client_secret' => '116d1d80289f378410dd70ab4e4b81dd8d2c52b6', 'client_secret' => '116d1d80289f378410dd70ab4e4b81dd8d2c52b6',
'webhook_secret' => '326a47b49054f03288f800d81247ec9414d0abf3', 'webhook_secret' => '326a47b49054f03288f800d81247ec9414d0abf3',
'private_key_id' => 1, 'private_key_id' => 2,
'team_id' => 0, 'team_id' => 0,
]); ]);
} }

View File

@@ -20,19 +20,5 @@ class GitlabAppSeeder extends Seeder
'is_public' => true, 'is_public' => true,
'team_id' => 0, 'team_id' => 0,
]); ]);
GitlabApp::create([
'id' => 2,
'name' => 'coolify-laravel-development-private-gitlab',
'api_url' => 'https://gitlab.com/api/v4',
'html_url' => 'https://gitlab.com',
'app_id' => 1234,
'app_secret' => '1234',
'oauth_id' => 1234,
'deploy_key_id' => '1234',
'public_key' => 'dfjasiourj',
'webhook_token' => '4u3928u4y392',
'private_key_id' => 2,
'team_id' => 0,
]);
} }
} }

View File

@@ -0,0 +1,40 @@
<?php
namespace Database\Seeders;
use App\Models\PrivateKey;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
class PopulateSshKeysDirectorySeeder extends Seeder
{
public function run()
{
try {
Storage::disk('ssh-keys')->deleteDirectory('');
Storage::disk('ssh-keys')->makeDirectory('');
Storage::disk('ssh-mux')->deleteDirectory('');
Storage::disk('ssh-mux')->makeDirectory('');
PrivateKey::chunk(100, function ($keys) {
foreach ($keys as $key) {
echo 'Storing key: '.$key->name."\n";
$key->storeInFileSystem();
}
});
if (isDev()) {
$user = env('PUID').':'.env('PGID');
Process::run("chown -R $user ".storage_path('app/ssh/keys'));
Process::run("chown -R $user ".storage_path('app/ssh/mux'));
} else {
Process::run('chown -R 9999:root '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:root '.storage_path('app/ssh/mux'));
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
ray($e->getMessage());
}
}
}

View File

@@ -13,9 +13,8 @@ class PrivateKeySeeder extends Seeder
public function run(): void public function run(): void
{ {
PrivateKey::create([ PrivateKey::create([
'id' => 0,
'team_id' => 0, 'team_id' => 0,
'name' => 'Testing-host', 'name' => 'Testing Host Key',
'description' => 'This is a test docker container', 'description' => 'This is a test docker container',
'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -25,10 +24,9 @@ AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----
', ',
]); ]);
PrivateKey::create([ PrivateKey::create([
'id' => 1,
'team_id' => 0, 'team_id' => 0,
'name' => 'development-github-app', 'name' => 'development-github-app',
'description' => 'This is the key for using the development GitHub app', 'description' => 'This is the key for using the development GitHub app',
@@ -61,12 +59,5 @@ a1C8EDKapCw5hAhizEFOUQKOygL8Ipn+tmEUkORYdZ8Q8cWFCv9nIw==
-----END RSA PRIVATE KEY-----', -----END RSA PRIVATE KEY-----',
'is_git_related' => true, 'is_git_related' => true,
]); ]);
PrivateKey::create([
'id' => 2,
'team_id' => 0,
'name' => 'development-gitlab-app',
'description' => 'This is the key for using the development Gitlab app',
'private_key' => 'asdf',
]);
} }
} }

View File

@@ -64,32 +64,8 @@ class ProductionSeeder extends Seeder
'team_id' => 0, 'team_id' => 0,
]); ]);
} }
if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) {
echo "Checking localhost key.\n";
// Save SSH Keys for the Coolify Host
$coolify_key_name = 'id.root@host.docker.internal';
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
if ($coolify_key) {
PrivateKey::updateOrCreate(
[
'id' => 0,
'team_id' => 0,
],
[
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'private_key' => $coolify_key,
]
);
} else {
echo "No SSH key found for the Coolify host machine (localhost).\n";
echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n";
echo "Then try to install again.\n";
exit(1);
}
// Add Coolify host (localhost) as Server if it doesn't exist // Add Coolify host (localhost) as Server if it doesn't exist
if (! isCloud()) {
if (Server::find(0) == null) { if (Server::find(0) == null) {
$server_details = [ $server_details = [
'id' => 0, 'id' => 0,
@@ -123,6 +99,50 @@ class ProductionSeeder extends Seeder
]); ]);
} }
} }
if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) {
echo "Checking localhost key.\n";
$coolify_key_name = '@host.docker.internal';
$ssh_keys_directory = Storage::disk('ssh-keys')->files();
$coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name));
$found = PrivateKey::find(0);
if ($found) {
echo 'Private Key found in database.\n';
if ($coolify_key) {
echo "SSH key found for the Coolify host machine (localhost).\n";
}
} else {
if ($coolify_key) {
$user = str($coolify_key)->before('@')->after('id.');
$coolify_key = Storage::disk('ssh-keys')->get($coolify_key);
PrivateKey::create([
'id' => 0,
'team_id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'private_key' => $coolify_key,
]);
$server->update(['user' => $user]);
echo "SSH key found for the Coolify host machine (localhost).\n";
} else {
PrivateKey::create(
[
'id' => 0,
'team_id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'private_key' => 'Paste here you private key!!',
]
);
echo "No SSH key found for the Coolify host machine (localhost).\n";
echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n";
echo "Your localhost connection won't work until then.";
}
}
}
if (config('coolify.is_windows_docker_desktop')) { if (config('coolify.is_windows_docker_desktop')) {
PrivateKey::updateOrCreate( PrivateKey::updateOrCreate(
[ [
@@ -179,8 +199,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
get_public_ips(); get_public_ips();
$oauth_settings_seeder = new OauthSettingSeeder; $this->call(OauthSettingSeeder::class);
$oauth_settings_seeder->run(); $this->call(PopulateSshKeysDirectorySeeder::class);
} }
} }

View File

@@ -2,6 +2,8 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@@ -15,7 +17,11 @@ class ServerSeeder extends Seeder
'description' => 'This is a test docker container in development mode', 'description' => 'This is a test docker container in development mode',
'ip' => 'coolify-testing-host', 'ip' => 'coolify-testing-host',
'team_id' => 0, 'team_id' => 0,
'private_key_id' => 0, 'private_key_id' => 1,
'proxy' => [
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
],
]); ]);
} }
} }

View File

@@ -68,7 +68,11 @@ wss.on('connection', (ws) => {
const messageHandlers = { const messageHandlers = {
message: (session, data) => session.ptyProcess.write(data), message: (session, data) => session.ptyProcess.write(data),
resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows), resize: (session, { cols, rows }) => {
cols = cols > 0 ? cols : 80;
rows = rows > 0 ? rows : 30;
session.ptyProcess.resize(cols, rows)
},
pause: (session) => session.ptyProcess.pause(), pause: (session) => session.ptyProcess.pause(),
resume: (session) => session.ptyProcess.resume(), resume: (session) => session.ptyProcess.resume(),
checkActive: (session, data) => { checkActive: (session, data) => {
@@ -140,6 +144,7 @@ async function handleCommand(ws, command, userId) {
ptyProcess.onData((data) => ws.send(data)); ptyProcess.onData((data) => ws.send(data));
// when parent closes
ptyProcess.onExit(({ exitCode, signal }) => { ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`); console.error(`Process exited with code ${exitCode} and signal ${signal}`);
userSession.isActive = false; userSession.isActive = false;

Some files were not shown because too many files have changed in this diff Show More