diff --git a/.env.development.example b/.env.development.example index 89d3af930..d4daed4f7 100644 --- a/.env.development.example +++ b/.env.development.example @@ -19,11 +19,7 @@ DB_PORT=5432 # Set to true to enable Ray RAY_ENABLED=false # Set custom ray port -RAY_PORT= - -# Clockwork Configuration (remove?) -CLOCKWORK_ENABLED=false -CLOCKWORK_QUEUE_COLLECT=true +# RAY_PORT= # Enable Laravel Telescope for debugging TELESCOPE_ENABLED=false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3ded74ce3..5afe00a30 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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 # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 590360ddb..4a3e0e538 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,15 +6,19 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis ## Table of Contents -1. [Setup Development Environment](#1-setup-development-environment) -2. [Verify Installation](#2-verify-installation-optional) -3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) -4. [Set up Environment Variables](#4-set-up-environment-variables) -5. [Start Coolify](#5-start-coolify) -6. [Start Development](#6-start-development) -7. [Development Notes](#7-development-notes) -8. [Create a Pull Request](#8-create-a-pull-request) -9. [Additional Contribution Guidelines](#additional-contribution-guidelines) +- [Contributing to Coolify](#contributing-to-coolify) + - [Table of Contents](#table-of-contents) + - [1. Setup Development Environment](#1-setup-development-environment) + - [2. Verify Installation (Optional)](#2-verify-installation-optional) + - [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository) + - [4. Set up Environment Variables](#4-set-up-environment-variables) + - [5. Start Coolify](#5-start-coolify) + - [6. Start Development](#6-start-development) + - [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 diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 3ed435049..61005845b 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -2,8 +2,8 @@ namespace App\Actions\Application; -use App\Models\Application; use App\Actions\Server\CleanupDocker; +use App\Models\Application; use Lorisleiva\Actions\Concerns\AsAction; class StopApplication @@ -14,13 +14,14 @@ class StopApplication { try { $server = $application->destination->server; - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping application: ' . $application->name); + ray('Stopping application: '.$application->name); if ($server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $server); + return; } @@ -32,10 +33,11 @@ class StopApplication } if ($dockerCleanup) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } } catch (\Exception $e) { ray($e->getMessage()); + return $e->getMessage(); } } diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index a5dfa9226..c691f52c0 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -4,13 +4,13 @@ namespace App\Actions\CoolifyTask; use App\Enums\ActivityTypes; use App\Enums\ProcessStatus; +use App\Helpers\SshMultiplexingHelper; use App\Jobs\ApplicationDeploymentJob; use App\Models\Server; use Illuminate\Process\ProcessResult; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; use Spatie\Activitylog\Models\Activity; -use App\Helpers\SshMultiplexingHelper; class RunRemoteProcess { diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 621834df0..352c6a59f 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -23,7 +23,7 @@ class StartDragonfly $startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -75,7 +75,7 @@ class StartDragonfly ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -118,10 +118,10 @@ class StartDragonfly $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -152,7 +152,7 @@ class StartDragonfly $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}"); } diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 9290efc7c..a11452a68 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -24,7 +24,7 @@ class StartKeydb $startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -74,7 +74,7 @@ class StartKeydb ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -94,10 +94,10 @@ class StartKeydb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) { + if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/keydb.conf', + 'source' => $this->configuration_dir.'/keydb.conf', 'target' => '/etc/keydb/keydb.conf', 'read_only' => true, ]; @@ -125,10 +125,10 @@ class StartKeydb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -159,7 +159,7 @@ class StartKeydb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}"); } diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index f37a5e361..a5630f734 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -21,7 +21,7 @@ class StartMariadb $this->database = $database; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -69,7 +69,7 @@ class StartMariadb ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -89,10 +89,10 @@ class StartMariadb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) { + if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/custom-config.cnf', + 'source' => $this->configuration_dir.'/custom-config.cnf', 'target' => '/etc/mysql/conf.d/custom-config.cnf', 'read_only' => true, ]; @@ -120,10 +120,10 @@ class StartMariadb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -154,18 +154,18 @@ class StartMariadb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) { $environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) { $environment_variables->push("MARIADB_USER={$this->database->mariadb_user}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}"); } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 42fc8f348..5bff194d5 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -23,7 +23,7 @@ class StartMongodb $startCommand = 'mongod'; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -77,7 +77,7 @@ class StartMongodb ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -97,19 +97,19 @@ class StartMongodb if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) { + if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/mongod.conf', + 'source' => $this->configuration_dir.'/mongod.conf', 'target' => '/etc/mongo/mongod.conf', 'read_only' => true, ]; - $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; + $docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf'; } $this->add_default_database(); $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d', + 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d', 'target' => '/docker-entrypoint-initdb.d', 'read_only' => true, ]; @@ -136,10 +136,10 @@ class StartMongodb $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -170,15 +170,15 @@ class StartMongodb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}"); } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 2043342fe..cc4203580 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -21,7 +21,7 @@ class StartMysql $this->database = $database; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -69,7 +69,7 @@ class StartMysql ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -89,10 +89,10 @@ class StartMysql if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) { + if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/custom-config.cnf', + 'source' => $this->configuration_dir.'/custom-config.cnf', 'target' => '/etc/mysql/conf.d/custom-config.cnf', 'read_only' => true, ]; @@ -120,10 +120,10 @@ class StartMysql $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -154,18 +154,18 @@ class StartMysql $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) { $environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) { $environment_variables->push("MYSQL_USER={$this->database->mysql_user}"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}"); } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index bc37fd5cf..2a8e5476c 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -37,7 +37,6 @@ class StartPostgresql $this->generate_init_scripts(); $this->add_custom_conf(); - $docker_compose = [ 'services' => [ $container_name => [ diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index b837414d6..eeddab924 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -24,7 +24,7 @@ class StartRedis $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; $container_name = $this->database->uuid; - $this->configuration_dir = database_configuration_dir() . '/' . $container_name; + $this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->commands = [ "echo 'Starting {$database->name}.'", @@ -78,7 +78,7 @@ class StartRedis ], ], ]; - if (!is_null($this->database->limits_cpuset)) { + if (! is_null($this->database->limits_cpuset)) { data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { @@ -98,10 +98,10 @@ class StartRedis if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) { + if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) { $docker_compose['services'][$container_name]['volumes'][] = [ 'type' => 'bind', - 'source' => $this->configuration_dir . '/redis.conf', + 'source' => $this->configuration_dir.'/redis.conf', 'target' => '/usr/local/etc/redis/redis.conf', 'read_only' => true, ]; @@ -130,10 +130,10 @@ class StartRedis $local_persistent_volumes = []; foreach ($this->database->persistentStorages as $persistentStorage) { if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) { - $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path; } else { $volume_name = $persistentStorage->name; - $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; + $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; } } @@ -164,7 +164,7 @@ class StartRedis $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); } diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 0f074bea9..e4cea7cee 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -28,7 +28,7 @@ class StopDatabase $this->stopContainer($database, $database->uuid, 300); if (! $isDeleteOperation) { if ($dockerCleanup) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index fdaa88ebf..0779da31d 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -543,7 +543,7 @@ class GetContainersStatus } } } - $exitedServices = $exitedServices->unique('id'); + $exitedServices = $exitedServices->unique('uuid'); foreach ($exitedServices as $exitedService) { if (str($exitedService->status)->startsWith('exited')) { continue; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 991c94b11..f025e5661 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -47,7 +47,8 @@ class StartProxy "echo 'Pulling docker image.'", 'docker compose pull', "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.'", 'docker compose up -d --remove-orphans', "echo 'Proxy started successfully.'", diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index 3946afe95..0d36e8863 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Events\CloudflareTunnelConfigured; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -40,12 +41,17 @@ class ConfigureCloudflared instant_remote_process($commands, $server); } catch (\Throwable $e) { ray($e); + $server->settings->is_cloudflare_tunnel = false; + $server->settings->save(); throw $e; } finally { + CloudflareTunnelConfigured::dispatch($server->team_id); + $commands = collect([ 'rm -fr /tmp/cloudflared', ]); instant_remote_process($commands, $server); + } } } diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 93c79383e..f28e5490e 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -2,8 +2,8 @@ namespace App\Actions\Service; -use App\Models\Service; use App\Actions\Server\CleanupDocker; +use App\Models\Service; use Lorisleiva\Actions\Concerns\AsAction; class DeleteService @@ -36,7 +36,7 @@ class DeleteService } // Execute volume deletion first, this must be done first otherwise volumes will not be deleted. - if (!empty($commands)) { + if (! empty($commands)) { foreach ($commands as $command) { $result = instant_remote_process([$command], $server, false); if ($result !== 0) { @@ -70,7 +70,7 @@ class DeleteService $service->forceDelete(); if ($dockerCleanup) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index d69898a58..5c7bbc2aa 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -2,8 +2,8 @@ namespace App\Actions\Service; -use App\Models\Service; use App\Actions\Server\CleanupDocker; +use App\Models\Service; use Lorisleiva\Actions\Concerns\AsAction; class StopService @@ -14,21 +14,22 @@ class StopService { try { $server = $service->destination->server; - if (!$server->isFunctional()) { + if (! $server->isFunctional()) { return 'Server is not functional'; } $containersToStop = $service->getContainersToStop(); $service->stopContainers($containersToStop, $server); - if (!$isDeleteOperation) { + if (! $isDeleteOperation) { $service->delete_connected_networks($service->uuid); if ($dockerCleanup) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } } } catch (\Exception $e) { ray($e->getMessage()); + return $e->getMessage(); } } diff --git a/app/Console/Commands/CleanupQueue.php b/app/Console/Commands/CleanupQueue.php deleted file mode 100644 index fd2b637ac..000000000 --- a/app/Console/Commands/CleanupQueue.php +++ /dev/null @@ -1,24 +0,0 @@ -keys('*:laravel*'); - foreach ($keys as $key) { - $keyWithoutPrefix = str_replace($prefix, '', $key); - Redis::connection()->del($keyWithoutPrefix); - } - } -} diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php new file mode 100644 index 000000000..ed0740d34 --- /dev/null +++ b/app/Console/Commands/CleanupRedis.php @@ -0,0 +1,31 @@ +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); + }); + + } +} diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 68beb448a..dfd09d4b7 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -2,10 +2,12 @@ namespace App\Console\Commands; +use App\Jobs\CleanupHelperContainersJob; use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; +use App\Models\Server; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command 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 { $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 7bfd1a14f..2f5d36140 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -5,7 +5,6 @@ namespace App\Console\Commands; use App\Actions\Server\StopSentinel; use App\Enums\ActivityTypes; use App\Enums\ApplicationDeploymentStatus; -use App\Jobs\CleanupHelperContainersJob; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; use App\Models\InstanceSettings; @@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http; 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'; @@ -26,9 +25,63 @@ class Init extends Command 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->alive(); - get_public_ips(); + if (isCloud()) { + + } else { + $this->send_alive_signal(); + 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'), '<=')) { foreach ($this->servers as $server) { 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() @@ -108,33 +105,28 @@ class Init extends Command private function cleanup_unnecessary_dynamic_proxy_configuration() { - if (isCloud()) { - foreach ($this->servers as $server) { - try { - if (! $server->isFunctional()) { - continue; - } - if ($server->id === 0) { - continue; - } - $file = $server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $server, false); - } catch (\Throwable $e) { - echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; + foreach ($this->servers as $server) { + try { + if (! $server->isFunctional()) { + continue; } + if ($server->id === 0) { + continue; + } + $file = $server->proxyPath().'/dynamic/coolify.yaml'; + return instant_remote_process([ + "rm -f $file", + ], $server, false); + } catch (\Throwable $e) { + echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; } + } } private function cleanup_unused_network_from_coolify_proxy() { - if (isCloud()) { - return; - } foreach ($this->servers as $server) { if (! $server->isFunctional()) { continue; @@ -175,39 +167,32 @@ class Init extends Command private function restore_coolify_db_backup() { - try { - $database = StandalonePostgresql::withTrashed()->find(0); - if ($database && $database->trashed()) { - echo "Restoring coolify db backup\n"; - $database->restore(); - $scheduledBackup = ScheduledDatabaseBackup::find(0); - if (! $scheduledBackup) { - ScheduledDatabaseBackup::create([ - 'id' => 0, - 'enabled' => true, - 'save_s3' => false, - 'frequency' => '0 0 * * *', - 'database_id' => $database->id, - 'database_type' => 'App\Models\StandalonePostgresql', - 'team_id' => 0, - ]); + if (version_compare('4.0.0-beta.179', config('version'), '<=')) { + try { + $database = StandalonePostgresql::withTrashed()->find(0); + if ($database && $database->trashed()) { + echo "Restoring coolify db backup\n"; + $database->restore(); + $scheduledBackup = ScheduledDatabaseBackup::find(0); + if (! $scheduledBackup) { + ScheduledDatabaseBackup::create([ + 'id' => 0, + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 0 * * *', + 'database_id' => $database->id, + 'database_type' => 'App\Models\StandalonePostgresql', + 'team_id' => 0, + ]); + } } - } - } catch (\Throwable $e) { - 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); + } catch (\Throwable $e) { + echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; } } } - private function alive() + private function send_alive_signal() { $id = config('app.id'); $version = config('version'); @@ -225,23 +210,7 @@ class Init extends Command 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() { // Cleanup any failed deployments @@ -263,11 +232,13 @@ class Init extends Command private function replace_slash_in_environment_name() { - $environments = Environment::all(); - foreach ($environments as $environment) { - if (str_contains($environment->name, '/')) { - $environment->name = str_replace('/', '-', $environment->name); - $environment->save(); + if (version_compare('4.0.0-beta.298', config('version'), '<=')) { + $environments = Environment::all(); + foreach ($environments as $environment) { + if (str_contains($environment->name, '/')) { + $environment->name = str_replace('/', '-', $environment->name); + $environment->save(); + } } } } diff --git a/app/Events/CloudflareTunnelConfigured.php b/app/Events/CloudflareTunnelConfigured.php new file mode 100644 index 000000000..3d7076d0d --- /dev/null +++ b/app/Events/CloudflareTunnelConfigured.php @@ -0,0 +1,34 @@ +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}"), + ]; + } +} diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index 71be77506..b0a832605 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -24,28 +24,24 @@ class SshMultiplexingHelper public static function ensureMultiplexedConnection(Server $server) { if (! self::isMultiplexingEnabled()) { - // ray('SSH Multiplexing: DISABLED')->red(); return; } - // ray('SSH Multiplexing: ENABLED')->green(); - // ray('Ensuring multiplexed connection for server:', $server); - $sshConfig = self::serverSshConfiguration($server); $muxSocket = $sshConfig['muxFilename']; $sshKeyLocation = $sshConfig['sshKeyLocation']; self::validateSshKey($sshKeyLocation); - $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; + $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) { - // ray('SSH Multiplexing: Existing connection check failed or not found')->orange(); - // ray('Establishing new connection'); self::establishNewMultiplexedConnection($server); - } else { - // ray('SSH Multiplexing: Existing connection is valid')->green(); } } @@ -55,37 +51,24 @@ class SshMultiplexingHelper $sshKeyLocation = $sshConfig['sshKeyLocation']; $muxSocket = $sshConfig['muxFilename']; - // ray('Establishing new multiplexed connection')->blue(); - // ray('SSH Key Location:', $sshKeyLocation); - // ray('Mux Socket:', $muxSocket); - $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} " - .self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval) - ."{$server->user}@{$server->ip}"; + $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; - // ray('Establish Command:', $establishCommand); + 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); - // ray('Establish Process Exit Code:', $establishProcess->exitCode()); - // ray('Establish Process Output:', $establishProcess->output()); - // ray('Establish Process Error Output:', $establishProcess->errorOutput()); - if ($establishProcess->exitCode() !== 0) { - // ray('Failed to establish multiplexed connection')->red(); throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput()); } - - // ray('Successfully established multiplexed connection')->green(); - - // Check if the mux socket file was created - if (! file_exists($muxSocket)) { - // ray('Mux socket file not found after connection establishment')->orange(); - } } public static function removeMuxFile(Server $server) @@ -93,20 +76,12 @@ class SshMultiplexingHelper $sshConfig = self::serverSshConfiguration($server); $muxSocket = $sshConfig['muxFilename']; - $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; - $process = Process::run($closeCommand); - - // ray('Closing multiplexed connection')->blue(); - // ray('Close command:', $closeCommand); - // ray('Close process exit code:', $process->exitCode()); - // ray('Close process output:', $process->output()); - // ray('Close process error output:', $process->errorOutput()); - - if ($process->exitCode() !== 0) { - // ray('Failed to close multiplexed connection')->orange(); - } else { - // ray('Successfully closed multiplexed connection')->green(); + $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) @@ -116,16 +91,18 @@ class SshMultiplexingHelper $muxSocket = $sshConfig['muxFilename']; $timeout = config('constants.ssh.command_timeout'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); $scp_command = "timeout $timeout scp "; if (self::isMultiplexingEnabled()) { - $muxPersistTime = config('constants.ssh.mux_persist_time'); $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; self::ensureMultiplexedConnection($server); } - self::addCloudflareProxyCommand($scp_command, $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}"; @@ -144,16 +121,18 @@ class SshMultiplexingHelper $muxSocket = $sshConfig['muxFilename']; $timeout = config('constants.ssh.command_timeout'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); $ssh_command = "timeout $timeout ssh "; if (self::isMultiplexingEnabled()) { - $muxPersistTime = config('constants.ssh.mux_persist_time'); $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; self::ensureMultiplexedConnection($server); } - self::addCloudflareProxyCommand($ssh_command, $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')); @@ -183,13 +162,6 @@ class SshMultiplexingHelper } } - private static function addCloudflareProxyCommand(string &$command, Server $server): void - { - if (data_get($server, 'settings.is_cloudflare_tunnel')) { - $command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; - } - } - private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string { $options = "-i {$sshKeyLocation} " diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 81b173011..48e126f27 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -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) { $teamId = getTeamIdFromToken(); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 96313d285..df166c1cd 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -27,9 +27,9 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Sleep; use Illuminate\Support\Str; -use Illuminate\Support\Facades\Process; use RuntimeException; use Symfony\Component\Yaml\Yaml; use Throwable; @@ -2227,7 +2227,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ); } } catch (\Exception $error) { - $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: " . $error->getMessage(), 'stderr'); + $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); } $this->remove_container($containerName); @@ -2250,7 +2250,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); if ($this->pull_request_id === 0) { $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id; + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; }); } $containers->each(function ($container) { diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index ddc264839..747a9a98a 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -9,8 +9,8 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Http; class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue { diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php index acb28c2f4..6d49bee4b 100644 --- a/app/Jobs/CleanupStaleMultiplexedConnections.php +++ b/app/Jobs/CleanupStaleMultiplexedConnections.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Models\Server; +use Carbon\Carbon; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -10,7 +11,6 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; -use Carbon\Carbon; class CleanupStaleMultiplexedConnections implements ShouldQueue { @@ -30,8 +30,9 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue $serverUuid = $this->extractServerUuidFromMuxFile($muxFile); $server = Server::where('uuid', $serverUuid)->first(); - if (!$server) { + if (! $server) { $this->removeMultiplexFile($muxFile); + continue; } @@ -60,7 +61,7 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue foreach ($muxFiles as $muxFile) { $serverUuid = $this->extractServerUuidFromMuxFile($muxFile); - if (!in_array($serverUuid, $existingServerUuids)) { + if (! in_array($serverUuid, $existingServerUuids)) { $this->removeMultiplexFile($muxFile); } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index e919855d5..22ae06ebd 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue @@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue 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() { GetContainersStatus::run($this->server); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 3bd13564b..947dc4317 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -23,7 +23,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; 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 { try { diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php deleted file mode 100644 index d3b0e99cf..000000000 --- a/app/Jobs/DatabaseBackupStatusJob.php +++ /dev/null @@ -1,62 +0,0 @@ -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)); - // } - } -} diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 37300593e..ac34d064e 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -80,7 +80,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue || $this->resource instanceof StandaloneClickhouse; $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); if (($this->dockerCleanup || $isDatabase) && $server) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } if ($this->deleteConnectedNetworks && ! $isDatabase) { @@ -92,7 +92,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue } finally { $this->resource->forceDelete(); if ($this->dockerCleanup) { - CleanupDocker::run($server, true); + CleanupDocker::dispatch($server, true); } Artisan::queue('cleanup:stucked-resources'); } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index f95cd2920..a961fae4c 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; @@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue 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 { try { diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index 3188d35d6..9c0a2b55b 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; @@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue 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() { try { diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php index f8c769382..32f84e6d5 100644 --- a/app/Jobs/PullSentinelImageJob.php +++ b/app/Jobs/PullSentinelImageJob.php @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue @@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue 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 handle(): void diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 93d5fca70..6850ae98a 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ScheduledTaskJob implements ShouldQueue @@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue { if ($this->resource instanceof Application) { $timezone = $this->resource->destination->server->settings->server_timezone; + return $timezone; } elseif ($this->resource instanceof Service) { $timezone = $this->resource->server->settings->server_timezone; + return $timezone; } + return 'UTC'; } - public function middleware(): array - { - return [new WithoutOverlapping($this->task->id)]; - } - - public function uniqueId(): int - { - return $this->task->id; - } - public function handle(): void { @@ -94,12 +86,12 @@ class ScheduledTaskJob implements ShouldQueue } elseif ($this->resource->type() == 'service') { $this->resource->applications()->get()->each(function ($application) { if (str(data_get($application, 'status'))->contains('running')) { - $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'); + $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid'); } }); $this->resource->databases()->get()->each(function ($database) { if (str(data_get($database, 'status'))->contains('running')) { - $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'); + $this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid'); } }); } @@ -112,8 +104,8 @@ class ScheduledTaskJob implements ShouldQueue } foreach ($this->containers as $containerName) { - if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) { - $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'"; + if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) { + $cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'"; $exec = "docker exec {$containerName} {$cmd}"; $this->task_output = instant_remote_process([$exec], $this->server, true); $this->task_log->update([ diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index be5ec20f9..7fde44f49 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Arr; @@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $tries = 3; + public $tries = 1; public $timeout = 60; @@ -45,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue 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() { try { diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 24292025b..b2c816f5d 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; +use Illuminate\Queue\Middleware\; use Illuminate\Queue\SerializesModels; class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue @@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue 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() { try { diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index ac9182eca..fcc33c859 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue @@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue 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() { if (! $this->server->isServerReady($this->tries)) { diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 21dcda50f..52d4674ee 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -247,7 +247,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->createdPrivateKey = $privateKey; $this->currentState = 'create-server'; } catch (\Exception $e) { - $this->addError('privateKey', 'Failed to save private key: ' . $e->getMessage()); + $this->addError('privateKey', 'Failed to save private key: '.$e->getMessage()); } } diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php index 5b3115dea..87ae83931 100644 --- a/app/Livewire/Destination/Form.php +++ b/app/Livewire/Destination/Form.php @@ -1,6 +1,7 @@ password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index f2968f6d9..3de895f8c 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; -use Illuminate\Support\Collection; use Livewire\Component; class Show extends Component diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 397e159ad..b1ba035dc 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -5,12 +5,12 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; use App\Models\Application; use App\Models\ApplicationPreview; +use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; use Livewire\Component; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; -use Illuminate\Process\InvokedProcess; -use Illuminate\Support\Facades\Process; class Previews extends Component { @@ -195,7 +195,7 @@ class Previews extends Component $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); $this->stopContainers($containers, $server, $timeout); } - + GetContainersStatus::run($server); $this->application->refresh(); $this->dispatch('containerStatusUpdated'); @@ -242,7 +242,7 @@ class Previews extends Component $startTime = time(); while (count($processes) > 0) { $finishedProcesses = array_filter($processes, function ($process) { - return !$process->running(); + return ! $process->running(); }); foreach (array_keys($finishedProcesses) as $containerName) { unset($processes[$containerName]); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 9efb2d9fc..ec87beead 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -3,10 +3,10 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; use Spatie\Url\Url; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Auth; class BackupEdit extends Component { @@ -15,7 +15,9 @@ class BackupEdit extends Component 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; @@ -54,8 +56,9 @@ class BackupEdit extends Component public function delete($password) { - if (!Hash::check($password, Auth::user()->password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } @@ -74,7 +77,7 @@ class BackupEdit extends Component $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); $url = $url->withFragment('backups'); - $url = $url->getPath() . "#{$url->getFragment()}"; + $url = $url->getPath()."#{$url->getFragment()}"; return redirect($url); } else { @@ -135,11 +138,11 @@ class BackupEdit extends Component } else { $server = $this->backup->database->destination->server; } - - if (!$backupFolder) { + + if (! $backupFolder) { $backupFolder = dirname($execution->filename); } - + delete_backup_locally($execution->filename, $server); $execution->delete(); } @@ -162,13 +165,13 @@ class BackupEdit extends Component 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); } @@ -182,7 +185,7 @@ class BackupEdit extends Component ['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.'] - ] + ], ]); } } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index e16397652..c8c33a022 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -3,19 +3,23 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; -use Livewire\Component; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Attributes\On; +use Livewire\Component; class BackupExecutions extends Component { public ?ScheduledDatabaseBackup $backup = null; + public $database; + public $executions = []; + public $setDeletableBackup; public $delete_backup_s3 = true; + public $delete_backup_sftp = true; public function getListeners() @@ -40,14 +44,16 @@ class BackupExecutions extends Component #[On('deleteBackup')] public function deleteBackup($executionId, $password) { - if (!Hash::check($password, Auth::user()->password)) { + 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)) { $this->dispatch('error', 'Backup execution not found.'); + return; } @@ -103,16 +109,18 @@ class BackupExecutions extends Component return $server; } } + return null; } public function getServerTimezone() { $server = $this->server(); - if (!$server) { + if (! $server) { return 'UTC'; } $serverTimezone = $server->settings->server_timezone; + return $serverTimezone; } @@ -125,6 +133,7 @@ class BackupExecutions extends Component } catch (\Exception $e) { $dateObj->setTimezone(new \DateTimeZone('UTC')); } + return $dateObj->format('Y-m-d H:i:s T'); } @@ -134,7 +143,7 @@ class BackupExecutions extends Component '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'], - ] + ], ]); } } diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index a6e2a1320..7a6446815 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -56,7 +56,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -73,14 +73,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -95,7 +95,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 00e0ff09f..394ba6c9a 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -54,7 +54,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -88,14 +88,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -110,7 +110,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 320feeac7..f976e1edd 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -57,7 +57,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -94,14 +94,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -116,7 +116,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 70545910c..12d4882f3 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -63,7 +63,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -100,14 +100,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -122,7 +122,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index d23b66c00..ac40e7dfa 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -61,7 +61,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -101,14 +101,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -123,7 +123,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 29a9cbae2..7d5270ddf 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -62,7 +62,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -99,14 +99,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -121,7 +121,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index fd2f9834f..72fd95de8 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -57,7 +57,7 @@ class General extends Component public function instantSaveAdvanced() { try { - if (!$this->server->isLogDrainEnabled()) { + if (! $this->server->isLogDrainEnabled()) { $this->database->is_log_drain_enabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); @@ -88,14 +88,14 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && !$this->database->public_port) { + if ($this->database->is_public && ! $this->database->public_port) { $this->dispatch('error', 'Public port is required.'); $this->database->is_public = false; return; } if ($this->database->is_public) { - if (!str($this->database->status)->startsWith('running')) { + if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); $this->database->is_public = false; @@ -110,7 +110,7 @@ class General extends Component $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { - $this->database->is_public = !$this->database->is_public; + $this->database->is_public = ! $this->database->is_public; return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 9fac897bf..215019112 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -14,9 +14,9 @@ use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use Livewire\Component; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Component; class FileStorage extends Component { @@ -87,8 +87,9 @@ class FileStorage extends Component public function delete($password) { - if (!Hash::check($password, Auth::user()->password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } @@ -134,7 +135,7 @@ class FileStorage extends Component $this->submit(); } - public function render() + public function render() { return view('livewire.project.service.file-storage', [ 'directoryDeletionCheckboxes' => [ @@ -142,7 +143,7 @@ class FileStorage extends Component ], 'fileDeletionCheckboxes' => [ ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'], - ] + ], ]); } } diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 04bb136db..7f2416e3e 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -33,7 +33,7 @@ class StackForm extends Component $key = data_get($field, 'key'); $value = data_get($field, 'value'); $rules = data_get($field, 'rules', 'nullable'); - $isPassword = data_get($field, 'isPassword'); + $isPassword = data_get($field, 'isPassword', false); $this->fields->put($key, [ 'serviceName' => $serviceName, 'key' => $key, @@ -47,7 +47,15 @@ class StackForm extends Component $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) diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 1be724568..7fb5c45db 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -8,10 +8,10 @@ use App\Events\ApplicationStatusChanged; use App\Jobs\ContainerStatusJob; use App\Models\Server; use App\Models\StandaloneDocker; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; use Visus\Cuid2\Cuid2; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Auth; class Destination extends Component { @@ -119,11 +119,12 @@ class Destination extends Component public function removeServer(int $network_id, int $server_id, $password) { - if (!Hash::check($password, Auth::user()->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) { $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index b48ee7e23..0e140b8c1 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Shared; +use App\Helpers\SshMultiplexingHelper; use App\Models\Application; use App\Models\Server; use App\Models\Service; @@ -17,7 +18,6 @@ use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Support\Facades\Process; use Livewire\Component; -use App\Helpers\SshMultiplexingHelper; class GetLogs extends Component { diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 5bd6b4b9b..017cc9fd7 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -7,7 +7,9 @@ use Livewire\Component; class Executions extends Component { public $executions = []; + public $selectedKey; + public $task; public function getListeners() @@ -29,7 +31,7 @@ class Executions extends Component public function server() { - if (!$this->task) { + if (! $this->task) { return null; } @@ -42,16 +44,18 @@ class Executions extends Component return $this->task->service->destination->server; } } + return null; } public function getServerTimezone() { $server = $this->server(); - if (!$server) { + if (! $server) { return 'UTC'; } $serverTimezone = $server->settings->server_timezone; + return $serverTimezone; } @@ -64,6 +68,7 @@ class Executions extends Component } catch (\Exception $e) { $dateObj->setTimezone(new \DateTimeZone('UTC')); } + return $dateObj->format('Y-m-d H:i:s T'); } } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index a0cb54aa0..e4b5c9b89 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -3,8 +3,8 @@ namespace App\Livewire\Project\Shared\Storages; use App\Models\LocalPersistentVolume; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class Show extends Component @@ -40,8 +40,9 @@ class Show extends Component public function delete($password) { - if (!Hash::check($password, Auth::user()->password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php index 95f6a71bf..319cec192 100644 --- a/app/Livewire/Security/PrivateKey/Create.php +++ b/app/Livewire/Security/PrivateKey/Create.php @@ -8,9 +8,13 @@ use Livewire\Component; class Create extends Component { public string $name = ''; + public string $value = ''; + public ?string $from = null; + public ?string $description = null; + public ?string $publicKey = null; protected $rules = [ @@ -49,7 +53,7 @@ class Create extends Component $privateKey = PrivateKey::createAndStore([ 'name' => $this->name, 'description' => $this->description, - 'private_key' => trim($this->value) . "\n", + 'private_key' => trim($this->value)."\n", 'team_id' => currentTeam()->id, ]); @@ -72,7 +76,7 @@ class Create extends Component $validationResult = PrivateKey::validateAndExtractPublicKey($this->value); $this->publicKey = $validationResult['publicKey']; - if (!$validationResult['isValid']) { + if (! $validationResult['isValid']) { $this->addError('value', 'Invalid private key'); } } diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php index 0b50ba1d8..76441a67e 100644 --- a/app/Livewire/Security/PrivateKey/Index.php +++ b/app/Livewire/Security/PrivateKey/Index.php @@ -2,8 +2,8 @@ namespace App\Livewire\Security\PrivateKey; -use Livewire\Component; use App\Models\PrivateKey; +use Livewire\Component; class Index extends Component { diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index f7306a5b5..a69a5e15d 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component { try { $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->ip = $this->ssh_domain; $server->save(); $server->settings->save(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('refreshServerShow'); + $this->dispatch('warning', 'Cloudflare Tunnels configuration started.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index 08e91a4c7..ed2345b2a 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -3,9 +3,9 @@ namespace App\Livewire\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Livewire\Component; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Component; class Delete extends Component { @@ -15,8 +15,9 @@ class Delete extends Component public function delete($password) { - if (!Hash::check($password, Auth::user()->password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } try { diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 3b3747a81..3cb3305b5 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -24,11 +24,16 @@ class Form extends Component public $timezones; - protected $listeners = [ - 'serverInstalled', - 'refreshServerShow' => 'serverInstalled', - 'revalidate' => '$refresh', - ]; + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured', + 'refreshServerShow' => 'serverInstalled', + 'revalidate' => '$refresh', + ]; + } protected $rules = [ '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() { $this->server->refresh(); @@ -238,4 +249,12 @@ class Form extends Component $this->server->settings->save(); $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.'); + } } diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index c66ed1795..eaa312663 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -6,9 +6,9 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Events\ProxyStatusChanged; use App\Models\Server; -use Livewire\Component; -use Illuminate\Support\Facades\Process; use Illuminate\Process\InvokedProcess; +use Illuminate\Support\Facades\Process; +use Livewire\Component; class Deploy extends Component { diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index ee5e673a5..3026cb297 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -4,9 +4,9 @@ namespace App\Livewire\Team; use App\Models\Team; use App\Models\User; -use Livewire\Component; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Livewire\Component; class AdminView extends Component { @@ -77,8 +77,9 @@ class AdminView extends Component public function delete($id, $password) { - if (!Hash::check($password, Auth::user()->password)) { + if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); + return; } if (! auth()->user()->isInstanceAdmin()) { diff --git a/app/Models/Application.php b/app/Models/Application.php index 8c7520eaf..55006745a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -6,10 +6,10 @@ use App\Enums\ApplicationDeploymentStatus; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Process; use Illuminate\Process\InvokedProcess; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; +use Illuminate\Support\Str; use OpenApi\Attributes as OA; use RuntimeException; use Spatie\Activitylog\Models\Activity; @@ -170,7 +170,7 @@ class Application extends BaseModel $startTime = time(); while (count($processes) > 0) { $finishedProcesses = array_filter($processes, function ($process) { - return !$process->running(); + return ! $process->running(); }); foreach ($finishedProcesses as $containerName => $process) { unset($processes[$containerName]); @@ -209,7 +209,7 @@ class Application extends BaseModel $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } @@ -237,7 +237,6 @@ class Application extends BaseModel instant_remote_process(["docker network rm {$uuid}"], $server, false); } - public function additional_servers() { return $this->belongsToMany(Server::class, 'additional_destinations') @@ -345,7 +344,7 @@ class Application extends BaseModel public function publishDirectory(): Attribute { return Attribute::make( - set: fn ($value) => $value ? '/' . ltrim($value, '/') : null, + set: fn ($value) => $value ? '/'.ltrim($value, '/') : null, ); } @@ -353,7 +352,7 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { if (str($this->git_repository)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}"; } @@ -380,7 +379,7 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; } // Convert the SSH URL to HTTPS URL @@ -399,7 +398,7 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL @@ -416,7 +415,7 @@ class Application extends BaseModel public function gitCommitLink($link): string { - if (!is_null(data_get($this, 'source.html_url')) && !is_null(data_get($this, 'git_repository')) && !is_null(data_get($this, 'git_branch'))) { + if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) { if (str($this->source->html_url)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; } @@ -427,7 +426,7 @@ class Application extends BaseModel $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); $url = $url->withUserInfo(''); - $url = $url->withPath($url->getPath() . '/commits/' . $link); + $url = $url->withPath($url->getPath().'/commits/'.$link); return $url->__toString(); } @@ -480,7 +479,7 @@ class Application extends BaseModel public function baseDirectory(): Attribute { return Attribute::make( - set: fn ($value) => '/' . ltrim($value, '/'), + set: fn ($value) => '/'.ltrim($value, '/'), ); } @@ -823,7 +822,7 @@ class Application extends BaseModel public function workdir() { - return application_configuration_dir() . "/{$this->uuid}"; + return application_configuration_dir()."/{$this->uuid}"; } public function isLogDrainEnabled() @@ -833,7 +832,7 @@ class Application extends BaseModel public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect; + $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -887,7 +886,7 @@ class Application extends BaseModel public function dirOnServer() { - return application_configuration_dir() . "/{$this->uuid}"; + return application_configuration_dir()."/{$this->uuid}"; } public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) @@ -933,7 +932,7 @@ class Application extends BaseModel if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; - if (!$only_checkout) { + if (! $only_checkout) { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true); } if ($exec_in_docker) { @@ -950,7 +949,7 @@ class Application extends BaseModel $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}"; $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; } - if (!$only_checkout) { + if (! $only_checkout) { $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false); } if ($exec_in_docker) { @@ -1011,7 +1010,7 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1019,14 +1018,14 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } @@ -1055,7 +1054,7 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1063,14 +1062,14 @@ class Application extends BaseModel } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } @@ -1096,6 +1095,7 @@ class Application extends BaseModel throw new \Exception($e->getMessage()); } $services = data_get($yaml, 'services'); + $commands = collect([]); $services = collect($services)->map(function ($service) use ($commands) { $serviceVolumes = collect(data_get($service, 'volumes', [])); @@ -1122,20 +1122,20 @@ class Application extends BaseModel } if ($source->startsWith('.')) { $source = $source->after('.'); - $source = $workdir . $source; + $source = $workdir.$source; } $commands->push("mkdir -p $source > /dev/null 2>&1 || true"); } } } $labels = collect(data_get($service, 'labels', [])); - if (!$labels->contains('coolify.managed')) { + if (! $labels->contains('coolify.managed')) { $labels->push('coolify.managed=true'); } - if (!$labels->contains('coolify.applicationId')) { - $labels->push('coolify.applicationId=' . $this->id); + if (! $labels->contains('coolify.applicationId')) { + $labels->push('coolify.applicationId='.$this->id); } - if (!$labels->contains('coolify.type')) { + if (! $labels->contains('coolify.type')) { $labels->push('coolify.type=application'); } data_set($service, 'labels', $labels->toArray()); @@ -1211,7 +1211,7 @@ class Application extends BaseModel $jsonNames = $json->keys()->toArray(); $diff = array_diff($jsonNames, $names); $json = $json->filter(function ($value, $key) use ($diff) { - return !in_array($key, $diff); + return ! in_array($key, $diff); }); if ($json) { $this->docker_compose_domains = json_encode($json); @@ -1233,7 +1233,7 @@ class Application extends BaseModel public function parseContainerLabels(?ApplicationPreview $preview = null) { $customLabels = data_get($this, 'custom_labels'); - if (!$customLabels) { + if (! $customLabels) { return; } if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) { @@ -1316,10 +1316,10 @@ class Application extends BaseModel continue; } if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) { - $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ '); + $healthcheckCommand .= ' '.trim($trimmedLine, '\\ '); } - if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) { - $healthcheckCommand .= ' ' . $trimmedLine; + if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) { + $healthcheckCommand .= ' '.$trimmedLine; break; } } diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 50a0c8173..ce5d3a87f 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel { return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); } + public function server() { if ($this->database) { if ($this->database->destination && $this->database->destination->server) { $server = $this->database->destination->server; + return $server; } } + return null; } } diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 82f0036a5..3cee5a875 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -4,8 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; -use App\Models\Service; -use App\Models\Application; class ScheduledTask extends BaseModel { @@ -37,19 +35,23 @@ class ScheduledTask extends BaseModel if ($this->application) { if ($this->application->destination && $this->application->destination->server) { $server = $this->application->destination->server; + return $server; } } elseif ($this->service) { if ($this->service->destination && $this->service->destination->server) { $server = $this->service->destination->server; + return $server; } } elseif ($this->database) { if ($this->database->destination && $this->database->destination->server) { $server = $this->database->destination->server; + return $server; } } + return null; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index df136c72b..adc8aa7e4 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -165,14 +165,13 @@ class Server extends BaseModel $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); $redirect_url = $this->proxy->redirect_url; - ray($proxy_type); if ($proxy_type === ProxyTypes::TRAEFIK->value) { $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"; } if (empty($redirect_url)) { - if ($proxy_type === 'CADDY') { + if ($proxy_type === ProxyTypes::CADDY->value) { $conf = ':80, :443 { respond 404 }'; @@ -242,7 +241,7 @@ respond 404 $conf; $base64 = base64_encode($conf); - } elseif ($proxy_type === 'CADDY') { + } elseif ($proxy_type === ProxyTypes::CADDY->value) { $conf = ":80, :443 { redir $redirect_url }"; @@ -258,9 +257,6 @@ respond 404 "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", ], $this); - if (config('app.env') == 'local') { - ray($conf); - } if ($proxy_type === 'CADDY') { $this->reloadCaddy(); } @@ -884,6 +880,35 @@ $schema://$host { 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 { return Attribute::make( diff --git a/app/Models/Service.php b/app/Models/Service.php index 80464db7d..d236869ba 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -6,9 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; -use Illuminate\Process\InvokedProcess; use Illuminate\Support\Facades\Storage; use OpenApi\Attributes as OA; use Spatie\Url\Url; @@ -70,7 +70,7 @@ class Service extends BaseModel $databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten()->sortBy('id'); $storages = $applicationStorages->merge($databaseStorages)->implode('updated_at'); - $newConfigHash = $images . $domains . $images . $storages; + $newConfigHash = $images.$domains.$images.$storages; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); @@ -144,6 +144,7 @@ class Service extends BaseModel foreach ($dbs as $db) { $containersToStop[] = "{$db->name}-{$this->uuid}"; } + return $containersToStop; } @@ -157,7 +158,7 @@ class Service extends BaseModel $startTime = time(); while (count($processes) > 0) { $finishedProcesses = array_filter($processes, function ($process) { - return !$process->running(); + return ! $process->running(); }); foreach (array_keys($finishedProcesses) as $containerName) { unset($processes[$containerName]); @@ -196,7 +197,7 @@ class Service extends BaseModel $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } @@ -1061,7 +1062,7 @@ class Service extends BaseModel public function workdir() { - return service_configuration_dir() . "/{$this->uuid}"; + return service_configuration_dir()."/{$this->uuid}"; } public function saveComposeConfigs() diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index f0c78cc08..d312fab96 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -23,7 +23,7 @@ class ServiceApplication extends BaseModel public function restart() { - $container_id = $this->name . '-' . $this->service->uuid; + $container_id = $this->name.'-'.$this->service->uuid; instant_remote_process(["docker restart {$container_id}"], $this->service->server); } @@ -69,7 +69,7 @@ class ServiceApplication extends BaseModel public function workdir() { - return service_configuration_dir() . "/{$this->service->uuid}"; + return service_configuration_dir()."/{$this->service->uuid}"; } public function serviceType() diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 2e25f4402..6b96738e8 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -21,7 +21,7 @@ class ServiceDatabase extends BaseModel public function restart() { - $container_id = $this->name . '-' . $this->service->uuid; + $container_id = $this->name.'-'.$this->service->uuid; remote_process(["docker restart {$container_id}"], $this->service->server); } @@ -88,7 +88,7 @@ class ServiceDatabase extends BaseModel public function workdir() { - return service_configuration_dir() . "/{$this->service->uuid}"; + return service_configuration_dir()."/{$this->service->uuid}"; } public function service() diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index c0e2a3c31..6377f2f15 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -52,7 +52,7 @@ class ForceDisabled extends Notification implements ShouldQueue 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; } @@ -60,7 +60,7 @@ class ForceDisabled extends Notification implements ShouldQueue public function toTelegram(): array { 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).", ]; } } diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 03726f095..f8ccee9db 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -3,11 +3,11 @@ namespace App\Traits; use App\Enums\ApplicationDeploymentStatus; +use App\Helpers\SshMultiplexingHelper; use App\Models\Server; use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; -use App\Helpers\SshMultiplexingHelper; trait ExecuteRemoteCommand { diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 8dce52f15..e252bda10 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -134,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data return 'exited'; } $container = format_docker_command_output_to_json($container); + if ($container->isEmpty()) { + return 'exited'; + } if ($all_data) { return $container[0]; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cd2779466..856222626 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1247,6 +1247,10 @@ function get_public_ips() } $settings->update(['public_ipv4' => $ipv4]); } + } catch (\Exception $e) { + echo "Error: {$e->getMessage()}\n"; + } + try { $ipv6 = $second->output(); if ($ipv6) { $ipv6 = trim($ipv6); @@ -2924,10 +2928,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } $parsedServices = collect([]); - ray()->clearAll(); + // ray()->clearAll(); $allMagicEnvironments = collect([]); foreach ($services as $serviceName => $service) { + $predefinedPort = null; $magicEnvironments = collect([]); $image = data_get_str($service, 'image'); $environment = collect(data_get($service, 'environment', [])); @@ -2936,6 +2941,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $isDatabase = isDatabaseImage(data_get_str($service, 'image')); 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) { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, @@ -2987,8 +3010,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 if (substr_count(str($key)->value(), '_') === 3) { $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); } else { $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + $port = null; } if ($isApplication) { $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); @@ -2999,19 +3024,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); } } + if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { $fqdn = "$fqdn$path"; } } + $fqdnWithPort = $fqdn; + if ($port) { + $fqdnWithPort = "$fqdn:$port"; + } if ($isApplication && is_null($resource->fqdn)) { data_forget($resource, 'environment_variables'); data_forget($resource, 'environment_variables_preview'); - $resource->fqdn = $fqdn; + $resource->fqdn = $fqdnWithPort; $resource->save(); } elseif ($isService && is_null($savedService->fqdn)) { - $savedService->fqdn = $fqdn; + $savedService->fqdn = $fqdnWithPort; $savedService->save(); } @@ -3040,7 +3070,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); - if ($magicEnvironments->count() > 0) { foreach ($magicEnvironments as $key => $value) { $key = str($key); @@ -3455,6 +3484,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $value = $value->after('?'); } 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; } $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ @@ -3547,6 +3588,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($environment->count() > 0) { $environment = $environment->filter(function ($value, $key) { 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); @@ -3631,6 +3683,14 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int data_forget($service, 'volumes.*.is_directory'); 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([ 'container_name' => $containerName, 'restart' => $restart->value(), @@ -3661,6 +3721,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $parsedServices->put($serviceName, $payload); } $topLevel->put('services', $parsedServices); + $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { diff --git a/composer.json b/composer.json index e8b46105d..17432c532 100644 --- a/composer.json +++ b/composer.json @@ -84,7 +84,11 @@ "@php artisan vendor:publish --tag=laravel-assets --ansi --force", "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": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], diff --git a/config/clockwork.php b/config/clockwork.php deleted file mode 100644 index ce880464a..000000000 --- a/config/clockwork.php +++ /dev/null @@ -1,424 +0,0 @@ - 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) - -]; diff --git a/config/sentry.php b/config/sentry.php index c65d3d1ff..60e183283 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.342', + 'release' => '4.0.0-beta.343', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 633c71d60..050ea885b 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ chunkById(100, function ($keys) { - foreach ($keys as $key) { - DB::table('private_keys') - ->where('id', $key->id) - ->update(['private_key' => Crypt::encryptString($key->private_key)]); - } - }); + 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(); + } + } } diff --git a/database/migrations/2024_09_16_170001_populate_ssh_keys_and_clear_mux_directory.php b/database/migrations/2024_09_16_170001_populate_ssh_keys_and_clear_mux_directory.php deleted file mode 100644 index aece86c88..000000000 --- a/database/migrations/2024_09_16_170001_populate_ssh_keys_and_clear_mux_directory.php +++ /dev/null @@ -1,25 +0,0 @@ -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) { - // $key->storeInFileSystem(); - // if ($key->id === 0) { - // Storage::disk('ssh-keys')->put('id.root@host.docker.internal', $key->private_key); - // } - // } - // }); - } -} diff --git a/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php b/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php index f16eccb5c..dfce5682a 100644 --- a/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php +++ b/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php @@ -3,6 +3,7 @@ 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 @@ -13,13 +14,20 @@ class AddSshKeyFingerprintToPrivateKeysTable extends Migration $table->string('fingerprint')->after('private_key')->nullable(); }); - PrivateKey::whereNull('fingerprint')->each(function ($key) { - $fingerprint = PrivateKey::generateFingerprint($key->private_key); - if ($fingerprint) { - $key->fingerprint = $fingerprint; - $key->save(); - } - }); + 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() diff --git a/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php index 77ec1cf3b..dc27d21b0 100644 --- a/database/seeders/PopulateSshKeysDirectorySeeder.php +++ b/database/seeders/PopulateSshKeysDirectorySeeder.php @@ -11,19 +11,30 @@ class PopulateSshKeysDirectorySeeder extends Seeder { public function run() { - Storage::disk('ssh-keys')->deleteDirectory(''); - Storage::disk('ssh-keys')->makeDirectory(''); - Storage::disk('ssh-mux')->deleteDirectory(''); - Storage::disk('ssh-mux')->makeDirectory(''); + 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(); + 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')); } - }); - - Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys')); - Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux')); + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + ray($e->getMessage()); + } } } diff --git a/database/seeders/ServerSeeder.php b/database/seeders/ServerSeeder.php index 197a0b5cb..d32843107 100644 --- a/database/seeders/ServerSeeder.php +++ b/database/seeders/ServerSeeder.php @@ -2,6 +2,8 @@ namespace Database\Seeders; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Database\Seeder; @@ -16,6 +18,10 @@ class ServerSeeder extends Seeder 'ip' => 'coolify-testing-host', 'team_id' => 0, 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], ]); } } diff --git a/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up index ea960df95..3b252b782 100644 --- a/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up +++ b/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up @@ -1,3 +1,3 @@ #!/command/execlineb -P s6-setuidgid webuser -php /var/www/html/artisan app:init --full-cleanup +php /var/www/html/artisan app:init diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 000000000..69a5a9d41 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,21 @@ +#!/bin/sh +# Detect whether /dev/tty is available & functional +if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then + exec < /dev/tty +fi + +# Get list of stashed PHP files +stashed_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php') + +# If there are no stashed PHP files, exit early +if [ -z "$stashed_files" ]; then + exit 0 +fi + +# Set files variable to only include stashed PHP files +files="$stashed_files" + +$(pwd)/vendor/bin/pint $files -q +if [ $? -eq 0 ]; then + git add $files +fi diff --git a/lang/en.json b/lang/en.json index e1603c303..45fd72743 100644 --- a/lang/en.json +++ b/lang/en.json @@ -30,7 +30,7 @@ "service.stop": "This service will be stopped.", "resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).", "resource.non_persistent": "All non-persistent data will be deleted.", - "resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.", - "resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.", - "resource.delete_configurations": "All configuration files will be permanently deleted from the server." + "resource.delete_volumes": "Permanently delete all volumes associated with this resource.", + "resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.", + "resource.delete_configurations": "Permanently delete all configuration files from the server." } diff --git a/other/nightly/.env.development.example b/other/nightly/.env.development.example index 3023a21a6..4de434df2 100644 --- a/other/nightly/.env.development.example +++ b/other/nightly/.env.development.example @@ -19,11 +19,7 @@ DB_PORT=5432 # Set to true to enable Ray RAY_ENABLED=false # Set custom ray port -RAY_PORT= - -# Clockwork Configuration -CLOCKWORK_ENABLED=false -CLOCKWORK_QUEUE_COLLECT=true +# RAY_PORT= # Enable Laravel Telescope for debugging TELESCOPE_ENABLED=false diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 61acbd333..020e7d45b 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -398,90 +398,10 @@ if [ ! -f ~/.ssh/authorized_keys ]; then chmod 600 ~/.ssh/authorized_keys fi -checkSshKeyInAuthorizedKeys() { - grep -qw "root@coolify" ~/.ssh/authorized_keys - return $? -} - -checkSshKeyInCoolifyData() { - [ -s /data/coolify/ssh/keys/id.root@host.docker.internal ] - return $? -} - -generateAuthorizedKeys() { - sed -i "/root@coolify/d" ~/.ssh/authorized_keys - cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys - rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub -} -generateSshKey() { - echo " - Generating SSH key." - ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify - chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal - generateAuthorizedKeys -} - -syncSshKeys() { - DB_RUNNING=$(docker inspect coolify-db --format '{{ .State.Status }}' 2>/dev/null) - # Check if SSH key exists in Coolify data but not in authorized_keys - if checkSshKeyInCoolifyData && ! checkSshKeyInAuthorizedKeys; then - # Add the existing Coolify SSH key to authorized_keys - cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys - # Check if SSH key exists in authorized_keys but not in Coolify data - elif checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then - # Ensure Coolify DB is running before proceeding - if [ "$DB_RUNNING" = "running" ]; then - # Retrieve DB user and SSH key from Coolify database - DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+') - DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t) - - if [ -z "$DB_SSH_KEY" ]; then - # If no key found in DB, generate a new one - echo " - SSH key not found in database. Generating new key." - generateSshKey - else - # If key found in DB, save it and update authorized_keys - echo " - SSH key found in database. Saving to file." - echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal - chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal - chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal - - # Generate public key from private key and update authorized_keys - ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub - sed -i "/root@coolify/d" ~/.ssh/authorized_keys - cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys - rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub - chmod 600 ~/.ssh/authorized_keys - fi - fi - # If SSH key doesn't exist in either location - elif ! checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then - # Ensure Coolify DB is running before proceeding - if [ "$DB_RUNNING" = "running" ]; then - # Retrieve DB user and SSH key from Coolify database - DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+') - DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t) - if [ -z "$DB_SSH_KEY" ]; then - # If no key found in DB, generate a new one - echo " - SSH key not found in database. Generating new key." - generateSshKey - else - # If key found in DB, save it and update authorized_keys - echo " - SSH key found in database. Saving to file." - echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal - chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal - ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub - sed -i "/root@coolify/d" ~/.ssh/authorized_keys - cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys - fi - else - generateSshKey - fi - fi -} - set +e IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) set -e + if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then echo " - Generating SSH key." ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify diff --git a/other/nightly/versions.json b/other/nightly/versions.json index b7e48c698..8bfd2b810 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.339" + "version": "4.0.0-beta.343" }, "nightly": { - "version": "4.0.0-beta.340" + "version": "4.0.0-beta.344" }, "helper": { "version": "1.0.1" @@ -13,4 +13,4 @@ "version": "1.0.1" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index ad1a3cc31..8f6fbde08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1860,9 +1860,9 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 84c6b7e32..babbb9347 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -8,20 +8,20 @@ 'hideLabel' => false, ]) -
- @if(!$hideLabel) - +
+ @if (!$hideLabel) + @endif merge(['class' => $defaultClass]) }} diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 04b4a41c6..8ef082165 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -3,7 +3,7 @@ 'w-full' => !$isMultiline, ])> @if ($label) -