diff --git a/.dockerignore b/.dockerignore index d6abd1451..3a0ec49f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,4 @@ yarn-error.log /_data .rnd /.ssh +.ignition.json diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index fd4be2f11..a9e8a5dd0 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -98,3 +98,4 @@ jobs: if: always() with: webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + diff --git a/.gitignore b/.gitignore index ac8a1e090..09504afee 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ _ide_helper_models.php .rnd /.ssh scripts/load-test/* +.ignition.json diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php new file mode 100644 index 000000000..69365f921 --- /dev/null +++ b/app/Actions/Application/GenerateConfig.php @@ -0,0 +1,17 @@ +clearAll(); + return $application->generateConfig(is_json: $is_json); + } +} diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index f4fe650c5..bdeafd061 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -22,7 +22,7 @@ class CheckConfiguration ]; $proxy_configuration = instant_remote_process($payload, $server, false); if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { - $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value; + $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value(); } if (! $proxy_configuration || is_null($proxy_configuration)) { throw new \Exception('Could not generate proxy configuration'); diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index cf0f6015c..03a0beddf 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -2,14 +2,17 @@ namespace App\Actions\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class CheckProxy { use AsAction; - public function handle(Server $server, $fromUI = false) + // It should return if the proxy should be started (true) or not (false) + public function handle(Server $server, $fromUI = false): bool { if (! $server->isFunctional()) { return false; @@ -62,22 +65,42 @@ class CheckProxy $ip = 'host.docker.internal'; } - $connection80 = @fsockopen($ip, '80'); - $connection443 = @fsockopen($ip, '443'); - $port80 = is_resource($connection80) && fclose($connection80); - $port443 = is_resource($connection443) && fclose($connection443); - if ($port80) { - if ($fromUI) { - throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + $portsToCheck = ['80', '443']; + + try { + if ($server->proxyType() !== ProxyTypes::NONE->value) { + $proxyCompose = CheckConfiguration::run($server); + if (isset($proxyCompose)) { + $yaml = Yaml::parse($proxyCompose); + $portsToCheck = []; + if ($server->proxyType() === ProxyTypes::TRAEFIK->value) { + $ports = data_get($yaml, 'services.traefik.ports'); + } elseif ($server->proxyType() === ProxyTypes::CADDY->value) { + $ports = data_get($yaml, 'services.caddy.ports'); + } + if (isset($ports)) { + foreach ($ports as $port) { + $portsToCheck[] = str($port)->before(':')->value(); + } + } + } } else { - return false; + $portsToCheck = []; } + } catch (\Exception $e) { + ray($e->getMessage()); } - if ($port443) { - if ($fromUI) { - throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); - } else { - return false; + if (count($portsToCheck) === 0) { + return false; + } + foreach ($portsToCheck as $port) { + $connection = @fsockopen($ip, $port); + if (is_resource($connection) && fclose($connection)) { + if ($fromUI) { + throw new \Exception("Port $port is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } else { + return false; + } } } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 4ef9618d0..f20c10123 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -26,7 +26,7 @@ class StartProxy } SaveConfiguration::run($server, $configuration); $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value; + $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); $server->save(); if ($server->isSwarm()) { $commands = $commands->merge([ diff --git a/app/Console/Commands/CheckApplicationDeploymentQueue.php b/app/Console/Commands/CheckApplicationDeploymentQueue.php new file mode 100644 index 000000000..e89d26f2c --- /dev/null +++ b/app/Console/Commands/CheckApplicationDeploymentQueue.php @@ -0,0 +1,50 @@ +option('seconds'); + $deployments = ApplicationDeploymentQueue::whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS, + ApplicationDeploymentStatus::QUEUED, + ])->where('created_at', '<=', now()->subSeconds($seconds))->get(); + if ($deployments->isEmpty()) { + $this->info('No deployments found in the last '.$seconds.' seconds.'); + + return; + } + + $this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.'); + + foreach ($deployments as $deployment) { + if ($this->option('force')) { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + $this->cancelDeployment($deployment); + } else { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + if ($this->confirm('Do you want to cancel this deployment?', true)) { + $this->cancelDeployment($deployment); + } + } + } + } + + private function cancelDeployment(ApplicationDeploymentQueue $deployment) + { + $deployment->update(['status' => ApplicationDeploymentStatus::FAILED]); + if ($deployment->server?->isFunctional()) { + remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false); + } + } +} diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php index f068e3eb2..3aae28ae6 100644 --- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php +++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php @@ -7,9 +7,9 @@ use Illuminate\Console\Command; class CleanupApplicationDeploymentQueue extends Command { - protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; + protected $signature = 'cleanup:deployment-queue {--team-id=}'; - protected $description = 'CleanupApplicationDeploymentQueue'; + protected $description = 'Cleanup application deployment queue.'; public function handle() { diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index dfd09d4b7..66c25ec27 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use App\Jobs\CleanupHelperContainersJob; use App\Models\Application; +use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; @@ -47,6 +48,17 @@ class CleanupStuckedResources extends Command } catch (\Throwable $e) { echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; } + try { + $applicationsDeploymentQueue = ApplicationDeploymentQueue::get(); + foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) { + if (is_null($applicationDeploymentQueue->application)) { + echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n"; + $applicationDeploymentQueue->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n"; + } try { $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index de64afefa..9720e81ac 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -39,8 +39,8 @@ class ServicesGenerate extends Command $serviceTemplatesJson[$name] = $parsed; } } - $serviceTemplatesJson = json_encode($serviceTemplatesJson); - file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson); + $serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL); } private function process_file($file) @@ -78,7 +78,7 @@ class ServicesGenerate extends Command if ($logo->count() > 0) { $logo = str($logo[0])->after('# logo:')->trim()->value(); } else { - $logo = 'svgs/unknown.svg'; + $logo = 'svgs/coolify.png'; } $minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values(); if ($minversion->count() > 0) { diff --git a/app/Enums/StaticImageTypes.php b/app/Enums/StaticImageTypes.php new file mode 100644 index 000000000..5f5304bf8 --- /dev/null +++ b/app/Enums/StaticImageTypes.php @@ -0,0 +1,8 @@ +isIpv6()) { + $scp_command .= '-6 '; + } if (self::isMultiplexingEnabled()) { $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; self::ensureMultiplexedConnection($server); @@ -136,8 +138,8 @@ class SshMultiplexingHelper $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); - $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $delimiter = Hash::make($command); + $delimiter = base64_encode($delimiter); $command = str_replace($delimiter, '', $command); $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 85d8c0c85..2a1f846d3 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -132,6 +132,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -236,6 +237,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -339,6 +341,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -633,7 +636,7 @@ class ApplicationsController extends Controller private function create_application(Request $request, $type) { - $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server']; + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -672,6 +675,7 @@ class ApplicationsController extends Controller $instantDeploy = $request->instant_deploy; $githubAppUuid = $request->github_app_uuid; $useBuildServer = $request->use_build_server; + $isStatic = $request->is_static; $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { @@ -700,8 +704,7 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -709,19 +712,21 @@ class ApplicationsController extends Controller 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } + $return = $this->validateDataApplications($request, $server); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } + $application = new Application; removeUnnecessaryFieldsFromRequest($request); @@ -744,9 +749,15 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; - $application->settings->is_build_server_enabled = $useBuildServer; - $application->settings->save(); $application->save(); + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); @@ -780,8 +791,7 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -790,10 +800,10 @@ class ApplicationsController extends Controller 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', - 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -842,8 +852,10 @@ class ApplicationsController extends Controller $application->environment_id = $environment->id; $application->source_type = $githubApp->getMorphClass(); $application->source_id = $githubApp->id; - $application->settings->is_build_server_enabled = $useBuildServer; - $application->settings->save(); + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { @@ -878,8 +890,8 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -888,10 +900,10 @@ class ApplicationsController extends Controller 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', - 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ @@ -936,8 +948,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; - $application->settings->is_build_server_enabled = $useBuildServer; - $application->settings->save(); + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { @@ -969,10 +983,13 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'dockerfile-'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + + $validationRules = [ 'dockerfile' => 'string|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1017,8 +1034,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; - $application->settings->is_build_server_enabled = $useBuildServer; - $application->settings->save(); + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1049,12 +1068,14 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'docker-image-'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'docker_registry_image_name' => 'string|required', 'docker_registry_image_tag' => 'string', 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1077,8 +1098,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; - $application->settings->is_build_server_enabled = $useBuildServer; - $application->settings->save(); + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1125,10 +1148,12 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'service'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'docker_compose_raw' => 'string|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1478,8 +1503,7 @@ class ApplicationsController extends Controller $server = $application->destination->server; $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server']; - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'name' => 'string|max:255', 'description' => 'string|nullable', 'static_image' => 'string', @@ -1489,7 +1513,9 @@ class ApplicationsController extends Controller 'docker_compose_domains' => 'array|nullable', 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); // Validate ports_exposes if ($request->has('ports_exposes')) { @@ -1555,8 +1581,11 @@ class ApplicationsController extends Controller $instantDeploy = $request->instant_deploy; $use_build_server = $request->use_build_server; - $application->settings->is_build_server_enabled = $use_build_server; - $application->settings->save(); + + if (isset($use_build_server)) { + $application->settings->is_build_server_enabled = $use_build_server; + $application->settings->save(); + } removeUnnecessaryFieldsFromRequest($request); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 7109fda3b..769739d5e 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Actions\Database\StopDatabase; use App\Events\BackupCreated; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; @@ -24,7 +23,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; -use Visus\Cuid2\Cuid2; class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { @@ -63,31 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public function __construct($backup) { $this->backup = $backup; - $this->team = Team::find($backup->team_id); - if (is_null($this->team)) { - return; - } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->service->server; - $this->s3 = $this->backup->s3; - } else { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->destination->server; - $this->s3 = $this->backup->s3; - } } public function handle(): void { try { - // Check if team is exists - if (is_null($this->team)) { - StopDatabase::run($this->database); - $this->database->delete(); + $this->team = Team::find($this->backup->team_id); + if (! $this->team) { + $this->backup->delete(); return; } + if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->service->server; + $this->s3 = $this->backup->s3; + } else { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->destination->server; + $this->s3 = $this->backup->s3; + } + if (is_null($this->server)) { + throw new \Exception('Server not found?!'); + } + if (is_null($this->database)) { + throw new \Exception('Database not found?!'); + } BackupCreated::dispatch($this->team->id); @@ -237,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } } $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; - if ($this->database->name === 'coolify-db') { $databasesToBackup = ['coolify']; $this->directory_name = $this->container_name = 'coolify-db'; @@ -250,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue try { if (str($databaseType)->contains('postgres')) { $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -278,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->backup_standalone_mongodb($database); } elseif (str($databaseType)->contains('mysql')) { $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -287,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->backup_standalone_mysql($database); } elseif (str($databaseType)->contains('mariadb')) { $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -325,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); throw $e; } finally { - BackupCreated::dispatch($this->team->id); + if ($this->team) { + BackupCreated::dispatch($this->team->id); + } } } @@ -384,7 +393,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->postgres_password) { $backupCommand .= " -e PGPASSWORD=$this->postgres_password"; } - $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + if ($this->backup->dump_all) { + $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location"; + } else { + $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + } $commands[] = $backupCommand; ray($commands); @@ -405,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; - ray($commands); + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; + } $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { @@ -424,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; + } ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); @@ -466,34 +486,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } } - // private function upload_to_s3(): void - // { - // try { - // if (is_null($this->s3)) { - // return; - // } - // $key = $this->s3->key; - // $secret = $this->s3->secret; - // // $region = $this->s3->region; - // $bucket = $this->s3->bucket; - // $endpoint = $this->s3->endpoint; - // $this->s3->testConnection(shouldSave: true); - // $configName = new Cuid2; - - // $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/'); - // $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'"; - // $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'"; - // instant_remote_process($commands, $this->server); - // $this->add_to_backup_output('Uploaded to S3.'); - // } catch (\Throwable $e) { - // $this->add_to_backup_output($e->getMessage()); - // throw $e; - // } finally { - // $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'"; - // $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'"; - // instant_remote_process($removeConfigCommands, $this->server, false); - // } - // } private function upload_to_s3(): void { try { @@ -515,10 +507,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->ensureHelperImageAvailable(); $fullImageName = $this->getFullImageName(); - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; - $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + + if (isDev()) { + if ($this->database->name === 'coolify-db') { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } else { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } + } else { + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; + } + if ($this->s3->isHetzner()) { + $endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value(); + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret"; + } else { + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + } $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); + $this->add_to_backup_output('Uploaded to S3.'); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 1f0b68dd3..d18a7689e 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -30,7 +30,7 @@ class Dashboard extends Component public function cleanup_queue() { - Artisan::queue('cleanup:application-deployment-queue', [ + Artisan::queue('cleanup:deployment-queue', [ '--team-id' => currentTeam()->id, ]); } diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 68691c1cd..934e81661 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -61,6 +61,7 @@ class Help extends Component send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); } $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); + $this->reset('description', 'subject'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index d2700f444..2e327d80f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -2,9 +2,11 @@ namespace App\Livewire\Project\Application; +use App\Actions\Application\GenerateConfig; use App\Models\Application; use Illuminate\Support\Collection; use Livewire\Component; +use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class General extends Component @@ -182,9 +184,7 @@ class General extends Component $storage->save(); }); } - } - } public function loadComposeFile($isInit = false) @@ -241,16 +241,6 @@ class General extends Component } } - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->resetDefaultLabels(); - } public function updatedApplicationBuildPack() { @@ -287,18 +277,22 @@ class General extends Component public function resetDefaultLabels() { - if ($this->application->settings->is_container_label_readonly_enabled) { - return; + try { + if ($this->application->settings->is_container_label_readonly_enabled) { + return; + } + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); + $this->ports_exposes = $this->application->ports_exposes; + $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->ports_exposes = $this->application->ports_exposes; - $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; - $this->application->custom_labels = base64_encode($this->customLabels); - $this->application->save(); - if ($this->application->build_pack === 'dockercompose') { - $this->loadComposeFile(); - } - $this->dispatch('configurationChanged'); } public function checkFqdns($showToaster = true) @@ -320,7 +314,7 @@ class General extends Component public function set_redirect() { try { - $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); + $has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count(); if ($has_www === 0 && $this->application->redirect === 'www') { $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.

Please add www to your domain list and as an A DNS record (if applicable).'); @@ -337,15 +331,18 @@ class General extends Component public function submit($showToaster = true) { try { - if ($this->application->isDirty('redirect')) { - $this->set_redirect(); - } $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); return str($domain)->trim()->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $this->resetDefaultLabels(); + + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->checkFqdns(); @@ -408,9 +405,25 @@ class General extends Component $this->application->save(); $showToaster && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } return handleError($e, $this); } finally { $this->dispatch('configurationChanged'); } } + public function downloadConfig() + { + $config = GenerateConfig::run($this->application, true); + $fileName = str($this->application->name)->slug()->append('_config.json'); + + return response()->streamDownload(function () use ($config) { + echo $config; + }, $fileName, [ + 'Content-Type' => 'application/json', + 'Content-Disposition' => 'attachment; filename=' . $fileName, + ]); + } } diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index e4f100fcf..9a0b9b851 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -31,10 +31,14 @@ class Form extends Component public function generate_real_url() { if (data_get($this->application, 'fqdn')) { - $firstFqdn = str($this->application->fqdn)->before(','); - $url = Url::fromString($firstFqdn); - $host = $url->getHost(); - $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + try { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } catch (\Exception $e) { + $this->dispatch('error', 'Invalid FQDN.'); + } } } diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 98b2b4263..7e2e4a12b 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -31,6 +31,7 @@ class BackupEdit extends Component 'backup.save_s3' => 'required|boolean', 'backup.s3_storage_id' => 'nullable|integer', 'backup.databases_to_backup' => 'nullable', + 'backup.dump_all' => 'required|boolean', ]; protected $validationAttributes = [ @@ -40,6 +41,7 @@ class BackupEdit extends Component 'backup.save_s3' => 'Save to S3', 'backup.s3_storage_id' => 'S3 Storage', 'backup.databases_to_backup' => 'Databases to Backup', + 'backup.dump_all' => 'Backup All Databases', ]; protected $messages = [ diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index beb5a9c39..8021e25d3 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -26,7 +26,7 @@ class ScheduledBackups extends Component public function mount(): void { if ($this->selectedBackupId) { - $this->setSelectedBackup($this->selectedBackupId); + $this->setSelectedBackup($this->selectedBackupId, true); } $this->parameters = get_route_parameters(); if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { @@ -37,10 +37,13 @@ class ScheduledBackups extends Component $this->s3s = currentTeam()->s3s; } - public function setSelectedBackup($backupId) + public function setSelectedBackup($backupId, $force = false) { + if ($this->selectedBackupId === $backupId && ! $force) { + return; + } $this->selectedBackupId = $backupId; - $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); + $this->selectedBackup = $this->database->scheduledBackups->find($backupId); if (is_null($this->selectedBackup)) { $this->selectedBackupId = null; } diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index b5c5cb1db..971d4700b 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -31,10 +31,12 @@ class PublicGitRepository extends Component public bool $isStatic = false; + public bool $checkCoolifyConfig = true; + public ?string $publish_directory = null; // In case of docker compose - public ?string $base_directory = null; + public string $base_directory = '/'; public ?string $docker_compose_location = '/docker-compose.yaml'; // End of docker compose @@ -97,6 +99,7 @@ class PublicGitRepository extends Component $this->base_directory = '/'.$this->base_directory; } } + } public function updatedDockerComposeLocation() @@ -275,6 +278,7 @@ class PublicGitRepository extends Component 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } else { $application_init = [ @@ -289,6 +293,7 @@ class PublicGitRepository extends Component 'source_id' => $this->git_source->id, 'source_type' => $this->git_source->getMorphClass(), 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } @@ -303,11 +308,15 @@ class PublicGitRepository extends Component $application->settings->is_static = $this->isStatic; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; $application->save(); - + if ($this->checkCoolifyConfig) { + // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id); + // if ($config) { + // $application->setConfig($config); + // } + } return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 3c5f3901b..7f8247597 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -49,11 +49,8 @@ class Select extends Component public ?string $existingPostgresqlUrl = null; - public ?string $search = null; - protected $queryString = [ 'server_id', - 'search', ]; public function mount() @@ -90,40 +87,119 @@ class Select extends Component // } // } - public function updatedSearch() + public function loadServices() { - $this->loadServices(); - } + $services = get_service_templates(true); + $services = collect($services)->map(function ($service, $key) { + return [ + 'name' => str($key)->headline(), + 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')), + ] + (array) $service; + })->all(); + $gitBasedApplications = [ + [ + 'id' => 'public', + 'name' => 'Public Repository', + 'description' => 'You can deploy any kind of public repositories from the supported git providers.', + 'logo' => asset('svgs/git.svg'), + ], + [ + 'id' => 'private-gh-app', + 'name' => 'Private Repository (with GitHub App)', + 'description' => 'You can deploy public & private repositories through your GitHub Apps.', + 'logo' => asset('svgs/github.svg'), + ], + [ + 'id' => 'private-deploy-key', + 'name' => 'Private Repository (with Deploy Key)', + 'description' => 'You can deploy private repositories with a deploy key.', + 'logo' => asset('svgs/git.svg'), + ], + ]; + $dockerBasedApplications = [ + [ + 'id' => 'dockerfile', + 'name' => 'Dockerfile', + 'description' => 'You can deploy a simple Dockerfile, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-compose-empty', + 'name' => 'Docker Compose Empty', + 'description' => 'You can deploy complex application easily with Docker Compose, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-image', + 'name' => 'Docker Image', + 'description' => 'You can deploy an existing Docker Image from any Registry, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + ]; + $databases = [ + [ + 'id' => 'postgresql', + 'name' => 'PostgreSQL', + 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.', + 'logo' => ' +', + ], + [ + 'id' => 'mysql', + 'name' => 'MySQL', + 'description' => 'MySQL is an open-source relational database management system. ', + 'logo' => ' + + + +', - public function loadServices(bool $force = false) - { - try { - $this->loadingServices = true; - if (count($this->allServices) > 0 && ! $force) { - if (! $this->search) { - $this->services = $this->allServices; + ], + [ + 'id' => 'mariadb', + 'name' => 'MariaDB', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source software under the GNU General Public License.', + 'logo' => '', + ], + [ + 'id' => 'redis', + 'name' => 'Redis', + 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.', + 'logo' => '', + ], + [ + 'id' => 'keydb', + 'name' => 'KeyDB', + 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.', + 'logo' => '
', + ], + [ + 'id' => 'dragonfly', + 'name' => 'Dragonfly', + 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.', + 'logo' => '
', + ], + [ + 'id' => 'mongodb', + 'name' => 'MongoDB', + 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.', + 'logo' => '', + ], + [ + 'id' => 'clickhouse', + 'name' => 'ClickHouse', + 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.', + 'logo' => '
', + ], - return; - } - $this->services = $this->allServices->filter(function ($service, $key) { - $tags = collect(data_get($service, 'tags', [])); + ]; - return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) { - return str_contains(strtolower($tag), strtolower($this->search)); - }); - }); - } else { - $this->search = null; - $this->allServices = get_service_templates($force); - $this->services = $this->allServices->filter(function ($service, $key) { - return str_contains(strtolower($key), strtolower($this->search)); - }); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->loadingServices = false; - } + return [ + 'services' => $services, + 'gitBasedApplications' => $gitBasedApplications, + 'dockerBasedApplications' => $dockerBasedApplications, + 'databases' => $databases, + ]; } public function instantSave() @@ -141,6 +217,7 @@ class Select extends Component public function setType(string $type) { + $type = str($type)->lower()->slug()->value(); if ($this->loading) { return; } diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 70e8006c7..4138f720e 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Models\ServiceApplication; use Livewire\Component; +use Spatie\Url\Url; class EditDomain extends Component { @@ -20,21 +21,16 @@ class EditDomain extends Component { $this->application = ServiceApplication::find($this->applicationId); } - - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } - public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -44,12 +40,15 @@ class EditDomain extends Component } else { $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->application->service->parse(); $this->dispatch('refresh'); $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 9cd309885..70b3b5db6 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -108,6 +108,21 @@ class Navbar extends Component return; } + StopService::run(service: $this->service, dockerCleanup: false); + $this->service->parse(); + $this->dispatch('imagePulled'); + $activity = StartService::run($this->service); + $this->dispatch('activityMonitor', $activity->id); + } + + public function pullAndRestartEvent() + { + $this->checkDeployments(); + if ($this->isDeploymentProgress) { + $this->dispatch('error', 'There is a deployment in progress.'); + + return; + } PullImage::run($this->service); StopService::run(service: $this->service, dockerCleanup: false); $this->service->parse(); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 56b506043..ba37313fd 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -6,6 +6,7 @@ use App\Models\ServiceApplication; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Component; +use Spatie\Url\Url; class ServiceApplicationView extends Component { @@ -31,13 +32,7 @@ class ServiceApplicationView extends Component public function updatedApplicationFqdn() { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); + } public function instantSave() @@ -83,6 +78,14 @@ class ServiceApplicationView extends Component public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -92,10 +95,13 @@ class ServiceApplicationView extends Component } else { $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->dispatch('generateDockerCompose'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 7f2416e3e..2c751aa92 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -34,6 +34,7 @@ class StackForm extends Component $value = data_get($field, 'value'); $rules = data_get($field, 'rules', 'nullable'); $isPassword = data_get($field, 'isPassword', false); + $customHelper = data_get($field, 'customHelper', false); $this->fields->put($key, [ 'serviceName' => $serviceName, 'key' => $key, @@ -41,6 +42,7 @@ class StackForm extends Component 'value' => $value, 'isPassword' => $isPassword, 'rules' => $rules, + 'customHelper' => $customHelper, ]); $this->rules["fields.$key.value"] = $rules; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index a859c90b0..0dbf0f957 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -48,14 +48,6 @@ class Add extends Component public function submit() { $this->validate(); - // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - // $type = str($this->value)->after('{{')->before('.')->value; - // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - - // return; - // } - // } $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 055788b57..5a711259b 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -53,30 +53,16 @@ class All extends Component public function sortEnvironmentVariables() { - if ($this->resource->type() === 'application') { - $this->resource->load(['environment_variables', 'environment_variables_preview']); - } else { - $this->resource->load(['environment_variables']); + if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) { + if ($this->resource->environment_variables) { + $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values(); + } + + if ($this->resource->environment_variables_preview) { + $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values(); + } } - $sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order'; - - $sortFunction = function ($variables) use ($sortBy) { - if (! $variables) { - return $variables; - } - if ($sortBy === 'key') { - return $variables->sortBy(function ($item) { - return strtolower($item->key); - }, SORT_NATURAL | SORT_FLAG_CASE)->values(); - } else { - return $variables->sortBy('order')->values(); - } - }; - - $this->resource->environment_variables = $sortFunction($this->resource->environment_variables); - $this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview); - $this->getDevView(); } @@ -121,6 +107,8 @@ class All extends Component $this->sortEnvironmentVariables(); } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->refreshEnvs(); } } diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 27be46227..916db650f 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -34,9 +34,9 @@ class Terminal extends Component if ($status !== 'running') { return; } - $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); } else { - $command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); } // ssh command is sent back to frontend then to websocket diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php new file mode 100644 index 000000000..dea842651 --- /dev/null +++ b/app/Livewire/Project/Shared/UploadConfig.php @@ -0,0 +1,41 @@ +config = '{ + "build_pack": "nixpacks", + "base_directory": "/nodejs", + "publish_directory": "/", + "ports_exposes": "3000", + "settings": { + "is_static": false + } +}'; + } + } + public function uploadConfig() + { + try { + $application = Application::findOrFail($this->applicationId); + $application->setConfig($this->config); + $this->dispatch('success', 'Application settings updated'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + return; + } + + } + public function render() + { + return view('livewire.project.shared.upload-config'); + } +} diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index 40752630e..fe68a8ba5 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -15,6 +15,8 @@ class ApiTokens extends Component public bool $readOnly = true; + public bool $rootAccess = false; + public array $permissions = ['read-only']; public $isApiEnabled; @@ -35,12 +37,11 @@ class ApiTokens extends Component if ($this->viewSensitiveData) { $this->permissions[] = 'view:sensitive'; $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; } else { $this->permissions = array_diff($this->permissions, ['view:sensitive']); } - if (count($this->permissions) == 0) { - $this->permissions = ['*']; - } + $this->makeSureOneIsSelected(); } public function updatedReadOnly() @@ -48,11 +49,30 @@ class ApiTokens extends Component if ($this->readOnly) { $this->permissions[] = 'read-only'; $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; } else { $this->permissions = array_diff($this->permissions, ['read-only']); } - if (count($this->permissions) == 0) { + $this->makeSureOneIsSelected(); + } + + public function updatedRootAccess() + { + if ($this->rootAccess) { $this->permissions = ['*']; + $this->readOnly = false; + $this->viewSensitiveData = false; + } else { + $this->readOnly = true; + $this->permissions = ['read-only']; + } + } + + public function makeSureOneIsSelected() + { + if (count($this->permissions) == 0) { + $this->permissions = ['read-only']; + $this->readOnly = true; } } @@ -62,12 +82,6 @@ class ApiTokens extends Component $this->validate([ 'description' => 'required|min:3|max:255', ]); - // if ($this->viewSensitiveData) { - // $this->permissions[] = 'view:sensitive'; - // } - // if ($this->readOnly) { - // $this->permissions[] = 'read-only'; - // } $token = auth()->user()->createToken($this->description, $this->permissions); $this->tokens = auth()->user()->tokens; session()->flash('token', $token->plainTextToken); diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index a69a5e15d..f58d7b6be 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -30,6 +30,11 @@ class ConfigureCloudflareTunnels extends Component public function submit() { try { + if (str($this->ssh_domain)->contains('https://')) { + $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); + // remove / from the end + $this->ssh_domain = str($this->ssh_domain)->replace('/', ''); + } $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); ConfigureCloudflared::dispatch($server, $this->cloudflare_token); $server->settings->is_cloudflare_tunnel = true; diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php index 20db4dad4..f4f18381f 100644 --- a/app/Livewire/Server/Proxy/Status.php +++ b/app/Livewire/Server/Proxy/Status.php @@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy; use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; -use App\Jobs\ContainerStatusJob; +use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -44,7 +44,10 @@ class Status extends Component } $this->numberOfPolls++; } - CheckProxy::run($this->server, true); + $shouldStart = CheckProxy::run($this->server, true); + if ($shouldStart) { + StartProxy::run($this->server, false); + } $this->dispatch('proxyStatusUpdated'); if ($this->server->proxy->status === 'running') { $this->polling = false; diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index a05834ecc..c5250e1e3 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -43,15 +43,17 @@ class Create extends Component 'endpoint' => 'Endpoint', ]; - public function mount() + public function updatedEndpoint($value) { - if (isDev()) { - $this->name = 'Local MinIO'; - $this->description = 'Local MinIO'; - $this->key = 'minioadmin'; - $this->secret = 'minioadmin'; - $this->bucket = 'local'; - $this->endpoint = 'http://coolify-minio:9000'; + if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) { + $this->endpoint = 'https://'.$value; + $value = $this->endpoint; + } + + if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) { + $this->bucket = str($value)->after('//')->before('.'); + } elseif (str($value)->contains('your-objectstorage.com')) { + $this->bucket = $this->bucket ?: str($value)->after('//')->before('.'); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index dfa875a5a..07aeb4c5b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use OpenApi\Attributes as OA; use RuntimeException; @@ -143,6 +144,9 @@ class Application extends BaseModel } $application->tags()->detach(); $application->previews()->delete(); + foreach ($application->deployment_queue as $deployment) { + $deployment->delete(); + } }); } @@ -710,6 +714,11 @@ class Application extends BaseModel return $this->hasMany(ApplicationPreview::class); } + public function deployment_queue() + { + return $this->hasMany(ApplicationDeploymentQueue::class); + } + public function destination() { return $this->morphTo(); @@ -1419,4 +1428,67 @@ class Application extends BaseModel return $parsedCollection->toArray(); } } + + public function generateConfig($is_json = false) + { + $config = collect([]); + if ($this->build_pack = 'nixpacks') { + $config = collect([ + 'build_pack' => 'nixpacks', + 'docker_registry_image_name' => $this->docker_registry_image_name, + 'docker_registry_image_tag' => $this->docker_registry_image_tag, + 'install_command' => $this->install_command, + 'build_command' => $this->build_command, + 'start_command' => $this->start_command, + 'base_directory' => $this->base_directory, + 'publish_directory' => $this->publish_directory, + 'custom_docker_run_options' => $this->custom_docker_run_options, + 'ports_exposes' => $this->ports_exposes, + 'ports_mappings' => $this->ports_mapping, + 'settings' => collect([ + 'is_static' => $this->settings->is_static, + ]), + ]); + } + $config = $config->filter(function ($value) { + return str($value)->isNotEmpty(); + }); + if ($is_json) { + return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + return $config; + } + public function setConfig($config) { + + $config = $config; + $validator = Validator::make(['config' => $config], [ + 'config' => 'required|json', + ]); + if ($validator->fails()) { + throw new \Exception('Invalid JSON format'); + } + $config = json_decode($config, true); + + $deepValidator = Validator::make(['config' => $config], [ + 'config.build_pack' => 'required|string', + 'config.base_directory' => 'required|string', + 'config.publish_directory' => 'required|string', + 'config.ports_exposes' => 'required|string', + 'config.settings.is_static' => 'required|boolean', + ]); + if ($deepValidator->fails()) { + throw new \Exception('Invalid data'); + } + $config = $deepValidator->validated()['config']; + + try { + $settings = data_get($config, 'settings', []); + data_forget($config, 'settings'); + $this->update($config); + $this->settings()->update($settings); + } catch (\Exception $e) { + throw new \Exception('Failed to update application settings'); + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 90d7608cc..c261c30c6 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; use OpenApi\Attributes as OA; @@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; + public function application(): Attribute + { + return Attribute::make( + get: fn () => Application::find($this->application_id), + ); + } + + public function server(): Attribute + { + return Attribute::make( + get: fn () => Server::find($this->server_id), + ); + } + public function setStatus(string $status) { $this->update([ diff --git a/app/Models/Project.php b/app/Models/Project.php index 18481751c..5a9dd964a 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -24,9 +24,11 @@ class Project extends BaseModel { protected $guarded = []; + protected $appends = ['default_environment']; + public static function ownedByCurrentTeam() { - return Project::whereTeamId(currentTeam()->id)->orderBy('name'); + return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)'); } protected static function booted() @@ -131,7 +133,7 @@ class Project extends BaseModel return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } - public function default_environment() + public function getDefaultEnvironmentAttribute() { $default = $this->environments()->where('name', 'production')->first(); if ($default) { diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 4c7faaa6f..a432a6e9c 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -40,6 +40,16 @@ class S3Storage extends BaseModel return "{$this->endpoint}/{$this->bucket}"; } + public function isHetzner() + { + return str($this->endpoint)->contains('your-objectstorage.com'); + } + + public function isDigitalOcean() + { + return str($this->endpoint)->contains('digitaloceanspaces.com'); + } + public function testConnection(bool $shouldSave = false) { try { diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index ce5d3a87f..3921e32e4 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -39,13 +39,19 @@ class ScheduledDatabaseBackup extends BaseModel public function server() { if ($this->database) { - if ($this->database->destination && $this->database->destination->server) { - $server = $this->database->destination->server; - + if ($this->database instanceof ServiceDatabase) { + $destination = data_get($this->database->service, 'destination'); + $server = data_get($destination, 'server'); + } else { + $destination = data_get($this->database, 'destination'); + $server = data_get($destination, 'server'); + } + if ($server) { return $server; } } + return null; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index f896541ad..0eca3c168 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -209,10 +209,13 @@ respond 404 1 => 'https', ], 'service' => 'noop', - 'rule' => 'HostRegexp(`{catchall:.*}`)', + 'rule' => 'HostRegexp(`.+`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], 'priority' => 1, 'middlewares' => [ - 0 => 'redirect-regexp@file', + 0 => 'redirect-regexp', ], ], ], @@ -448,11 +451,19 @@ $schema://$host { // Should move everything except /caddy and /nginx to /traefik // The code needs to be modified as well, so maybe it does not worth it if ($proxyType === ProxyTypes::TRAEFIK->value) { - $proxy_path = $proxy_path; + // Do nothing } elseif ($proxyType === ProxyTypes::CADDY->value) { - $proxy_path = $proxy_path.'/caddy'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'; + } else { + $proxy_path = $proxy_path.'/caddy'; + } } elseif ($proxyType === ProxyTypes::NGINX->value) { - $proxy_path = $proxy_path.'/nginx'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; + } else { + $proxy_path = $proxy_path.'/nginx'; + } } return $proxy_path; @@ -460,15 +471,6 @@ $schema://$host { public function proxyType() { - // $proxyType = $this->proxy->get('type'); - // if ($proxyType === ProxyTypes::NONE->value) { - // return $proxyType; - // } - // if (is_null($proxyType)) { - // $this->proxy->type = ProxyTypes::TRAEFIK->value; - // $this->proxy->status = ProxyStatus::EXITED->value; - // $this->save(); - // } return data_get($this->proxy, 'type'); } @@ -1221,4 +1223,9 @@ $schema://$host { return instant_remote_process($commands, $this, false); } + + public function isIpv6(): bool + { + return str($this->ip)->contains(':'); + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index c9c086622..0036a9fda 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -283,9 +283,162 @@ class Service extends BaseModel $fields = collect([]); $applications = $this->applications()->get(); foreach ($applications as $application) { - $image = str($application->image)->before(':')->value(); + $image = str($application->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)?->contains('rabbitmq'): + case $image->contains('castopod'): + $data = collect([]); + $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first(); + if ($disable_https) { + $data = $data->merge([ + 'Disable HTTPS' => [ + 'key' => 'CP_DISABLE_HTTPS', + 'value' => data_get($disable_https, 'value'), + 'rules' => 'required', + 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS", + ], + ]); + } + $fields->put('Castopod', $data->toArray()); + break; + case $image->contains('label-studio'): + $data = collect([]); + $username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => 'LABEL_STUDIO_USERNAME', + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Label Studio', $data->toArray()); + break; + case $image->contains('litellm'): + $data = collect([]); + $username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Litellm', $data->toArray()); + break; + case $image->contains('langfuse'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first(); + if ($email) { + $data = $data->merge([ + 'Admin Email' => [ + 'key' => data_get($email, 'key'), + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first(); + ray('password', $password); + if ($password) { + $data = $data->merge([ + 'Admin Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Langfuse', $data->toArray()); + break; + case $image->contains('invoiceninja'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first(); + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($email, 'key'), + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Invoice Ninja', $data->toArray()); + break; + case $image->contains('argilla'): + $data = collect([]); + $api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first(); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first(); + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Argilla', $data->toArray()); + break; + case $image->contains('rabbitmq'): $data = collect([]); $host_port = $this->environment_variables()->where('key', 'PORT')->first(); $username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first(); @@ -320,7 +473,7 @@ class Service extends BaseModel } $fields->put('RabbitMQ', $data->toArray()); break; - case str($image)?->contains('tolgee'): + case $image->contains('tolgee'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first(); $data = $data->merge([ @@ -334,7 +487,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_TOLGEE', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -343,7 +496,7 @@ class Service extends BaseModel } $fields->put('Tolgee', $data->toArray()); break; - case str($image)?->contains('logto'): + case $image->contains('logto'): $data = collect([]); $logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first(); $logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); @@ -367,7 +520,7 @@ class Service extends BaseModel } $fields->put('Logto', $data->toArray()); break; - case str($image)?->contains('unleash-server'): + case $image->contains('unleash-server'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first(); $data = $data->merge([ @@ -381,7 +534,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_UNLEASH', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -390,7 +543,7 @@ class Service extends BaseModel } $fields->put('Unleash', $data->toArray()); break; - case str($image)?->contains('grafana'): + case $image->contains('grafana'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first(); $data = $data->merge([ @@ -404,7 +557,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'GF_SECURITY_ADMIN_PASSWORD', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -413,7 +566,7 @@ class Service extends BaseModel } $fields->put('Grafana', $data->toArray()); break; - case str($image)?->contains('directus'): + case $image->contains('directus'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -439,7 +592,7 @@ class Service extends BaseModel } $fields->put('Directus', $data->toArray()); break; - case str($image)?->contains('kong'): + case $image->contains('kong'): $data = collect([]); $dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); $dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -463,7 +616,7 @@ class Service extends BaseModel ]); } $fields->put('Supabase', $data->toArray()); - case str($image)?->contains('minio'): + case $image->contains('minio'): $data = collect([]); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first(); @@ -516,7 +669,7 @@ class Service extends BaseModel $fields->put('MinIO', $data->toArray()); break; - case str($image)?->contains('weblate'): + case $image->contains('weblate'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first(); @@ -542,7 +695,7 @@ class Service extends BaseModel } $fields->put('Weblate', $data->toArray()); break; - case str($image)?->contains('meilisearch'): + case $image->contains('meilisearch'): $data = collect([]); $SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first(); if ($SERVICE_PASSWORD_MEILISEARCH) { @@ -556,7 +709,7 @@ class Service extends BaseModel } $fields->put('Meilisearch', $data->toArray()); break; - case str($image)?->contains('ghost'): + case $image->contains('ghost'): $data = collect([]); $MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first(); $MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first(); @@ -616,45 +769,8 @@ class Service extends BaseModel $fields->put('Ghost', $data->toArray()); break; - default: - $data = collect([]); - $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); - // Chaskiq - $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); - $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); - if ($admin_user) { - $data = $data->merge([ - 'User' => [ - 'key' => 'SERVICE_USER_ADMIN', - 'value' => data_get($admin_user, 'value', 'admin'), - 'readonly' => true, - 'rules' => 'required', - ], - ]); - } - if ($admin_password) { - $data = $data->merge([ - 'Password' => [ - 'key' => 'SERVICE_PASSWORD_ADMIN', - 'value' => data_get($admin_password, 'value'), - 'rules' => 'required', - 'isPassword' => true, - ], - ]); - } - if ($admin_email) { - $data = $data->merge([ - 'Email' => [ - 'key' => 'ADMIN_EMAIL', - 'value' => data_get($admin_email, 'value'), - 'rules' => 'required|email', - ], - ]); - } - $fields->put('Admin', $data->toArray()); - break; - case str($image)?->contains('vaultwarden'): + case $image->contains('vaultwarden'): $data = collect([]); $DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first(); @@ -720,7 +836,7 @@ class Service extends BaseModel $fields->put('Vaultwarden', $data); break; - case str($image)->contains('gitlab/gitlab'): + case $image->contains('gitlab/gitlab'): $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first(); $data = collect([]); if ($password) { @@ -744,7 +860,7 @@ class Service extends BaseModel $fields->put('GitLab', $data->toArray()); break; - case str($image)->contains('code-server'): + case $image->contains('code-server'): $data = collect([]); $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first(); if ($password) { @@ -770,14 +886,78 @@ class Service extends BaseModel } $fields->put('Code Server', $data->toArray()); break; + case $image->contains('elestio/strapi'): + $data = collect([]); + $license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first(); + if ($license) { + $data = $data->merge([ + 'License' => [ + 'key' => data_get($license, 'key'), + 'value' => data_get($license, 'value'), + ], + ]); + } + $nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first(); + if ($nodeEnv) { + $data = $data->merge([ + 'Node Environment' => [ + 'key' => data_get($nodeEnv, 'key'), + 'value' => data_get($nodeEnv, 'value'), + ], + ]); + } + + $fields->put('Strapi', $data->toArray()); + break; + default: + $data = collect([]); + $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); + // Chaskiq + $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); + + $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); + if ($admin_user) { + $data = $data->merge([ + 'User' => [ + 'key' => data_get($admin_user, 'key'), + 'value' => data_get($admin_user, 'value', 'admin'), + 'readonly' => true, + 'rules' => 'required', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($admin_password, 'key'), + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + if ($admin_email) { + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($admin_email, 'key'), + 'value' => data_get($admin_email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + $fields->put('Admin', $data->toArray()); + break; } } $databases = $this->databases()->get(); foreach ($databases as $database) { - $image = str($database->image)->before(':')->value(); + $image = str($database->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)->contains('postgres'): + case $image->contains('postgres'): $userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL']; $passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL']; $dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB']; @@ -815,10 +995,10 @@ class Service extends BaseModel } $fields->put('PostgreSQL', $data->toArray()); break; - case str($image)->contains('mysql'): + case $image->contains('mysql'): $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER']; - $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT']; + $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT']; $dbNameVariables = ['MYSQL_DATABASE']; $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); @@ -865,7 +1045,7 @@ class Service extends BaseModel } $fields->put('MySQL', $data->toArray()); break; - case str($image)->contains('mariadb'): + case $image->contains('mariadb'): $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER']; $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD']; $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD']; @@ -928,6 +1108,7 @@ class Service extends BaseModel foreach ($fields as $field) { $key = data_get($field, 'key'); $value = data_get($field, 'value'); + ray($key, $value); $found = $this->environment_variables()->where('key', $key)->first(); if ($found) { $found->value = $value; @@ -1052,12 +1233,12 @@ class Service extends BaseModel public function environment_variables(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC"); + return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function environment_variables_preview(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); + return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function workdir() @@ -1108,7 +1289,6 @@ class Service extends BaseModel $real_value = escapeEnvVariables($env->real_value); } } - ray("echo \"{$env->key}={$real_value}\" >> .env"); $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; } } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index d312fab96..0e79e1e2e 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -112,4 +112,9 @@ class ServiceApplication extends BaseModel { getFilesystemVolumesFromServer($this, $isInit); } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 6b96738e8..927527118 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -115,4 +115,13 @@ class ServiceDatabase extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function isBackupSolutionAvailable() + { + return str($this->databaseType())->contains('mysql') || + str($this->databaseType())->contains('postgres') || + str($this->databaseType())->contains('postgis') || + str($this->databaseType())->contains('mariadb') || + str($this->databaseType())->contains('mongodb'); + } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index ee5c3becc..e4341b1b9 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -294,4 +294,9 @@ class StandaloneClickhouse extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 361abf110..94ab2d745 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -294,4 +294,9 @@ class StandaloneDragonfly extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index e05879371..335c8931c 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -294,4 +294,9 @@ class StandaloneKeydb extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index c1e6c85d7..c6c08dee5 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -294,4 +294,9 @@ class StandaloneMariadb extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index e5ed0a5f4..99893b1d1 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -314,4 +314,9 @@ class StandaloneMongodb extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index bd4a7abb7..f2a5b5c14 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -295,4 +295,9 @@ class StandaloneMysql extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index db771c7cd..1b18a5ca7 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -296,4 +296,9 @@ class StandalonePostgresql extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index c524d4d03..a5868e243 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -290,4 +290,9 @@ class StandaloneRedis extends BaseModel return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 006b095cf..d7c16b607 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -2,6 +2,7 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; +use App\Enums\StaticImageTypes; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -89,6 +90,7 @@ function sharedDataApplications() 'git_branch' => 'string', 'build_pack' => Rule::enum(BuildPackTypes::class), 'is_static' => 'boolean', + 'static_image' => Rule::enum(StaticImageTypes::class), 'domains' => 'string', 'redirect' => Rule::enum(RedirectTypes::class), 'git_commit_sha' => 'string', @@ -176,4 +178,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('github_app_uuid'); $request->offsetUnset('private_key_uuid'); $request->offsetUnset('use_build_server'); + $request->offsetUnset('is_static'); } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 1eeec8f94..303fcab8e 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -20,12 +20,16 @@ const RESTART_MODE = 'unless-stopped'; const DATABASE_DOCKER_IMAGES = [ 'bitnami/mariadb', 'bitnami/mongodb', - 'bitnami/mysql', - 'bitnami/postgresql', 'bitnami/redis', 'mysql', + 'bitnami/mysql', + 'mysql/mysql-server', 'mariadb', + 'postgis/postgis', 'postgres', + 'bitnami/postgresql', + 'supabase/postgres', + 'elestio/postgres', 'mongo', 'redis', 'memcached', @@ -33,10 +37,10 @@ const DATABASE_DOCKER_IMAGES = [ 'neo4j', 'influxdb', 'clickhouse/clickhouse-server', - 'supabase/postgres', ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', + 'minio/minio', 'svhd/logto', ]; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index e252bda10..397bce029 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -325,38 +325,20 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); - $basic_auth = false; - $basic_auth_middleware = null; - $redirect = false; - $redirect_middleware = null; + $middlewares_from_labels = collect([]); if ($serviceLabels) { - $basic_auth = $serviceLabels->contains(function ($value) { - return str_contains($value, 'basicauth'); - }); - if ($basic_auth) { - $basic_auth_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } - $redirect = $serviceLabels->contains(function ($value) { - return str_contains($value, 'redirectregex'); - }); - if ($redirect) { - $redirect_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } + $middlewares_from_labels = $serviceLabels->map(function ($item) { + if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) { + return $matches[1]; + } + if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { + return explode(',', $matches[1]); + } + return null; + })->flatten() + ->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { @@ -404,20 +386,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); } if ($path !== '/') { + // Middleware handling $middlewares = collect([]); - if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -425,10 +402,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -437,13 +417,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_gzip_enabled) { $middlewares->push('gzip'); - } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } + } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -455,6 +429,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -490,12 +467,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -507,6 +478,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -516,12 +490,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -533,6 +501,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 5d1ad5390..309ccee4a 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -164,6 +164,7 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', '8080:8080', ], 'healthcheck' => [ @@ -187,6 +188,7 @@ function generate_default_proxy_configuration(Server $server) '--entryPoints.http.http2.maxConcurrentStreams=50', '--entrypoints.https.http.encodequerysemicolons=true', '--entryPoints.https.http2.maxConcurrentStreams=50', + '--entrypoints.https.http3', '--providers.docker.exposedbydefault=false', '--providers.file.directory=/traefik/dynamic/', '--providers.file.watch=true', diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php index 4a2252016..2ee7bf44a 100644 --- a/bootstrap/helpers/s3.php +++ b/bootstrap/helpers/s3.php @@ -1,14 +1,11 @@ endpoint) { - $is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com'); - } + config()->set('filesystems.disks.custom-s3', [ 'driver' => 's3', 'region' => $s3['region'], @@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3) 'bucket' => $s3['bucket'], 'endpoint' => $s3['endpoint'], 'use_path_style_endpoint' => true, - 'bucket_endpoint' => $is_digital_ocean, + 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(), 'aws_url' => $s3->awsUrl(), ]); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index ffd53a99a..cfdea81fb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -522,6 +522,11 @@ function sslip(Server $server) function get_service_templates(bool $force = false): Collection { + if (isDev()) { + $services = File::get(base_path('templates/service-templates.json')); + + return collect(json_decode($services))->sortKeys(); + } if ($force) { try { $response = Http::retry(3, 1000)->get(config('constants.services.official')); @@ -708,7 +713,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -758,7 +765,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -824,6 +833,31 @@ function convertToArray($collection) return $collection; } +function parseCommandFromMagicEnvVariable(Str|string $key): Stringable +{ + $value = str($key); + $count = substr_count($value->value(), '_'); + if ($count === 2) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } else { + // SERVICE_BASE64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + if ($count === 3) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI_1000 + $command = $value->after('SERVICE_')->before('_'); + } else { + // SERVICE_BASE64_64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + + return str($command); +} function parseEnvVariable(Str|string $value) { $value = str($value); @@ -855,6 +889,7 @@ function parseEnvVariable(Str|string $value) } else { // SERVICE_BASE64_64_UMAMI $command = $value->after('SERVICE_')->beforeLast('_'); + ray($command); } } } @@ -1139,10 +1174,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); } } } @@ -1158,10 +1193,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); } } } @@ -1184,14 +1219,16 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { - if (! str(trim($line))->startsWith([ - 'cd', - 'command', - 'echo', - 'true', - 'if', - 'fi', - ])) { + if ( + ! str(trim($line))->startsWith([ + 'cd', + 'command', + 'echo', + 'true', + 'if', + 'fi', + ]) + ) { return "sudo $line"; } @@ -1606,7 +1643,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -2521,7 +2560,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -2982,11 +3023,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $predefinedPort = '8000'; } if ($isDatabase) { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, @@ -3096,7 +3148,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int foreach ($magicEnvironments as $key => $value) { $key = str($key); $value = replaceVariables($value); - $command = $key->after('SERVICE_')->before('_'); + $command = parseCommandFromMagicEnvVariable($key); $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first(); if ($found) { continue; @@ -3129,6 +3181,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } elseif ($isService) { $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); } + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ 'key' => $key->value(), $nameOfId => $resource->id, @@ -3207,12 +3260,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($serviceName === 'plausible') { $predefinedPort = '8000'; } + if ($isDatabase) { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, @@ -3643,6 +3708,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int }); } $serviceLabels = $labels->merge($defaultLabels); + if ($serviceLabels->count() > 0) { + if ($isApplication) { + $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); + } else { + $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); + } + if ($isContainerLabelEscapeEnabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if ($isApplication) { $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; @@ -3819,6 +3896,8 @@ function isAssociativeArray($array) */ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|Application|Service $resource, Collection &$where_to_add, ?Collection $where_to_check = null) { + // Currently disabled + return; if ($resource instanceof Service) { $ip = $resource->server->ip; } else { @@ -3863,14 +3942,19 @@ function convertComposeEnvironmentToArray($environment) { $convertedServiceVariables = collect([]); if (isAssociativeArray($environment)) { + // Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux']; if ($environment instanceof Collection) { $changedEnvironment = collect([]); $environment->each(function ($value, $key) use ($changedEnvironment) { - $parts = explode('=', $value, 2); - if (count($parts) === 2) { - $key = $parts[0]; - $realValue = $parts[1] ?? ''; - $changedEnvironment->put($key, $realValue); + if (is_numeric($key)) { + $parts = explode('=', $value, 2); + if (count($parts) === 2) { + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + $changedEnvironment->put($key, $realValue); + } else { + $changedEnvironment->put($key, $value); + } } else { $changedEnvironment->put($key, $value); } @@ -3880,12 +3964,15 @@ function convertComposeEnvironmentToArray($environment) } $convertedServiceVariables = $environment; } else { + // Example: $environment = ['FOO=bar', 'BAZ=qux']; foreach ($environment as $value) { - $parts = explode('=', $value, 2); - $key = $parts[0]; - $realValue = $parts[1] ?? ''; - if ($key) { - $convertedServiceVariables->put($key, $realValue); + if (is_string($value)) { + $parts = explode('=', $value, 2); + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + if ($key) { + $convertedServiceVariables->put($key, $realValue); + } } } } @@ -3897,3 +3984,31 @@ function instanceSettings() { return InstanceSettings::get(); } + +function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) { + + $server = Server::find($server_id)->where('team_id', $team_id)->first(); + if (!$server) { + return; + } + $uuid = new Cuid2(); + $cloneCommand = "git clone --no-checkout -b $branch $repository ."; + $workdir = rtrim($base_directory, '/'); + $fileList = collect([".$workdir/coolify.json"]); + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir/coolify.json", + 'rm -rf /tmp/{$uuid}', + ]); + try { + return instant_remote_process($commands, $server); + } catch (\Exception $e) { + // continue + } +} diff --git a/composer.json b/composer.json index 03adf9823..fbd77d0cf 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "guzzlehttp/guzzle": "^7.5.0", "laravel/fortify": "^v1.16.0", "laravel/framework": "^v11", - "laravel/horizon": "^5.27.1", + "laravel/horizon": "^5.29.1", "laravel/prompts": "^0.1.6", "laravel/sanctum": "^v4.0", "laravel/socialite": "^v5.14.0", diff --git a/composer.lock b/composer.lock index 420d87ec0..0b8da82d0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42c28ab141b70fcabf75b51afa96c670", + "content-hash": "c47adf3684eb727e22503937435c0914", "packages": [ { "name": "amphp/amp", @@ -317,16 +317,16 @@ }, { "name": "amphp/parallel", - "version": "v2.2.9", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238" + "reference": "9777db1460d1535bc2a843840684fb1205225b87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/73d293f1fc4df1bebc3c4fce1432e82dd7032238", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238", + "url": "https://api.github.com/repos/amphp/parallel/zipball/9777db1460d1535bc2a843840684fb1205225b87", + "reference": "9777db1460d1535bc2a843840684fb1205225b87", "shasum": "" }, "require": { @@ -389,7 +389,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.2.9" + "source": "https://github.com/amphp/parallel/tree/v2.3.0" }, "funding": [ { @@ -397,7 +397,7 @@ "type": "github" } ], - "time": "2024-03-24T18:27:44+00:00" + "time": "2024-09-14T19:16:14+00:00" }, { "name": "amphp/parser", @@ -921,16 +921,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.321.9", + "version": "3.324.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a" + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", - "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b258712f0d986e00e1143d55246b6f9e344c7184", + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184", "shasum": "" }, "require": { @@ -1013,22 +1013,22 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.321.9" + "source": "https://github.com/aws/aws-sdk-php/tree/3.324.0" }, - "time": "2024-09-11T18:15:49+00:00" + "time": "2024-10-10T18:06:36+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41" + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", "shasum": "" }, "require": { @@ -1067,9 +1067,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" }, - "time": "2024-04-18T11:16:25+00:00" + "time": "2024-10-01T13:55:55+00:00" }, { "name": "brick/math", @@ -1518,16 +1518,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.1", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -1543,7 +1543,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -1611,7 +1611,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.1" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -1627,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-09-01T13:49:23+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -1937,16 +1937,16 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.3", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" + "reference": "8c784d071debd117328803d86b2097615b457500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", "shasum": "" }, "require": { @@ -1959,10 +1959,14 @@ "require-dev": { "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.0", - "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { "Cron\\": "src/Cron/" @@ -1986,7 +1990,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" }, "funding": [ { @@ -1994,7 +1998,7 @@ "type": "github" } ], - "time": "2023-08-10T19:36:49+00:00" + "time": "2024-10-09T13:47:03+00:00" }, { "name": "egulias/email-validator", @@ -2789,16 +2793,16 @@ }, { "name": "laravel/fortify", - "version": "v1.24.1", + "version": "v1.24.2", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20" + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/8158ba0960bb5f4aae509d01d74a95e16e30de20", - "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20", + "url": "https://api.github.com/repos/laravel/fortify/zipball/42695c45087e5abb3e173725b4f1ef4956a7b47d", + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d", "shasum": "" }, "require": { @@ -2850,20 +2854,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-09-03T10:02:14+00:00" + "time": "2024-09-16T19:20:52+00:00" }, { "name": "laravel/framework", - "version": "v11.23.2", + "version": "v11.27.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3" + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", - "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", + "url": "https://api.github.com/repos/laravel/framework/zipball/a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", "shasum": "" }, "require": { @@ -2882,7 +2886,7 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.18", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", @@ -2968,7 +2972,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.4.0", + "orchestra/testbench-core": "^9.5", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", @@ -3027,6 +3031,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -3058,20 +3063,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-09-11T21:59:23+00:00" + "time": "2024-10-09T04:17:35+00:00" }, { "name": "laravel/horizon", - "version": "v5.28.1", + "version": "v5.29.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f" + "reference": "9f482f21c23ed01c2366d1157843165165579c23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", - "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", + "url": "https://api.github.com/repos/laravel/horizon/zipball/9f482f21c23ed01c2366d1157843165165579c23", + "reference": "9f482f21c23ed01c2366d1157843165165579c23", "shasum": "" }, "require": { @@ -3135,9 +3140,9 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.28.1" + "source": "https://github.com/laravel/horizon/tree/v5.29.1" }, - "time": "2024-09-04T14:06:50+00:00" + "time": "2024-10-08T18:23:02+00:00" }, { "name": "laravel/prompts", @@ -3199,16 +3204,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.0.2", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1" + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1", - "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab", + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab", "shasum": "" }, "require": { @@ -3259,20 +3264,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2024-04-10T19:39:58+00:00" + "time": "2024-09-27T14:55:41+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.3.4", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81" + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/61b87392d986dc49ad5ef64e75b1ff5fee24ef81", - "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", "shasum": "" }, "require": { @@ -3320,7 +3325,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2024-08-02T07:48:17+00:00" + "time": "2024-09-23T13:33:08+00:00" }, { "name": "laravel/socialite", @@ -3465,16 +3470,16 @@ }, { "name": "laravel/tinker", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe" + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5", + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "shasum": "" }, "require": { @@ -3525,9 +3530,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.9.0" + "source": "https://github.com/laravel/tinker/tree/v2.10.0" }, - "time": "2024-01-04T16:10:04+00:00" + "time": "2024-09-23T13:32:56+00:00" }, { "name": "laravel/ui", @@ -3594,38 +3599,38 @@ }, { "name": "lcobucci/jwt", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051", + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051", "shasum": "" }, "require": { "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.27.0", - "lcobucci/clock": "^3.0", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2.9", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.10.7", "phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.2.6" + "phpunit/phpunit": "^11.1" }, "suggest": { - "lcobucci/clock": ">= 3.0" + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -3651,7 +3656,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.0" }, "funding": [ { @@ -3663,7 +3668,7 @@ "type": "patreon" } ], - "time": "2024-04-11T23:07:54+00:00" + "time": "2024-10-08T22:06:45+00:00" }, { "name": "league/commonmark", @@ -3855,16 +3860,16 @@ }, { "name": "league/flysystem", - "version": "3.28.0", + "version": "3.29.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c" + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", "shasum": "" }, "require": { @@ -3932,22 +3937,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" }, - "time": "2024-05-22T10:09:12+00:00" + "time": "2024-10-08T08:58:34+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8" + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/22071ef1604bc776f5ff2468ac27a752514665c8", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", "shasum": "" }, "require": { @@ -3987,22 +3992,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-17T13:10:48+00:00" }, { "name": "league/flysystem-local", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40" + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", "shasum": "" }, "require": { @@ -4036,22 +4041,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-09T21:24:39+00:00" }, { "name": "league/flysystem-sftp-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41" + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/abedadd3c64d4f0e276d6ecc796ec8194d136b41", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252", + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252", "shasum": "" }, "require": { @@ -4085,22 +4090,22 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-14T19:35:54+00:00" }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", "shasum": "" }, "require": { @@ -4131,7 +4136,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -4143,7 +4148,7 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "league/oauth1-client", @@ -4955,24 +4960,24 @@ }, { "name": "nette/schema", - "version": "v1.3.0", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.3" + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.4", + "nette/tester": "^2.5.2", "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.8" }, @@ -5011,9 +5016,9 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.0" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2023-12-11T11:54:22+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", @@ -5103,16 +5108,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -5155,9 +5160,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-07-01T20:03:41+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nubs/random-name-generator", @@ -5897,16 +5902,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.41", + "version": "3.0.42", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb" + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/621c73f7dcb310b61de34d1da4c4204e8ace6ceb", - "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", "shasum": "" }, "require": { @@ -5987,7 +5992,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.41" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" }, "funding": [ { @@ -6003,20 +6008,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T00:13:54+00:00" + "time": "2024-09-16T03:06:04+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.1", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -6048,22 +6053,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-09-07T20:13:05+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.3", + "version": "1.12.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009" + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0fcbf194ab63d8159bb70d9aa3e1350051632009", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", "shasum": "" }, "require": { @@ -6108,7 +6113,7 @@ "type": "github" } ], - "time": "2024-09-09T08:10:35+00:00" + "time": "2024-10-06T15:03:59+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6814,16 +6819,16 @@ }, { "name": "purplepixie/phpdns", - "version": "2.1.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/purplepixie/phpdns.git", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad" + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/18cd3a43fadcfd16e2789e3c78a264945f6cbfad", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad", + "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", "shasum": "" }, "require": { @@ -6856,9 +6861,9 @@ "description": "PHP DNS Direct Query Module", "support": { "issues": "https://github.com/purplepixie/phpdns/issues", - "source": "https://github.com/purplepixie/phpdns/tree/2.1.1" + "source": "https://github.com/purplepixie/phpdns/tree/2.2.0" }, - "time": "2024-05-27T13:27:50+00:00" + "time": "2024-09-26T14:39:58+00:00" }, { "name": "pusher/pusher-php-server", @@ -7148,21 +7153,21 @@ }, { "name": "rector/rector", - "version": "1.2.5", + "version": "1.2.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339" + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/e98aa793ca3fcd17e893cfaf9103ac049775d339", - "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.12.2" + "phpstan/phpstan": "^1.12.5" }, "conflict": { "rector/rector-doctrine": "*", @@ -7195,7 +7200,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.5" + "source": "https://github.com/rectorphp/rector/tree/1.2.6" }, "funding": [ { @@ -7203,7 +7208,7 @@ "type": "github" } ], - "time": "2024-09-08T17:43:24+00:00" + "time": "2024-10-03T08:56:44+00:00" }, { "name": "resend/resend-laravel", @@ -7494,16 +7499,16 @@ }, { "name": "sentry/sentry-laravel", - "version": "4.8.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7" + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3", + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3", "shasum": "" }, "require": { @@ -7567,7 +7572,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.8.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0" }, "funding": [ { @@ -7579,7 +7584,7 @@ "type": "custom" } ], - "time": "2024-08-15T19:03:01+00:00" + "time": "2024-09-19T12:58:53+00:00" }, { "name": "socialiteproviders/manager", @@ -8580,16 +8585,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -8653,7 +8658,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -8669,7 +8674,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/css-selector", @@ -9100,16 +9105,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a" + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", "shasum": "" }, "require": { @@ -9157,7 +9162,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.1.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" }, "funding": [ { @@ -9173,20 +9178,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747" + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6efcbd1b3f444f631c386504fc83eeca25963747", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b", + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b", "shasum": "" }, "require": { @@ -9271,7 +9276,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.1.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.5" }, "funding": [ { @@ -9287,20 +9292,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T17:02:28+00:00" + "time": "2024-09-21T06:09:21+00:00" }, { "name": "symfony/mailer", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee" + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/8fcff0af9043c8f8a8e229437cea363e282f9aee", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b", + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b", "shasum": "" }, "require": { @@ -9351,7 +9356,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.1.2" + "source": "https://github.com/symfony/mailer/tree/v7.1.5" }, "funding": [ { @@ -9367,20 +9372,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T08:00:31+00:00" + "time": "2024-09-08T12:32:26+00:00" }, { "name": "symfony/mime", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758" + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758", + "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", "shasum": "" }, "require": { @@ -9435,7 +9440,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.4" + "source": "https://github.com/symfony/mime/tree/v7.1.5" }, "funding": [ { @@ -9451,7 +9456,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:28:19+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/options-resolver", @@ -10238,16 +10243,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -10279,7 +10284,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -10295,7 +10300,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -10608,16 +10613,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -10675,7 +10680,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -10691,20 +10696,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/translation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1" + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/8d5e50c813ba2859a6dfc99a0765c550507934a1", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1", + "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea", + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea", "shasum": "" }, "require": { @@ -10769,7 +10774,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.1.3" + "source": "https://github.com/symfony/translation/tree/v7.1.5" }, "funding": [ { @@ -10785,7 +10790,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-16T06:30:38+00:00" }, { "name": "symfony/translation-contracts", @@ -10867,16 +10872,16 @@ }, { "name": "symfony/uid", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf" + "reference": "8c7bb8acb933964055215d89f9a9871df0239317" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/82177535395109075cdb45a70533aa3d7a521cdf", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf", + "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317", + "reference": "8c7bb8acb933964055215d89f9a9871df0239317", "shasum": "" }, "require": { @@ -10921,7 +10926,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.1.4" + "source": "https://github.com/symfony/uid/tree/v7.1.5" }, "funding": [ { @@ -10937,20 +10942,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa" + "reference": "e20e03889539fd4e4211e14d2179226c513c010d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5fa7481b199090964d6fd5dab6294d5a870c7aa", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", + "reference": "e20e03889539fd4e4211e14d2179226c513c010d", "shasum": "" }, "require": { @@ -11004,7 +11009,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" }, "funding": [ { @@ -11020,20 +11025,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:12:47+00:00" + "time": "2024-09-16T10:07:02+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "be37e7f13195e05ab84ca5269365591edd240335" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/be37e7f13195e05ab84ca5269365591edd240335", - "reference": "be37e7f13195e05ab84ca5269365591edd240335", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -11076,7 +11081,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.11" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -11092,7 +11097,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:55:28+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11742,16 +11747,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.10.6", + "version": "4.11.0", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6" + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e462ff5269ea0ec91070edd5d51dc7215bdea3b6", - "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", "shasum": "" }, "require": { @@ -11765,7 +11770,7 @@ "require-dev": { "composer/package-versions-deprecated": "^1.11", "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", + "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0", "phpstan/phpstan": "^1.6", "phpunit/phpunit": ">=8", "vimeo/psalm": "^4.23" @@ -11817,31 +11822,31 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.10.6" + "source": "https://github.com/zircote/swagger-php/tree/4.11.0" }, - "time": "2024-07-26T03:04:43+00:00" + "time": "2024-10-09T03:11:12+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.13.5", + "version": "v3.14.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07" + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", "shasum": "" }, "require": { "illuminate/routing": "^9|^10|^11", "illuminate/session": "^9|^10|^11", "illuminate/support": "^9|^10|^11", - "maximebf/debugbar": "~1.22.0", + "maximebf/debugbar": "~1.23.0", "php": "^8.0", "symfony/finder": "^6|^7" }, @@ -11854,7 +11859,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.13-dev" + "dev-master": "3.14-dev" }, "laravel": { "providers": [ @@ -11893,7 +11898,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.3" }, "funding": [ { @@ -11905,7 +11910,7 @@ "type": "github" } ], - "time": "2024-04-12T11:20:37+00:00" + "time": "2024-10-02T09:17:49+00:00" }, { "name": "brianium/paratest", @@ -12127,26 +12132,26 @@ }, { "name": "filp/whoops", - "version": "2.15.4", + "version": "2.16.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", + "php": "^7.1 || ^8.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -12186,7 +12191,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.4" + "source": "https://github.com/filp/whoops/tree/2.16.0" }, "funding": [ { @@ -12194,7 +12199,7 @@ "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12249,16 +12254,16 @@ }, { "name": "laravel/dusk", - "version": "v8.2.5", + "version": "v8.2.8", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad" + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/e641800393ce4ad39f0a47133f51aae67ceb01ad", - "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad", + "url": "https://api.github.com/repos/laravel/dusk/zipball/5bff1e8dd87ec653a2202475377152e5d14fde40", + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40", "shasum": "" }, "require": { @@ -12315,22 +12320,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v8.2.5" + "source": "https://github.com/laravel/dusk/tree/v8.2.8" }, - "time": "2024-08-26T12:34:33+00:00" + "time": "2024-10-04T14:02:20+00:00" }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -12383,20 +12388,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T17:22:50+00:00" }, { "name": "maximebf/debugbar", - "version": "v1.22.5", + "version": "v1.23.2", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989" + "reference": "689720d724c771ac4add859056744b7b3f2406da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", + "reference": "689720d724c771ac4add859056744b7b3f2406da", "shasum": "" }, "require": { @@ -12418,7 +12423,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } }, "autoload": { @@ -12449,9 +12454,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" }, - "time": "2024-09-09T08:05:55+00:00" + "time": "2024-09-16T11:23:09+00:00" }, { "name": "mockery/mockery", @@ -14892,16 +14897,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65" + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", "shasum": "" }, "require": { @@ -14965,7 +14970,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.11" + "source": "https://github.com/symfony/http-client/tree/v6.4.12" }, "funding": [ { @@ -14981,7 +14986,7 @@ "type": "tidelift" } ], - "time": "2024-08-26T06:30:21+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/sentry.php b/config/sentry.php index 5e091b3ec..ade6923ac 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,8 @@ 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.349', + 'release' => '4.0.0-beta.360', + // 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 91c58fdcd..5639fc8a8 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('dump_all')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->dropColumn('dump_all'); + }); + } +}; diff --git a/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php new file mode 100644 index 000000000..feb144de6 --- /dev/null +++ b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php @@ -0,0 +1,34 @@ +dropUnique(['fqdn']); + }); + Schema::table('applications', function (Blueprint $table) { + $table->dropUnique(['fqdn']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + Schema::table('applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + } +}; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5df6aa5d6..f1da567fc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -58,6 +58,7 @@ services: SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" + entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] vite: image: node:20 pull_policy: always diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 95031deab..b15a109c3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -113,7 +113,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index 9a7a68376..f0d6db906 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -1,9 +1,27 @@ FROM quay.io/soketi/soketi:1.6-16-alpine + +ARG TARGETPLATFORM +# https://github.com/cloudflare/cloudflared/releases +ARG CLOUDFLARED_VERSION=2024.4.1 + WORKDIR /terminal -RUN apk add --no-cache openssh-client make g++ python3 +RUN apk add --no-cache openssh-client make g++ python3 curl COPY docker/coolify-realtime/package.json ./ RUN npm i RUN npm rebuild node-pty --update-binary COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js + +RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ + echo 'amd64' && \ + curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" + +RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ + echo 'arm64' && \ + curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ + ;fi" + + + ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] diff --git a/docker/coolify-realtime/soketi-entrypoint.sh b/docker/coolify-realtime/soketi-entrypoint.sh index 3f1f0dc8c..3bb85bdeb 100644 --- a/docker/coolify-realtime/soketi-entrypoint.sh +++ b/docker/coolify-realtime/soketi-entrypoint.sh @@ -1,11 +1,19 @@ #!/bin/sh # Function to timestamp logs + +# Check if the first argument is 'watch' +if [ "$1" = "watch" ]; then + WATCH_MODE="--watch" +else + WATCH_MODE="" +fi + timestamp() { date "+%Y-%m-%d %H:%M:%S" } # Start the terminal server in the background with logging -node /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & TERMINAL_PID=$! # Start the Soketi process in the background with logging diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js index 8658ecdf8..6633204b2 100755 --- a/docker/coolify-realtime/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -61,9 +61,13 @@ wss.on('connection', (ws) => { const userSession = { ws, userId, ptyProcess: null, isActive: false }; userSessions.set(userId, userSession); - ws.on('message', (message) => handleMessage(userSession, message)); + ws.on('message', (message) => { + handleMessage(userSession, message); + + }); ws.on('error', (err) => handleError(err, userId)); ws.on('close', () => handleClose(userId)); + }); const messageHandlers = { @@ -108,7 +112,6 @@ function parseMessage(message) { async function handleCommand(ws, command, userId) { const userSession = userSessions.get(userId); - if (userSession && userSession.isActive) { const result = await killPtyProcess(userId); if (!result) { @@ -127,6 +130,7 @@ async function handleCommand(ws, command, userId) { cols: 80, rows: 30, cwd: process.env.HOME, + env: {}, }; // NOTE: - Initiates a process within the Terminal container @@ -139,13 +143,16 @@ async function handleCommand(ws, command, userId) { ws.send('pty-ready'); - ptyProcess.onData((data) => ws.send(data)); + ptyProcess.onData((data) => { + ws.send(data); + }); // when parent closes ptyProcess.onExit(({ exitCode, signal }) => { console.error(`Process exited with code ${exitCode} and signal ${signal}`); ws.send('pty-exited'); userSession.isActive = false; + }); if (timeout) { @@ -179,7 +186,7 @@ async function killPtyProcess(userId) { // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 - session.ptyProcess.write('kill -TERM -$$ && exit\n'); + session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n'); setTimeout(() => { if (!session.isActive || !session.ptyProcess) { @@ -228,5 +235,5 @@ function extractHereDocContent(commandString) { } server.listen(6002, () => { - console.log('Server listening on port 6002'); + console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!'); }); diff --git a/lang/fr.json b/lang/fr.json index ae7fa0a03..dbd5a1bf7 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,30 +1,37 @@ { - "auth.login": "Connexion", - "auth.login.azure": "Connexion avec Microsoft", - "auth.login.bitbucket": "Connexion avec Bitbucket", - "auth.login.github": "Connexion avec GitHub", - "auth.login.gitlab": "Connexion avec Gitlab", - "auth.login.google": "Connexion avec Google", - "auth.already_registered": "Déjà enregistré ?", - "auth.confirm_password": "Confirmer le mot de passe", - "auth.forgot_password": "Mot de passe oublié", - "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", - "auth.register_now": "S'enregistrer", - "auth.logout": "Déconnexion", - "auth.register": "S'enregistrer", - "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administateur.", - "auth.reset_password": "Réinitialiser le mot de passe", - "auth.failed": "Aucune correspondance n'a été trouvé pour les informations d'identification renseignées.", - "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", - "auth.failed.password": "Le mot de passe renseigné est incorrect.", - "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", - "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", - "input.name": "Nom", - "input.email": "Email", - "input.password": "Mot de passe", - "input.password.again": "Mot de passe identique", - "input.code": "Code à usage unique", - "input.recovery_code": "Code de récupération", - "button.save": "Sauvegarder", - "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée." + "auth.login": "Connexion", + "auth.login.azure": "Connexion avec Microsoft", + "auth.login.bitbucket": "Connexion avec Bitbucket", + "auth.login.github": "Connexion avec GitHub", + "auth.login.gitlab": "Connexion avec Gitlab", + "auth.login.google": "Connexion avec Google", + "auth.already_registered": "Déjà enregistré ?", + "auth.confirm_password": "Confirmer le mot de passe", + "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", + "auth.register_now": "S'enregistrer", + "auth.logout": "Déconnexion", + "auth.register": "S'enregistrer", + "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administrateur.", + "auth.reset_password": "Réinitialiser le mot de passe", + "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.", + "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", + "auth.failed.password": "Le mot de passe renseigné est incorrect.", + "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", + "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", + "input.name": "Nom", + "input.email": "Email", + "input.password": "Mot de passe", + "input.password.again": "Mot de passe identique", + "input.code": "Code à usage unique", + "input.recovery_code": "Code de récupération", + "button.save": "Sauvegarder", + "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée.", + "service.stop": "Ce service sera arrêté.", + "resource.docker_cleanup": "Exécuter le nettoyage Docker (supprimer les images inutilisées et le cache du builder).", + "resource.non_persistent": "Toutes les données non persistantes seront supprimées.", + "resource.delete_volumes": "Supprimer définitivement tous les volumes associés à cette ressource.", + "resource.delete_connected_networks": "Supprimer définitivement tous les réseaux non-prédéfinis associés à cette ressource.", + "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.", + "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local." } diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 29f6a0f82..b15a109c3 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -46,6 +46,9 @@ services: - PUSHER_APP_ID - PUSHER_APP_KEY - PUSHER_APP_SECRET + - TERMINAL_PROTOCOL + - TERMINAL_HOST + - TERMINAL_PORT - AUTOUPDATE - SELF_HOSTED - SSH_MUX_ENABLED @@ -110,7 +113,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/versions.json b/other/nightly/versions.json index a86555e95..c04a3dee6 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.349" + "version": "4.0.0-beta.354" }, "nightly": { - "version": "4.0.0-beta.350" + "version": "4.0.0-beta.355" }, "helper": { - "version": "1.0.1" + "version": "1.0.2" }, "realtime": { - "version": "1.0.2" + "version": "1.0.3" } } } diff --git a/package-lock.json b/package-lock.json index 8f6fbde08..adb1dc65a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", @@ -960,10 +960,9 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.0.tgz", + "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==", "engines": { "node": ">= 0.6" } diff --git a/package.json b/package.json index 3633a7ed1..29f8f1a37 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", diff --git a/public/svgs/anythingllm.svg b/public/svgs/anythingllm.svg new file mode 100644 index 000000000..1c25f8711 --- /dev/null +++ b/public/svgs/anythingllm.svg @@ -0,0 +1,166 @@ + + + + + + + diff --git a/public/svgs/argilla.png b/public/svgs/argilla.png new file mode 100644 index 000000000..3ead32785 Binary files /dev/null and b/public/svgs/argilla.png differ diff --git a/public/svgs/audiobookshelf.svg b/public/svgs/audiobookshelf.svg new file mode 100644 index 000000000..d641b765b --- /dev/null +++ b/public/svgs/audiobookshelf.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/azimutt.png b/public/svgs/azimutt.png new file mode 100644 index 000000000..ef69062cd Binary files /dev/null and b/public/svgs/azimutt.png differ diff --git a/public/svgs/bitcoin.svg b/public/svgs/bitcoin.svg new file mode 100644 index 000000000..2b75c99bc --- /dev/null +++ b/public/svgs/bitcoin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/public/svgs/bookstack.png b/public/svgs/bookstack.png new file mode 100644 index 000000000..d10b3ca43 Binary files /dev/null and b/public/svgs/bookstack.png differ diff --git a/public/svgs/castopod.svg b/public/svgs/castopod.svg new file mode 100644 index 000000000..c73008400 --- /dev/null +++ b/public/svgs/castopod.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/svgs/clickhouse.svg b/public/svgs/clickhouse.svg new file mode 100644 index 000000000..d536536de --- /dev/null +++ b/public/svgs/clickhouse.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/coolify.png b/public/svgs/coolify.png new file mode 100644 index 000000000..fa01fec05 Binary files /dev/null and b/public/svgs/coolify.png differ diff --git a/public/svgs/dozzle.svg b/public/svgs/dozzle.svg new file mode 100644 index 000000000..bf88e8729 --- /dev/null +++ b/public/svgs/dozzle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/svgs/easyappointments.png b/public/svgs/easyappointments.png new file mode 100644 index 000000000..8f00d3353 Binary files /dev/null and b/public/svgs/easyappointments.png differ diff --git a/public/svgs/forgejo.svg b/public/svgs/forgejo.svg new file mode 100644 index 000000000..804b05e28 --- /dev/null +++ b/public/svgs/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/getoutline.jpeg b/public/svgs/getoutline.jpeg new file mode 100644 index 000000000..422e402f7 Binary files /dev/null and b/public/svgs/getoutline.jpeg differ diff --git a/public/svgs/homarr.svg b/public/svgs/homarr.svg new file mode 100644 index 000000000..4d4289604 --- /dev/null +++ b/public/svgs/homarr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/infisical.png b/public/svgs/infisical.png new file mode 100644 index 000000000..48eddae78 Binary files /dev/null and b/public/svgs/infisical.png differ diff --git a/public/svgs/it-tools.svg b/public/svgs/it-tools.svg new file mode 100644 index 000000000..3de5ecff7 --- /dev/null +++ b/public/svgs/it-tools.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svgs/joplin.png b/public/svgs/joplin.png new file mode 100644 index 000000000..d17a1d2c1 Binary files /dev/null and b/public/svgs/joplin.png differ diff --git a/public/svgs/keycloak.svg b/public/svgs/keycloak.svg new file mode 100644 index 000000000..849ac2759 --- /dev/null +++ b/public/svgs/keycloak.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/labelstudio.png b/public/svgs/labelstudio.png new file mode 100644 index 000000000..afa5160b9 Binary files /dev/null and b/public/svgs/labelstudio.png differ diff --git a/public/svgs/langfuse.png b/public/svgs/langfuse.png new file mode 100644 index 000000000..8dec0fe4a Binary files /dev/null and b/public/svgs/langfuse.png differ diff --git a/public/svgs/libreoffice.svg b/public/svgs/libreoffice.svg new file mode 100644 index 000000000..227471bef --- /dev/null +++ b/public/svgs/libreoffice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/litellm.svg b/public/svgs/litellm.svg new file mode 100644 index 000000000..01830c3f6 --- /dev/null +++ b/public/svgs/litellm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/mattermost.svg b/public/svgs/mattermost.svg new file mode 100644 index 000000000..b01d38eb7 --- /dev/null +++ b/public/svgs/mattermost.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/svgs/mautic.svg b/public/svgs/mautic.svg new file mode 100644 index 000000000..b528f72ef --- /dev/null +++ b/public/svgs/mautic.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/public/svgs/nitropage.svg b/public/svgs/nitropage.svg new file mode 100644 index 000000000..67b2df17f --- /dev/null +++ b/public/svgs/nitropage.svg @@ -0,0 +1,8 @@ + + + NP + + + + + diff --git a/public/svgs/ollama.svg b/public/svgs/ollama.svg new file mode 100644 index 000000000..3df9a9fba --- /dev/null +++ b/public/svgs/ollama.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/svgs/onedev.svg b/public/svgs/onedev.svg new file mode 100644 index 000000000..fb9c9c060 --- /dev/null +++ b/public/svgs/onedev.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/organizr.png b/public/svgs/organizr.png new file mode 100644 index 000000000..92541ea72 Binary files /dev/null and b/public/svgs/organizr.png differ diff --git a/public/svgs/paperless.svg b/public/svgs/paperless.svg new file mode 100644 index 000000000..347b1e759 --- /dev/null +++ b/public/svgs/paperless.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/public/svgs/postgresql.svg b/public/svgs/postgresql.svg new file mode 100644 index 000000000..1ff223856 --- /dev/null +++ b/public/svgs/postgresql.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/prefect.png b/public/svgs/prefect.png new file mode 100644 index 000000000..2f87ec0d7 Binary files /dev/null and b/public/svgs/prefect.png differ diff --git a/public/svgs/qdrant.png b/public/svgs/qdrant.png new file mode 100644 index 000000000..ecb2a56d5 Binary files /dev/null and b/public/svgs/qdrant.png differ diff --git a/public/svgs/searxng.svg b/public/svgs/searxng.svg new file mode 100644 index 000000000..b94fe3728 --- /dev/null +++ b/public/svgs/searxng.svg @@ -0,0 +1,56 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/public/svgs/soketi.jpeg b/public/svgs/soketi.jpeg new file mode 100644 index 000000000..00c8d82cd Binary files /dev/null and b/public/svgs/soketi.jpeg differ diff --git a/public/svgs/strapi.svg b/public/svgs/strapi.svg new file mode 100644 index 000000000..d1a71abc2 --- /dev/null +++ b/public/svgs/strapi.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/svgs/supertokens.svg b/public/svgs/supertokens.svg new file mode 100644 index 000000000..30b435bd0 --- /dev/null +++ b/public/svgs/supertokens.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/svgs/tolgee.svg b/public/svgs/tolgee.svg new file mode 100644 index 000000000..0f216e0c6 --- /dev/null +++ b/public/svgs/tolgee.svg @@ -0,0 +1,5 @@ + + + + diff --git a/public/svgs/unstructured.png b/public/svgs/unstructured.png new file mode 100644 index 000000000..a6ec855b6 Binary files /dev/null and b/public/svgs/unstructured.png differ diff --git a/public/svgs/weaviate.png b/public/svgs/weaviate.png new file mode 100644 index 000000000..134294253 Binary files /dev/null and b/public/svgs/weaviate.png differ diff --git a/resources/js/terminal.js b/resources/js/terminal.js index fb69628c0..59c9a79a8 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -16,6 +16,7 @@ export function initializeTerminalComponent() { paused: false, MAX_PENDING_WRITES: 5, keepAliveInterval: null, + reconnectInterval: null, init() { this.setupTerminal(); @@ -48,6 +49,9 @@ export function initializeTerminalComponent() { document.addEventListener(event, () => { this.checkIfProcessIsRunningAndKillIt(); clearInterval(this.keepAliveInterval); + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } }, { once: true }); }); @@ -103,11 +107,27 @@ export function initializeTerminalComponent() { }; this.socket.onclose = () => { console.log('WebSocket connection closed'); - + this.reconnect(); }; } }, + reconnect() { + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } + this.reconnectInterval = setInterval(() => { + console.log('Attempting to reconnect...'); + this.initializeWebSocket(); + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + console.log('Reconnected successfully'); + clearInterval(this.reconnectInterval); + this.reconnectInterval = null; + window.location.reload(); + } + }, 2000); + }, + handleSocketMessage(event) { this.message = '(connection closed)'; if (event.data === 'pty-ready') { @@ -164,10 +184,6 @@ export function initializeTerminalComponent() { // Copy and paste functionality this.term.attachCustomKeyEventHandler((arg) => { if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") { - navigator.clipboard.readText() - .then(text => { - this.socket.send(JSON.stringify({ message: text })); - }); return false; } diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index d82a046d3..ef6c477f2 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -222,12 +222,12 @@ @endforeach - @if ($confirmWithText && $confirmationText) + @if ($confirmWithText)

Confirm Actions

{{ $confirmationLabel }}

-
@endif @@ -272,8 +272,10 @@ class="block text-sm font-medium text-gray-700 dark:text-gray-300"> Your Password - +
+ +

@error('password')

{{ $message }}

@@ -296,20 +298,13 @@