diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml new file mode 100644 index 000000000..33e048627 --- /dev/null +++ b/.github/workflows/coolify-realtime-next.yml @@ -0,0 +1,103 @@ +name: Coolify Realtime Development (v4) + +on: + push: + branches: [ "next" ] + paths: + - .github/workflows/coolify-realtime.yml + - docker/coolify-realtime/Dockerfile + - docker/coolify-realtime/terminal-server.js + - docker/coolify-realtime/package.json + - docker/coolify-realtime/soketi-entrypoint.sh + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "coollabsio/coolify-realtime" + +jobs: + amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + no-cache: true + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + labels: | + coolify.managed=true + aarch64: + runs-on: [ self-hosted, arm64 ] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + no-cache: true + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/aarch64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + labels: | + coolify.managed=true + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ amd64, aarch64 ] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Create & publish manifest + run: | + docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 75e3f1681..30910ae0b 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -2,7 +2,7 @@ name: Coolify Realtime (v4) on: push: - branches: [ "main", "next" ] + branches: [ "main" ] paths: - .github/workflows/coolify-realtime.yml - docker/coolify-realtime/Dockerfile diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 352c6a59f..3ee46a2e1 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -46,9 +46,6 @@ class StartDragonfly 'networks' => [ $this->database->destination->network, ], - 'ulimits' => [ - 'memlock' => '-1', - ], 'labels' => [ 'coolify.managed' => 'true', ], diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index f8882d12a..481757162 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -2,7 +2,6 @@ namespace App\Actions\Fortify; -use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; @@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers */ public function create(array $input): User { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { abort(403); } @@ -48,7 +47,7 @@ class CreateNewUser implements CreatesNewUsers $team = $user->teams()->first(); // Disable registration after first user is created - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $settings->is_registration_enabled = false; $settings->save(); } else { diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index dcb4058c0..55af1a8c0 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -2,7 +2,6 @@ namespace App\Actions\License; -use App\Models\InstanceSettings; use Illuminate\Support\Facades\Http; use Lorisleiva\Actions\Concerns\AsAction; @@ -13,7 +12,7 @@ class CheckResaleLicense public function handle() { try { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (isDev()) { $settings->update([ 'is_resale_license_active' => true, 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/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 19bb03383..dc6ac12bf 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -2,7 +2,6 @@ namespace App\Actions\Server; -use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -12,7 +11,7 @@ class CleanupDocker public function handle(Server $server) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $helperImageVersion = data_get($settings, 'helper_version'); $helperImage = config('coolify.helper_image'); $helperImageWithVersion = "$helperImage:$helperImageVersion"; diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 901f2cf77..30664df26 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -3,7 +3,6 @@ namespace App\Actions\Server; use App\Jobs\PullHelperImageJob; -use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -20,7 +19,7 @@ class UpdateCoolify public function handle($manual_update = false) { try { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $this->server = Server::find(0); if (! $this->server) { return; 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/Dev.php b/app/Console/Commands/Dev.php index 964b8e46e..20a2667c3 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -48,6 +48,13 @@ class Dev extends Command echo "Generating APP_KEY.\n"; Artisan::call('key:generate'); } + + // Generate STORAGE link if not exists + if (! file_exists(public_path('storage'))) { + echo "Generating STORAGE link.\n"; + Artisan::call('storage:link'); + } + // Seed database if it's empty $settings = InstanceSettings::find(0); if (! $settings) { diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 2f5d36140..ad7bff86d 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -7,7 +7,6 @@ use App\Enums\ActivityTypes; use App\Enums\ApplicationDeploymentStatus; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; @@ -69,7 +68,7 @@ class Init extends Command } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! is_null(env('AUTOUPDATE', null))) { if (env('AUTOUPDATE') == true) { $settings->update(['is_auto_update_enabled' => true]); @@ -196,7 +195,7 @@ class Init extends Command { $id = config('app.id'); $version = config('version'); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $do_not_track = data_get($settings, 'do_not_track'); if ($do_not_track == true) { echo "Skipping alive as do_not_track is enabled\n"; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 03d479400..1430fcdd1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,8 +12,8 @@ use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; +use App\Jobs\ServerStorageCheckJob; use App\Jobs\UpdateCoolifyJob; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; @@ -28,7 +28,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { $this->all_servers = Server::all(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -66,7 +66,7 @@ class Kernel extends ConsoleKernel private function pull_images($schedule) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { @@ -88,7 +88,7 @@ class Kernel extends ConsoleKernel private function schedule_updates($schedule) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $updateCheckFrequency = $settings->update_check_frequency; $schedule->job(new CheckForUpdatesJob) @@ -116,6 +116,7 @@ class Kernel extends ConsoleKernel } foreach ($servers as $server) { $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer(); $serverTimezone = $server->settings->server_timezone; if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 6b69350fe..63fbfc862 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -65,7 +65,7 @@ class Handler extends ExceptionHandler if ($e instanceof RuntimeException) { return; } - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); if ($this->settings->do_not_track) { return; } diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index b0a832605..1a2146799 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -94,7 +94,9 @@ class SshMultiplexingHelper $muxPersistTime = config('constants.ssh.mux_persist_time'); $scp_command = "timeout $timeout scp "; - + if ($server->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 48e126f27..eef179048 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -177,6 +177,7 @@ class ApplicationsController extends Controller 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -279,6 +280,7 @@ class ApplicationsController extends Controller 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -381,6 +383,7 @@ class ApplicationsController extends Controller 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -468,6 +471,7 @@ class ApplicationsController extends Controller 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -552,6 +556,7 @@ class ApplicationsController extends Controller 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -602,6 +607,7 @@ class ApplicationsController extends Controller 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -627,7 +633,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']; + $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']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -665,6 +671,7 @@ class ApplicationsController extends Controller $fqdn = $request->domains; $instantDeploy = $request->instant_deploy; $githubAppUuid = $request->github_app_uuid; + $useBuildServer = $request->use_build_server; $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { @@ -737,6 +744,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + 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) { @@ -833,6 +844,10 @@ class ApplicationsController extends Controller $application->environment_id = $environment->id; $application->source_type = $githubApp->getMorphClass(); $application->source_id = $githubApp->id; + 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) { @@ -925,6 +940,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + 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) { @@ -1004,6 +1023,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1062,6 +1085,10 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1259,16 +1286,10 @@ class ApplicationsController extends Controller format: 'uuid', ) ), - new OA\Parameter( - name: 'cleanup', - in: 'query', - description: 'Delete configurations and volumes.', - required: false, - schema: new OA\Schema( - type: 'boolean', - default: true, - ) - ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -1316,10 +1337,14 @@ class ApplicationsController extends Controller 'message' => 'Application not found', ], 404); } + DeleteResourceJob::dispatch( resource: $application, - deleteConfigurations: $cleanup, - deleteVolumes: $cleanup); + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Application deletion request queued.', @@ -1404,6 +1429,7 @@ class ApplicationsController extends Controller 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -1460,7 +1486,7 @@ class ApplicationsController extends Controller ], 404); } $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']; + $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(), @@ -1538,6 +1564,13 @@ class ApplicationsController extends Controller } $instantDeploy = $request->instant_deploy; + $use_build_server = $request->use_build_server; + + if (isset($use_build_server)) { + $application->settings->is_build_server_enabled = $use_build_server; + $application->settings->save(); + } + removeUnnecessaryFieldsFromRequest($request); $data = $request->all(); diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a205704cc..65873f818 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1541,16 +1541,10 @@ class DatabasesController extends Controller format: 'uuid', ) ), - new OA\Parameter( - name: 'cleanup', - in: 'query', - description: 'Delete configurations and volumes.', - required: false, - schema: new OA\Schema( - type: 'boolean', - default: true, - ) - ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -1595,10 +1589,14 @@ class DatabasesController extends Controller if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + DeleteResourceJob::dispatch( resource: $database, - deleteConfigurations: $cleanup, - deleteVolumes: $cleanup); + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Database deletion request queued.', diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index c085b88a5..2414b7a42 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -86,7 +86,7 @@ class OtherController extends Controller if ($teamId !== '0') { return response()->json(['message' => 'You are not allowed to enable the API.'], 403); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $settings->update(['is_api_enabled' => true]); return response()->json(['message' => 'API enabled.'], 200); @@ -138,7 +138,7 @@ class OtherController extends Controller if ($teamId !== '0') { return response()->json(['message' => 'You are not allowed to disable the API.'], 403); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $settings->update(['is_api_enabled' => false]); return response()->json(['message' => 'API disabled.'], 200); diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 5f0d6bb12..a49515579 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -308,7 +308,7 @@ class ServersController extends Controller $projects = Project::where('team_id', $teamId)->get(); $domains = collect(); $applications = $projects->pluck('applications')->flatten(); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($applications->count() > 0) { foreach ($applications as $application) { $ip = $application->destination->server->ip; diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 0a6154410..89418517b 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -432,6 +432,10 @@ class ServicesController extends Controller tags: ['Services'], parameters: [ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -476,7 +480,14 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - DeleteResourceJob::dispatch($service); + + DeleteResourceJob::dispatch( + resource: $service, + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Service deletion request queued.', @@ -516,7 +527,8 @@ class ServicesController extends Controller items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -619,7 +631,8 @@ class ServicesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -738,7 +751,8 @@ class ServicesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -853,7 +867,8 @@ class ServicesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -953,7 +968,8 @@ class ServicesController extends Controller ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1025,9 +1041,11 @@ class ServicesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1101,9 +1119,11 @@ class ServicesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1177,9 +1197,11 @@ class ServicesController extends Controller type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 9569e8cfa..630d01045 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -22,7 +21,7 @@ class OauthController extends Controller $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); if (! $user) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { abort(403, 'Registration is disabled'); } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index 648720ba4..471e6d602 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -14,7 +14,7 @@ class ApiAllowed if (isCloud()) { return $next($request); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->is_api_enabled === false) { return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 24565b389..9ae383a9f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -12,7 +12,6 @@ use App\Models\ApplicationPreview; use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; -use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -962,7 +961,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { - if ($this->application->compose_parsing_version === '3') { + if ((int) $this->application->compose_parsing_version >= 3) { $envs->push("COOLIFY_URL={$this->application->fqdn}"); } else { $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); @@ -970,7 +969,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); - if ($this->application->compose_parsing_version === '3') { + if ((int) $this->application->compose_parsing_version >= 3) { $envs->push("COOLIFY_FQDN={$url}"); } else { $envs->push("COOLIFY_URL={$url}"); @@ -1334,7 +1333,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function prepare_builder_image() { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $helperImage = config('coolify.helper_image'); $helperImage = "{$helperImage}:{$settings->helper_version}"; // Get user home directory diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index 747a9a98a..f2348118a 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Models\InstanceSettings; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -22,7 +21,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue if (isDev() || isCloud()) { return; } - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 947dc4317..769739d5e 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -2,9 +2,7 @@ namespace App\Jobs; -use App\Actions\Database\StopDatabase; use App\Events\BackupCreated; -use App\Models\InstanceSettings; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; @@ -25,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 { @@ -64,32 +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)) { - $this->backup->update(['status' => 'failed']); - 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); @@ -239,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'; @@ -252,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, @@ -280,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, @@ -289,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, @@ -327,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); + } } } @@ -386,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); @@ -407,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 === '') { @@ -426,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); @@ -468,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 { @@ -517,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()); @@ -562,7 +569,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue private function getFullImageName(): string { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $helperImage = config('coolify.helper_image'); $latestVersion = $settings->helper_version; diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index ac34d064e..2442d5b06 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -31,10 +31,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, - public bool $deleteConfigurations, - public bool $deleteVolumes, - public bool $dockerCleanup, - public bool $deleteConnectedNetworks + public bool $deleteConfigurations = true, + public bool $deleteVolumes = true, + public bool $dockerCleanup = true, + public bool $deleteConnectedNetworks = true ) {} public function handle() diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index ef1659680..4b208fc31 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -26,7 +25,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $latest_version = data_get($versions, 'coolify.helper.version'); $current_version = $settings->helper_version; if (version_compare($latest_version, $current_version, '>')) { diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php new file mode 100644 index 000000000..376cb8532 --- /dev/null +++ b/app/Jobs/ServerStorageCheckJob.php @@ -0,0 +1,59 @@ +server->isFunctional()) { + ray('Server is not ready.'); + + return 'Server is not ready.'; + } + $team = $this->server->team; + $percentage = $this->server->storageCheck(); + if ($percentage > 1) { + ray('Server storage is at '.$percentage.'%'); + } + + } catch (\Throwable $e) { + ray($e->getMessage()); + + return handleError($e); + } + + } +} diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php index 4c65a711f..2cc705e4a 100644 --- a/app/Jobs/UpdateCoolifyJob.php +++ b/app/Jobs/UpdateCoolifyJob.php @@ -3,7 +3,6 @@ namespace App\Jobs; use App\Actions\Server\UpdateCoolify; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -23,7 +22,7 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue { try { CheckForUpdatesJob::dispatchSync(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->new_version_available) { Log::info('No new version available. Skipping update.'); 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 d6dc0d521..934e81661 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -47,7 +47,7 @@ class Help extends Component ] ); $mail->subject("[HELP]: {$this->subject}"); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); if (! $type) { $url = 'https://app.coolify.io/api/feedback'; @@ -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/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 2960ed226..53673292e 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -172,7 +172,7 @@ class Email extends Component public function copyFromInstanceSettings() { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->smtp_enabled) { $team = currentTeam(); $team->update([ 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/Select.php b/app/Livewire/Project/New/Select.php index 3c5f3901b..4c5c3a691 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -90,6 +90,121 @@ class Select extends Component // } // } + public function loadServices2() + { + $services = get_service_templates(true); + $services = collect($services)->map(function ($service, $key) { + return array_merge($service, [ + 'name' => str($key)->headline(), + 'logo' => asset($service['logo']), + ]); + })->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 known for its simplicity and performance.', + 'logo' => ' + + + +', + + ], + [ + 'id' => 'mariadb', + 'name' => 'MariaDB', + 'description' => 'MariaDB is an open-source relational database known for its simplicity and performance.', + 'logo' => '', + ], + [ + 'id' => 'redis', + 'name' => 'Redis', + 'description' => 'Redis is an open-source in-memory data structure store known for its simplicity and performance.', + 'logo' => '', + ], + [ + 'id' => 'keydb', + 'name' => 'KeyDB', + 'description' => 'KeyDB is an open-source in-memory data structure store known for its simplicity and performance.', + 'logo' => '
', + ], + [ + 'id' => 'dragonfly', + 'name' => 'Dragonfly', + 'description' => 'Dragonfly is an open-source in-memory data structure store known for its simplicity and performance.', + 'logo' => '
', + ], + [ + 'id' => 'mongodb', + 'name' => 'MongoDB', + 'description' => 'MongoDB is an open-source document-oriented database known for its simplicity and performance.', + 'logo' => '', + ], + [ + 'id' => 'clickhouse', + 'name' => 'ClickHouse', + 'description' => 'ClickHouse is an open-source column-oriented database known for its simplicity and performance.', + 'logo' => '
', + ], + + ]; + + return [ + 'services' => $services, + 'gitBasedApplications' => $gitBasedApplications, + 'dockerBasedApplications' => $dockerBasedApplications, + 'databases' => $databases, + ]; + } + public function updatedSearch() { $this->loadServices(); diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 42c9357fd..70b3b5db6 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -108,8 +108,23 @@ 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($this->service); + StopService::run(service: $this->service, dockerCleanup: false); $this->service->parse(); $this->dispatch('imagePulled'); $activity = StartService::run($this->service); diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 543e64539..c05260899 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -91,10 +91,12 @@ class Danger extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (isProduction()) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if (! $this->resource) { 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/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index d95443621..90419caed 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -11,6 +11,8 @@ use Livewire\Component; class ExecuteContainerCommand extends Component { + public $selected_container = 'default'; + public $container; public Collection $containers; @@ -83,11 +85,14 @@ class ExecuteContainerCommand extends Component $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); } foreach ($containers as $container) { - $payload = [ - 'server' => $server, - 'container' => $container, - ]; - $this->containers = $this->containers->push($payload); + // if container state is running + if (data_get($container, 'State') === 'running') { + $payload = [ + 'server' => $server, + 'container' => $container, + ]; + $this->containers = $this->containers->push($payload); + } } } elseif (data_get($this->parameters, 'database_uuid')) { if ($this->resource->isRunning()) { @@ -100,7 +105,6 @@ class ExecuteContainerCommand extends Component } } elseif (data_get($this->parameters, 'service_uuid')) { $this->resource->applications()->get()->each(function ($application) { - ray($application); if ($application->isRunning()) { $this->containers->push([ 'server' => $this->resource->server, @@ -131,9 +135,14 @@ class ExecuteContainerCommand extends Component #[On('connectToContainer')] public function connectToContainer() { + if ($this->selected_container === 'default') { + $this->dispatch('error', 'Please select a container.'); + + return; + } try { - $container_name = data_get($this->container, 'container.Names'); - if (is_null($container_name)) { + $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container); + if (is_null($container)) { throw new \RuntimeException('Container not found.'); } $server = data_get($this->container, 'server'); @@ -141,11 +150,11 @@ class ExecuteContainerCommand extends Component if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - - $this->dispatch('send-terminal-command', - true, - $container_name, - $server->uuid, + $this->dispatch( + 'send-terminal-command', + isset($container), + data_get($container, 'container.Names'), + data_get($container, 'server.uuid') ); } catch (\Throwable $e) { 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/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/Settings/Index.php b/app/Livewire/Settings/Index.php index c52970258..754f0929b 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -60,7 +60,7 @@ class Index extends Component public function mount() { if (isInstanceAdmin()) { - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; @@ -162,7 +162,7 @@ class Index extends Component { CheckForUpdatesJob::dispatchSync(); $this->dispatch('updateAvailable'); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->new_version_available) { $this->dispatch('success', 'New version available!'); } else { diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index f9402fd7b..ca0c9c1ae 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -29,7 +29,7 @@ class License extends Component abort(404); } $this->instance_id = config('app.id'); - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); } public function render() diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 99b8f8d49..9240aa96d 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -42,7 +42,7 @@ class SettingsBackup extends Component public function mount() { if (isInstanceAdmin()) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $this->database = StandalonePostgresql::whereName('coolify-db')->first(); $s3s = S3Storage::whereTeamId(0)->get() ?? []; if ($this->database) { diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 3eb8ea646..4515df9a7 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -43,7 +43,7 @@ class SettingsEmail extends Component public function mount() { if (isInstanceAdmin()) { - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $this->emails = auth()->user()->email; } else { return redirect()->route('dashboard'); diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 75d7fd04a..193b650ff 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -99,7 +99,7 @@ class Change extends Component return redirect()->route('source.all'); } $this->applications = $this->github_app->applications; - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->name = str($this->github_app->name)->kebab(); 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/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index c278bf58e..df450cf7e 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -23,7 +23,7 @@ class Index extends Component if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { return redirect()->route('subscription.show'); } - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 55006745a..e4ab3918a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -104,7 +104,7 @@ class Application extends BaseModel { use SoftDeletes; - private static $parserVersion = '3'; + private static $parserVersion = '4'; protected $guarded = []; @@ -143,6 +143,9 @@ class Application extends BaseModel } $application->tags()->detach(); $application->previews()->delete(); + foreach ($application->deployment_queue as $deployment) { + $deployment->delete(); + } }); } @@ -304,7 +307,7 @@ class Application extends BaseModel 'application_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $url = Url::fromString($route); $url = $url->withPort(null); @@ -710,6 +713,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(); @@ -1150,7 +1158,7 @@ class Application extends BaseModel public function parse(int $pull_request_id = 0, ?int $preview_id = null) { - if ($this->compose_parsing_version === '3') { + if ((int) $this->compose_parsing_version >= 3) { return newParser($this, $pull_request_id, $preview_id); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); 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/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 138775aba..9f8e4b342 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -126,11 +126,6 @@ class EnvironmentVariable extends Model $env = $this->get_real_environment_variables($this->value, $resource); return data_get($env, 'value', $env); - if (is_string($env)) { - return $env; - } - - return $env->value; } ); } diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 27a181ee4..bb3d1478b 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -85,4 +85,17 @@ class InstanceSettings extends Model implements SendsEmail return "[{$instanceName}]"; } + + public function helperVersion(): Attribute + { + return Attribute::make( + get: function ($value) { + if (isDev()) { + return 'latest'; + } + + return $value; + } + ); + } } 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/Server.php b/app/Models/Server.php index a1b4c0828..8864deef1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -268,7 +268,7 @@ respond 404 public function setupDynamicProxyConfiguration() { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $dynamic_config_path = $this->proxyPath().'/dynamic'; if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { $file = "$dynamic_config_path/coolify.yaml"; @@ -448,11 +448,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 +468,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'); } @@ -1212,4 +1211,18 @@ $schema://$host { return $this; } + + public function storageCheck(): ?string + { + $commands = [ + 'df / --output=pcent | tr -cd 0-9', + ]; + + 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 d236869ba..a5aafaaf9 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -42,7 +42,7 @@ class Service extends BaseModel { use HasFactory, SoftDeletes; - private static $parserVersion = '3'; + private static $parserVersion = '4'; protected $guarded = []; @@ -285,6 +285,65 @@ class Service extends BaseModel foreach ($applications as $application) { $image = str($application->image)->before(':')->value(); switch ($image) { + case str($image)?->contains('invoiceninja'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first(); + $data = $data->merge([ + 'Email' => [ + 'key' => 'IN_USER_EMAIL', + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => 'IN_PASSWORD', + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Invoice Ninja', $data->toArray()); + break; + case str($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 str($image)?->contains('rabbitmq'): $data = collect([]); $host_port = $this->environment_variables()->where('key', 'PORT')->first(); @@ -770,6 +829,30 @@ class Service extends BaseModel } $fields->put('Code Server', $data->toArray()); break; + case str($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; + } } $databases = $this->databases()->get(); @@ -1052,12 +1135,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() @@ -1095,7 +1178,21 @@ class Service extends BaseModel return 3; }); foreach ($sorted as $env) { - $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + if (version_compare($env->version, '4.0.0-beta.347', '<=')) { + $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + } else { + $real_value = $env->real_value; + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->real_value; + } else { + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + } + $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; + } } if ($sorted->count() === 0) { $commands[] = 'touch .env'; @@ -1105,7 +1202,7 @@ class Service extends BaseModel public function parse(bool $isNew = false): Collection { - if ($this->compose_parsing_version === '3') { + if ((int) $this->compose_parsing_version >= 3) { return newParser($this); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile($this, $isNew); 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/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 549fc6cd3..cc7d76ebf 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -13,7 +13,7 @@ class TransactionalEmailChannel { public function send(User $notifiable, Notification $notification): void { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) { Log::info('SMTP/Resend not enabled'); diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 8b1c02d39..3938a8da7 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -18,7 +18,7 @@ class ResetPassword extends Notification public function __construct($token) { - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); $this->token = $token; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index cd90918ad..8b4c2eef2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,10 +2,8 @@ namespace App\Providers; -use App\Models\InstanceSettings; use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Laravel\Sanctum\Sanctum; @@ -30,9 +28,5 @@ class AppServiceProvider extends ServiceProvider ])->baseUrl($api_url); } }); - // if (! env('CI')) { - // View::share('instanceSettings', InstanceSettings::get()); - // } - } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 53a2e9281..b916b6234 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -46,7 +46,7 @@ class FortifyServiceProvider extends ServiceProvider Fortify::registerView(function () { $isFirstUser = User::count() === 0; - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { return redirect()->route('login'); } @@ -60,7 +60,7 @@ class FortifyServiceProvider extends ServiceProvider }); Fortify::loginView(function () { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $enabled_oauth_providers = OauthSetting::where('enabled', true)->get(); $users = User::count(); if ($users == 0) { diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 8e14ef9ee..006b095cf 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -175,4 +175,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('instant_deploy'); $request->offsetUnset('github_app_uuid'); $request->offsetUnset('private_key_uuid'); + $request->offsetUnset('use_build_server'); } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 1eeec8f94..d8dc26a48 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,7 +37,6 @@ const DATABASE_DOCKER_IMAGES = [ 'neo4j', 'influxdb', 'clickhouse/clickhouse-server', - 'supabase/postgres', ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index e252bda10..7e902fcdd 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -325,38 +325,16 @@ 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]; + } + return null; + })->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { @@ -404,20 +382,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 +398,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 +413,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 +425,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 +463,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 +474,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 +486,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 +497,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/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 975edf9ca..4d78021c4 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { if (! $settings) { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); @@ -281,7 +281,7 @@ function base_ip(): string if (isDev()) { return 'localhost'; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->public_ipv4) { return "$settings->public_ipv4"; } @@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn) */ function base_url(bool $withPort = true): string { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->fqdn) { return $settings->fqdn; } @@ -343,6 +343,11 @@ function isSubscribed() { return isSubscriptionActive() || auth()->user()->isInstanceAdmin(); } + +function isProduction(): bool +{ + return ! isDev(); +} function isDev(): bool { return config('app.env') === 'local'; @@ -384,7 +389,7 @@ function send_internal_notification(string $message): void } function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); if (! $type) { throw new Exception('No email settings found.'); @@ -703,7 +708,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); + } } } } @@ -753,7 +760,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); + } } } } @@ -819,6 +828,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); @@ -850,6 +884,7 @@ function parseEnvVariable(Str|string $value) } else { // SERVICE_BASE64_64_UMAMI $command = $value->after('SERVICE_')->beforeLast('_'); + ray($command); } } } @@ -970,7 +1005,7 @@ function validate_dns_entry(string $fqdn, Server $server) if (str($host)->contains('sslip.io')) { return true; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); if (! $is_dns_validation_enabled) { return true; @@ -1090,7 +1125,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = if ($domainFound) { return true; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $domain = data_get($settings, 'fqdn'); if (str($domain)->endsWith('/')) { @@ -1162,7 +1197,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null } } if ($resource) { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $domain = data_get($settings, 'fqdn'); if (str($domain)->endsWith('/')) { @@ -1179,12 +1214,26 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { - if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) { + if ( + ! str(trim($line))->startsWith([ + 'cd', + 'command', + 'echo', + 'true', + 'if', + 'fi', + ]) + ) { return "sudo $line"; } + if (str(trim($line))->startsWith('if')) { + return str_replace('if', 'if sudo', $line); + } + return $line; }); + $commands = $commands->map(function ($line) use ($server) { if (Str::startsWith($line, 'sudo mkdir -p')) { return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); @@ -1192,6 +1241,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array return $line; }); + $commands = $commands->map(function ($line) { $line = str($line); if (str($line)->contains('$(')) { @@ -1588,7 +1638,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); + } } } } @@ -2503,7 +2555,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); + } } } } @@ -2964,11 +3018,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, @@ -3078,7 +3143,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; @@ -3189,12 +3254,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, @@ -3264,7 +3341,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { $volume = $source->value().':'.$target->value(); } else { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } elseif ($isService) { + $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + } + } else { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } $source = replaceLocalSource($source, $mainDirectory); if ($isApplication && $isPullRequest) { $source = $source."-pr-$pullRequestId"; @@ -3284,6 +3369,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'resource_type' => get_class($originalResource), ] ); + if (isDev()) { + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } elseif ($isService) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); + } + } else { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } + } $volume = "$source:$target"; } } elseif ($type->value() === 'volume') { @@ -3606,6 +3702,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; @@ -3826,14 +3934,37 @@ 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) { + 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); + } + }); + + return $changedEnvironment; + } $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); + } } } } @@ -3841,3 +3972,7 @@ function convertComposeEnvironmentToArray($environment) return $convertedServiceVariables; } +function instanceSettings() +{ + return InstanceSettings::get(); +} diff --git a/composer.json b/composer.json index 17432c532..03adf9823 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "zircote/swagger-php": "^4.10" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.13", "fakerphp/faker": "^v1.21.0", "laravel/dusk": "^v8.0", "laravel/pint": "^1.16", diff --git a/composer.lock b/composer.lock index fffb320d3..420d87ec0 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": "96f8146407d0e6e897ff097c5eccd3a4", + "content-hash": "42c28ab141b70fcabf75b51afa96c670", "packages": [ { "name": "amphp/amp", @@ -11823,6 +11823,90 @@ } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.13.5", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07", + "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07", + "shasum": "" + }, + "require": { + "illuminate/routing": "^9|^10|^11", + "illuminate/session": "^9|^10|^11", + "illuminate/support": "^9|^10|^11", + "maximebf/debugbar": "~1.22.0", + "php": "^8.0", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7|^8|^9", + "phpunit/phpunit": "^9.6|^10.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.13-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-04-12T11:20:37+00:00" + }, { "name": "brianium/paratest", "version": "v7.4.3", @@ -12301,6 +12385,74 @@ }, "time": "2024-09-03T15:00:28+00:00" }, + { + "name": "maximebf/debugbar", + "version": "v1.22.5", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989", + "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989", + "shasum": "" + }, + "require": { + "php": "^7.2|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.22-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "support": { + "issues": "https://github.com/maximebf/php-debugbar/issues", + "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5" + }, + "time": "2024-09-09T08:05:55+00:00" + }, { "name": "mockery/mockery", "version": "1.6.12", diff --git a/config/debugbar.php b/config/debugbar.php new file mode 100644 index 000000000..eae406ba7 --- /dev/null +++ b/config/debugbar.php @@ -0,0 +1,325 @@ + env('DEBUGBAR_ENABLED', null), + 'except' => [ + 'telescope*', + 'horizon*', + ], + + /* + |-------------------------------------------------------------------------- + | Storage settings + |-------------------------------------------------------------------------- + | + | DebugBar stores data for session/ajax requests. + | You can disable this, so the debugbar stores data in headers/session, + | but this can cause problems with large data collectors. + | By default, file storage (in the storage folder) is used. Redis and PDO + | can also be used. For PDO, run the package migrations first. + | + | Warning: Enabling storage.open will allow everyone to access previous + | request, do not enable open storage in publicly available environments! + | Specify a callback if you want to limit based on IP or authentication. + | Leaving it to null will allow localhost only. + */ + 'storage' => [ + 'enabled' => true, + 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. + 'driver' => 'file', // redis, file, pdo, socket, custom + 'path' => storage_path('debugbar'), // For file driver + 'connection' => null, // Leave null for default connection (Redis/PDO) + 'provider' => '', // Instance of StorageInterface for custom driver + 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver + 'port' => 2304, // Port to use with the "socket" driver + ], + + /* + |-------------------------------------------------------------------------- + | Editor + |-------------------------------------------------------------------------- + | + | Choose your preferred editor to use when clicking file name. + | + | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", + | "vscode-insiders-remote", "vscodium", "textmate", "emacs", + | "sublime", "atom", "nova", "macvim", "idea", "netbeans", + | "xdebug", "espresso" + | + */ + + 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | Remote Path Mapping + |-------------------------------------------------------------------------- + | + | If you are using a remote dev server, like Laravel Homestead, Docker, or + | even a remote VPS, it will be necessary to specify your path mapping. + | + | Leaving one, or both of these, empty or null will not trigger the remote + | URL changes and Debugbar will treat your editor links as local files. + | + | "remote_sites_path" is an absolute base path for your sites or projects + | in Homestead, Vagrant, Docker, or another remote development server. + | + | Example value: "/home/vagrant/Code" + | + | "local_sites_path" is an absolute base path for your sites or projects + | on your local computer where your IDE or code editor is running on. + | + | Example values: "/Users//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), + 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), + + /* + |-------------------------------------------------------------------------- + | Vendors + |-------------------------------------------------------------------------- + | + | Vendor files are included by default, but can be set to false. + | This can also be set to 'js' or 'css', to only include javascript or css vendor files. + | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + | and for js: jquery and highlight.js + | So if you want syntax highlighting, set it to true. + | jQuery is set to not conflict with existing jQuery scripts. + | + */ + + 'include_vendors' => true, + + /* + |-------------------------------------------------------------------------- + | Capture Ajax Requests + |-------------------------------------------------------------------------- + | + | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + | you can use this option to disable sending the data through the headers. + | + | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + | + | Note for your request to be identified as ajax requests they must either send the header + | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. + | + | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. + | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. + */ + + 'capture_ajax' => true, + 'add_ajax_timing' => false, + 'ajax_handler_auto_show' => true, + 'ajax_handler_enable_tab' => true, + + /* + |-------------------------------------------------------------------------- + | Custom Error Handler for Deprecated warnings + |-------------------------------------------------------------------------- + | + | When enabled, the Debugbar shows deprecated warnings for Symfony components + | in the Messages tab. + | + */ + 'error_handler' => false, + + /* + |-------------------------------------------------------------------------- + | Clockwork integration + |-------------------------------------------------------------------------- + | + | The Debugbar can emulate the Clockwork headers, so you can use the Chrome + | Extension, without the server-side code. It uses Debugbar collectors instead. + | + */ + 'clockwork' => false, + + /* + |-------------------------------------------------------------------------- + | DataCollectors + |-------------------------------------------------------------------------- + | + | Enable/disable DataCollectors + | + */ + + 'collectors' => [ + 'phpinfo' => true, // Php version + 'messages' => true, // Messages + 'time' => true, // Time Datalogger + 'memory' => true, // Memory usage + 'exceptions' => true, // Exception displayer + 'log' => true, // Logs from Monolog (merged in messages if enabled) + 'db' => true, // Show database (PDO) queries and bindings + 'views' => true, // Views with their data + 'route' => true, // Current route information + 'auth' => false, // Display Laravel authentication status + 'gate' => true, // Display Laravel Gate checks + 'session' => true, // Display session data + 'symfony_request' => true, // Only one can be enabled.. + 'mail' => true, // Catch mail messages + 'laravel' => false, // Laravel version and environment + 'events' => false, // All events fired + 'default_request' => false, // Regular or special Symfony request logger + 'logs' => false, // Add the latest log messages + 'files' => false, // Show the included files + 'config' => false, // Display config settings + 'cache' => false, // Display cache events + 'models' => true, // Display models + 'livewire' => true, // Display Livewire (when available) + 'jobs' => false, // Display dispatched jobs + ], + + /* + |-------------------------------------------------------------------------- + | Extra options + |-------------------------------------------------------------------------- + | + | Configure some DataCollectors + | + */ + + 'options' => [ + 'time' => [ + 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate + ], + 'messages' => [ + 'trace' => true, // Trace the origin of the debug message + ], + 'memory' => [ + 'reset_peak' => false, // run memory_reset_peak_usage before collecting + 'with_baseline' => false, // Set boot memory usage as memory peak baseline + 'precision' => 0, // Memory rounding precision + ], + 'auth' => [ + 'show_name' => true, // Also show the users name/email in the debugbar + 'show_guards' => true, // Show the guards that are used + ], + 'db' => [ + 'with_params' => true, // Render SQL with the parameters substituted + 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. + 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) + 'timeline' => false, // Add the queries to the timeline + 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute. + 'explain' => [ // Show EXPLAIN output on queries + 'enabled' => false, + 'types' => ['SELECT'], // Deprecated setting, is always only SELECT + ], + 'hints' => false, // Show hints for common mistakes + 'show_copy' => false, // Show copy button next to the query, + 'slow_threshold' => false, // Only track queries that last longer than this time in ms + 'memory_usage' => false, // Show queries memory usage + 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured + 'hard_limit' => 500, // After the hard limit, queries are ignored + ], + 'mail' => [ + 'timeline' => false, // Add mails to the timeline + 'show_body' => true, + ], + 'views' => [ + 'timeline' => false, // Add the views to the timeline (Experimental) + 'data' => false, //true for all data, 'keys' for only names, false for no parameters. + 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force + 'exclude_paths' => [ // Add the paths which you don't want to appear in the views + 'vendor/filament', // Exclude Filament components by default + ], + ], + 'route' => [ + 'label' => true, // show complete route on bar + ], + 'session' => [ + 'hiddens' => [], // hides sensitive values using array paths + ], + 'symfony_request' => [ + 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password + ], + 'events' => [ + 'data' => false, // collect events data, listeners + ], + 'logs' => [ + 'file' => null, + ], + 'cache' => [ + 'values' => true, // collect cache values + ], + ], + + /* + |-------------------------------------------------------------------------- + | Inject Debugbar in Response + |-------------------------------------------------------------------------- + | + | Usually, the debugbar is added just before , by listening to the + | Response after the App is done. If you disable this, you have to add them + | in your template yourself. See http://phpdebugbar.com/docs/rendering.html + | + */ + + 'inject' => true, + + /* + |-------------------------------------------------------------------------- + | DebugBar route prefix + |-------------------------------------------------------------------------- + | + | Sometimes you want to set route prefix to be used by DebugBar to load + | its resources from. Usually the need comes from misconfigured web server or + | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + | + */ + 'route_prefix' => '_debugbar', + + /* + |-------------------------------------------------------------------------- + | DebugBar route middleware + |-------------------------------------------------------------------------- + | + | Additional middleware to run on the Debugbar routes + */ + 'route_middleware' => [], + + /* + |-------------------------------------------------------------------------- + | DebugBar route domain + |-------------------------------------------------------------------------- + | + | By default DebugBar route served from the same domain that request served. + | To override default domain, specify it as a non-empty value. + */ + 'route_domain' => null, + + /* + |-------------------------------------------------------------------------- + | DebugBar theme + |-------------------------------------------------------------------------- + | + | Switches between light and dark theme. If set to auto it will respect system preferences + | Possible values: auto, light, dark + */ + 'theme' => env('DEBUGBAR_THEME', 'auto'), + + /* + |-------------------------------------------------------------------------- + | Backtrace stack limit + |-------------------------------------------------------------------------- + | + | By default, the DebugBar limits the number of frames returned by the 'debug_backtrace()' function. + | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. + */ + 'debug_backtrace_limit' => 50, +]; diff --git a/config/sentry.php b/config/sentry.php index 5efd8968d..14f8ff5f1 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.348', + 'release' => '4.0.0-beta.356', // 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 09d7ab3e7..b33115d86 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ schemalessAttributes('smtp'); }); - $instance_setting = InstanceSettings::get(); + $instance_setting = instanceSettings(); $instance_setting->smtp = [ 'enabled' => $instance_setting->smtp_enabled, 'from_address' => $instance_setting->smtp_from_address, diff --git a/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php new file mode 100644 index 000000000..b1f301bd3 --- /dev/null +++ b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php @@ -0,0 +1,28 @@ +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/seeders/InstanceSettingsSeeder.php b/database/seeders/InstanceSettingsSeeder.php index c3182a2dd..35fc8506b 100644 --- a/database/seeders/InstanceSettingsSeeder.php +++ b/database/seeders/InstanceSettingsSeeder.php @@ -27,14 +27,14 @@ class InstanceSettingsSeeder extends Seeder $ipv4 = Process::run('curl -4s https://ifconfig.io')->output(); $ipv4 = trim($ipv4); $ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv4) && $ipv4) { $settings->update(['public_ipv4' => $ipv4]); } $ipv6 = Process::run('curl -6s https://ifconfig.io')->output(); $ipv6 = trim($ipv6); $ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv6) && $ipv6) { $settings->update(['public_ipv6' => $ipv6]); } 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 9777c52df..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.1' + 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 d6b820583..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,27 +130,29 @@ async function handleCommand(ws, command, userId) { cols: 80, rows: 30, cwd: process.env.HOME, + env: {}, }; // NOTE: - Initiates a process within the Terminal container // Establishes an SSH connection to root@coolify with RequestTTY enabled // Executes the 'docker exec' command to connect to a specific container - // If the user types 'exit', it terminates the container connection and reverts to the server. - const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options); + const ptyProcess = pty.spawn('ssh', sshArgs.concat([hereDocContent]), options); + userSession.ptyProcess = ptyProcess; userSession.isActive = true; - ptyProcess.write(hereDocContent + '\n'); - // clear the terminal if the user has clear command - ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n'); 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) { @@ -181,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) { @@ -230,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/openapi.yaml b/openapi.yaml index ce0503e1f..91d5c1443 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -236,6 +236,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -457,6 +461,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -678,6 +686,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -850,6 +862,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1013,6 +1029,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1067,6 +1087,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1126,9 +1150,33 @@ paths: type: string format: uuid - - name: cleanup + name: delete_configurations in: query - description: 'Delete configurations and volumes.' + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' required: false schema: type: boolean @@ -1351,6 +1399,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1738,6 +1790,52 @@ paths: security: - bearerAuth: [] + '/applications/{uuid}/execute': + post: + tags: + - Applications + summary: 'Execute Command' + description: "Execute a command on the application's current container." + operationId: execute-command-application + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Command to execute.' + required: true + content: + application/json: + schema: + properties: + command: + type: string + description: 'Command to execute.' + type: object + responses: + '200': + description: "Execute a command on the application's current container." + content: + application/json: + schema: + properties: + message: { type: string, example: 'Command executed.' } + response: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] /databases: get: tags: @@ -1809,9 +1907,33 @@ paths: type: string format: uuid - - name: cleanup + name: delete_configurations in: query - description: 'Delete configurations and volumes.' + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' required: false schema: type: boolean @@ -3812,6 +3934,38 @@ paths: required: true schema: type: string + - + name: delete_configurations + in: query + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' + required: false + schema: + type: boolean + default: true responses: '200': description: 'Delete a service by UUID' @@ -4769,6 +4923,10 @@ components: type: boolean swarm_cluster: type: string + delete_unused_volumes: + type: boolean + delete_unused_networks: + type: boolean type: object ServerSetting: description: 'Server Settings model' diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 3eb270a2a..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.1' + 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 74cbecb19..c04a3dee6 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.347" + "version": "4.0.0-beta.354" }, "nightly": { - "version": "4.0.0-beta.348" + "version": "4.0.0-beta.355" }, "helper": { - "version": "1.0.1" + "version": "1.0.2" }, "realtime": { - "version": "1.0.1" + "version": "1.0.3" } } } 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/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/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/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/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/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/mailpit.svg b/public/svgs/mailpit.svg new file mode 100644 index 000000000..e569e71cc --- /dev/null +++ b/public/svgs/mailpit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/mixpost.svg b/public/svgs/mixpost.svg new file mode 100644 index 000000000..bd915e77a --- /dev/null +++ b/public/svgs/mixpost.svg @@ -0,0 +1,5 @@ + + + + + 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/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/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 21854ed63..4cdab0e07 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') { @@ -125,6 +145,10 @@ export function initializeTerminalComponent() { if (this.term) this.term.reset(); this.terminalActive = false; this.message = '(sorry, something went wrong, please try again)'; + } else if (event.data === 'pty-exited') { + this.terminalActive = false; + this.term.reset(); + this.commandBuffer = ''; } else { this.pendingWrites++; this.term.write(event.data, this.flowControlCallback.bind(this)); @@ -136,9 +160,12 @@ export function initializeTerminalComponent() { if (this.pendingWrites > this.MAX_PENDING_WRITES && !this.paused) { this.paused = true; this.socket.send(JSON.stringify({ pause: true })); - } else if (this.pendingWrites <= this.MAX_PENDING_WRITES && this.paused) { + return; + } + if (this.pendingWrites <= this.MAX_PENDING_WRITES && this.paused) { this.paused = false; this.socket.send(JSON.stringify({ resume: true })); + return; } }, @@ -147,15 +174,7 @@ export function initializeTerminalComponent() { this.term.onData((data) => { this.socket.send(JSON.stringify({ message: data })); - // Handle CTRL + D or exit command - if (data === '\x04' || (data === '\r' && this.stripAnsiCommands(this.commandBuffer).trim().includes('exit'))) { - this.checkIfProcessIsRunningAndKillIt(); - setTimeout(() => { - this.terminalActive = false; - this.term.reset(); - }, 500); - this.commandBuffer = ''; - } else if (data === '\r') { + if (data === '\r') { this.commandBuffer = ''; } else { this.commandBuffer += data; @@ -183,10 +202,6 @@ export function initializeTerminalComponent() { }); }, - stripAnsiCommands(input) { - return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); - }, - keepAlive() { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ ping: true })); diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php index 8d491a218..287f2f170 100644 --- a/resources/views/auth/confirm-password.blade.php +++ b/resources/views/auth/confirm-password.blade.php @@ -8,7 +8,7 @@
@csrf - + {{ __('auth.confirm_password') }} @if ($errors->any()) diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index 54d57a302..f66b460be 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -12,7 +12,7 @@ @if (is_transactional_emails_active())
@csrf - + {{ __('auth.forgot_password_send_email') }} @else diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 1af8a0cd1..7d615885f 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -10,7 +10,7 @@ @csrf @env('local') + required label="{{ __('input.email') }}" /> @@ -20,7 +20,7 @@ @else + label="{{ __('input.email') }}" /> diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index 2e1a63d84..cc13989b8 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -16,7 +16,7 @@ label="{{ __('input.email') }}" />
+ label="{{ __('input.password') }}" />
diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index 6761992a9..9288ff16a 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -9,7 +9,7 @@
@csrf
- +
Enter diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index dcb760ef9..fde75ab24 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -151,9 +151,9 @@ @endif @endif
-
-

Services

- Reload List +

Docker Based

+
+
-
- - @if ($loadingServices) - - @else -
- @forelse ($services as $serviceName => $service) - @if (data_get($service, 'minversion') && version_compare(config('version'), data_get($service, 'minversion'), '<')) - - {{ Str::headline($serviceName) }} - - @if (data_get($service, 'slogan')) - {{ data_get($service, 'slogan') }} - @endif - - - @if (data_get($service, 'logo')) - - @endif - - - {{ data_get($service, 'documentation') }} - - - You need to upgrade Coolify to {{ data_get($service, 'minversion') }} to use this - service. - - - @else - - {{ Str::headline($serviceName) }} - - @if (data_get($service, 'slogan')) - {{ data_get($service, 'slogan') }} - @endif - - - @if (file_exists(public_path(data_get($service, 'logo')))) - - @endif - - - {{ data_get($service, 'documentation') }} - - - {{-- --}} - @endif - @empty -
No service found. Please try to reload the list!
- @endforelse + +

Databases

+
+ +
+
+
+

Services

+ Reload List
- @endif +
Trademarks Policy: The respective trademarks mentioned here are owned by the + respective + companies, and use of them does not imply any affiliation or endorsement.
Find more services here.
+ +
+ +
+
+
@endif @if ($current_step === 'servers') @@ -637,8 +269,7 @@ diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index 62047bd41..f6502762a 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -48,8 +48,12 @@ class="items-center justify-center box">+ Add New Resource @else
- +
+ +