diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml new file mode 100644 index 0000000000..33e048627d --- /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 75e3f1681c..30910ae0bd 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 352c6a59f9..3ee46a2e10 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -46,9 +46,6 @@ public function handle(StandaloneDragonfly $database) '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 f8882d12aa..4817571622 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 @@ public function create(array $input): User $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 dcb4058c0f..55af1a8c02 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/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 19bb033835..dc6ac12bff 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 901f2cf777..30664df263 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/Dev.php b/app/Console/Commands/Dev.php index 964b8e46e9..20a2667c32 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -48,6 +48,13 @@ public function init() 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 2f5d361400..ad7bff86da 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -7,7 +7,6 @@ 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 @@ public function handle() } 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 @@ private function send_alive_signal() { $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 03d4794003..1430fcdd1d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,8 +12,8 @@ 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 @@ protected function schedule(Schedule $schedule): void 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 @@ private function pull_images($schedule) private function schedule_updates($schedule) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $updateCheckFrequency = $settings->update_check_frequency; $schedule->job(new CheckForUpdatesJob) @@ -116,6 +116,7 @@ private function check_resources($schedule) } 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 6b69350fe5..63fbfc8621 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -65,7 +65,7 @@ public function register(): void 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/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 48e126f27c..85d8c0c855 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -177,6 +177,7 @@ public function applications(Request $request) '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 @@ public function create_public_application(Request $request) '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 @@ public function create_private_gh_app_application(Request $request) '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 @@ public function create_private_deploy_key_application(Request $request) '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 @@ public function create_dockerfile_application(Request $request) '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 @@ public function create_dockerimage_application(Request $request) '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 @@ public function create_dockercompose_application(Request $request) 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 @@ private function create_application(Request $request, $type) $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,8 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { @@ -833,6 +842,8 @@ private function create_application(Request $request, $type) $application->environment_id = $environment->id; $application->source_type = $githubApp->getMorphClass(); $application->source_id = $githubApp->id; + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { @@ -925,6 +936,8 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { @@ -1004,6 +1017,8 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1062,6 +1077,8 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1259,16 +1276,10 @@ public function application_by_uuid(Request $request) 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 +1327,14 @@ public function delete_by_uuid(Request $request) '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 +1419,7 @@ public function delete_by_uuid(Request $request) '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 +1476,7 @@ public function update_by_uuid(Request $request) ], 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 +1554,10 @@ public function update_by_uuid(Request $request) } $instantDeploy = $request->instant_deploy; + $use_build_server = $request->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 a205704cc0..65873f8187 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1541,16 +1541,10 @@ public function create_database(Request $request, NewDatabaseTypes $type) 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 @@ public function delete_by_uuid(Request $request) 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 c085b88a5d..2414b7a420 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -86,7 +86,7 @@ public function enable_api(Request $request) 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 @@ public function disable_api(Request $request) 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 5f0d6bb126..a495155792 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -308,7 +308,7 @@ public function domains_by_server(Request $request) $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 0a6154410a..89418517ba 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -432,6 +432,10 @@ public function service_by_uuid(Request $request) 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 @@ public function delete_by_uuid(Request $request) 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 @@ public function delete_by_uuid(Request $request) items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -619,7 +631,8 @@ public function envs(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -738,7 +751,8 @@ public function update_env_by_uuid(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -853,7 +867,8 @@ public function create_bulk_envs(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -953,7 +968,8 @@ public function create_env(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1025,9 +1041,11 @@ public function delete_env_by_uuid(Request $request) type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1101,9 +1119,11 @@ public function action_deploy(Request $request) type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1177,9 +1197,11 @@ public function action_stop(Request $request) 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 9569e8cfa4..630d010459 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 @@ public function callback(string $provider) $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 648720ba49..471e6d602e 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -14,7 +14,7 @@ public function handle(Request $request, Closure $next): Response 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 24565b3898..9ae383a9fa 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -12,7 +12,6 @@ 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 @@ private function save_environment_variables() } } 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 @@ private function save_environment_variables() } 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 @@ private function create_workdir() 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 747a9a98af..f2348118a3 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 @@ public function handle(): void 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 947dc4317e..7109fda3be 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -4,7 +4,6 @@ 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; @@ -84,7 +83,6 @@ 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(); @@ -562,7 +560,7 @@ private function pullHelperImage(string $fullImageName): void 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 ac34d064e5..2442d5b066 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 ef16596807..4b208fc315 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 @@ public function handle(): void $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 0000000000..376cb85325 --- /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 4c65a711ff..2cc705e4ac 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 @@ public function handle(): void { 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/Help.php b/app/Livewire/Help.php index d6dc0d5213..68691c1cdc 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -47,7 +47,7 @@ public function submit() ] ); $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'; diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 2960ed2261..53673292ed 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -172,7 +172,7 @@ public function submitResend() public function copyFromInstanceSettings() { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->smtp_enabled) { $team = currentTeam(); $team->update([ diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 42c9357fdc..9cd3098855 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -109,7 +109,7 @@ public function restart() 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 543e645393..c052608995 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -91,10 +91,12 @@ public function mount() 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/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index d954436215..90419caed1 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -11,6 +11,8 @@ class ExecuteContainerCommand extends Component { + public $selected_container = 'default'; + public $container; public Collection $containers; @@ -83,11 +85,14 @@ public function loadContainers() $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 @@ public function loadContainers() } } 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 @@ public function loadContainers() #[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 @@ public function connectToContainer() 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/Settings/Index.php b/app/Livewire/Settings/Index.php index c529702589..754f0929bd 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 @@ public function checkManually() { 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 f9402fd7b6..ca0c9c1ae3 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -29,7 +29,7 @@ public function mount() 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 99b8f8d493..9240aa96d2 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 3eb8ea646d..4515df9a7d 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 75d7fd04a7..193b650ffe 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -99,7 +99,7 @@ public function mount() 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/Subscription/Index.php b/app/Livewire/Subscription/Index.php index c278bf58ee..df450cf7ef 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -23,7 +23,7 @@ public function mount() 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 55006745ac..dfa875a5a0 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 = []; @@ -304,7 +304,7 @@ public function failedTaskLink($task_uuid) '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); @@ -1150,7 +1150,7 @@ public function oldRawParser() 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/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 138775abad..9f8e4b342f 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -126,11 +126,6 @@ public function realValue(): Attribute $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 27a181ee43..bb3d1478b6 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -85,4 +85,17 @@ public function getTitleDisplayName(): string return "[{$instanceName}]"; } + + public function helperVersion(): Attribute + { + return Attribute::make( + get: function ($value) { + if (isDev()) { + return 'latest'; + } + + return $value; + } + ); + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index a1b4c08289..f896541adf 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -268,7 +268,7 @@ public function setupDefault404Redirect() 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"; @@ -1212,4 +1212,13 @@ public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null return $this; } + + public function storageCheck(): ?string + { + $commands = [ + 'df / --output=pcent | tr -cd 0-9', + ]; + + return instant_remote_process($commands, $this, false); + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index d236869baa..c9c086622f 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 = []; @@ -1095,7 +1095,22 @@ public function saveComposeConfigs() 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); + } + } + ray("echo \"{$env->key}={$real_value}\" >> .env"); + $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; + } } if ($sorted->count() === 0) { $commands[] = 'touch .env'; @@ -1105,7 +1120,7 @@ public function saveComposeConfigs() 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/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 549fc6cd32..cc7d76ebf2 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 8b1c02d398..3938a8da79 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 cd90918adb..8b4c2eef2d 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 @@ public function boot(): void ])->baseUrl($api_url); } }); - // if (! env('CI')) { - // View::share('instanceSettings', InstanceSettings::get()); - // } - } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 53a2e9281d..b916b62340 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -46,7 +46,7 @@ public function boot(): void 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 @@ public function boot(): void }); 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 8e14ef9eee..006b095cfb 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/shared.php b/bootstrap/helpers/shared.php index 350f168370..ffd53a99ad 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.'); @@ -970,7 +975,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 +1095,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 +1167,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 +1184,24 @@ 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 +1209,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array return $line; }); + $commands = $commands->map(function ($line) { $line = str($line); if (str($line)->contains('$(')) { @@ -1236,8 +1254,6 @@ function parseLineForSudo(string $command, Server $server): string function get_public_ips() { try { - echo "Refreshing public ips!\n"; - $settings = \App\Models\InstanceSettings::get(); [$first, $second] = Process::concurrently(function (Pool $pool) { $pool->path(__DIR__)->command('curl -4s https://ifconfig.io'); $pool->path(__DIR__)->command('curl -6s https://ifconfig.io'); @@ -1251,7 +1267,7 @@ function get_public_ips() return; } - $settings->update(['public_ipv4' => $ipv4]); + InstanceSettings::get()->update(['public_ipv4' => $ipv4]); } } catch (\Exception $e) { echo "Error: {$e->getMessage()}\n"; @@ -1266,7 +1282,7 @@ function get_public_ips() return; } - $settings->update(['public_ipv6' => $ipv6]); + InstanceSettings::get()->update(['public_ipv6' => $ipv6]); } } catch (\Throwable $e) { echo "Error: {$e->getMessage()}\n"; @@ -3266,7 +3282,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"; @@ -3286,6 +3310,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') { @@ -3828,6 +3863,21 @@ function convertComposeEnvironmentToArray($environment) { $convertedServiceVariables = collect([]); if (isAssociativeArray($environment)) { + if ($environment instanceof Collection) { + $changedEnvironment = collect([]); + $environment->each(function ($value, $key) use ($changedEnvironment) { + $parts = explode('=', $value, 2); + if (count($parts) === 2) { + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + $changedEnvironment->put($key, $realValue); + } else { + $changedEnvironment->put($key, $value); + } + }); + + return $changedEnvironment; + } $convertedServiceVariables = $environment; } else { foreach ($environment as $value) { @@ -3843,3 +3893,7 @@ function convertComposeEnvironmentToArray($environment) return $convertedServiceVariables; } +function instanceSettings() +{ + return InstanceSettings::get(); +} diff --git a/composer.json b/composer.json index 17432c5320..03adf9823a 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 fffb320d3c..420d87ec0b 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 0000000000..eae406ba77 --- /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 1e83eae9e5..5efd8968de 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // 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.347', + 'release' => '4.0.0-beta.348', // 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 6e7f911aef..09d7ab3e72 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/seeders/InstanceSettingsSeeder.php b/database/seeders/InstanceSettingsSeeder.php index c3182a2dd6..35fc8506ba 100644 --- a/database/seeders/InstanceSettingsSeeder.php +++ b/database/seeders/InstanceSettingsSeeder.php @@ -27,14 +27,14 @@ public function run(): void $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/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php index dc27d21b00..e2543ee02e 100644 --- a/database/seeders/PopulateSshKeysDirectorySeeder.php +++ b/database/seeders/PopulateSshKeysDirectorySeeder.php @@ -19,7 +19,6 @@ public function run() PrivateKey::chunk(100, function ($keys) { foreach ($keys as $key) { - echo 'Storing key: '.$key->name."\n"; $key->storeInFileSystem(); } }); diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index d0f0f10f40..206f04d6b6 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -101,18 +101,13 @@ public function run(): void } if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { - echo "Checking localhost key.\n"; $coolify_key_name = '@host.docker.internal'; $ssh_keys_directory = Storage::disk('ssh-keys')->files(); $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name)); - $found = PrivateKey::find(0); - if ($found) { - echo 'Private Key found in database.\n'; - if ($coolify_key) { - echo "SSH key found for the Coolify host machine (localhost).\n"; - } - } else { + $server = Server::find(0); + $found = $server->privateKey; + if (! $found) { if ($coolify_key) { $user = str($coolify_key)->before('@')->after('id.'); $coolify_key = Storage::disk('ssh-keys')->get($coolify_key); @@ -125,17 +120,7 @@ public function run(): void ]); $server->update(['user' => $user]); echo "SSH key found for the Coolify host machine (localhost).\n"; - } else { - PrivateKey::create( - [ - 'id' => 0, - 'team_id' => 0, - 'name' => 'localhost\'s key', - 'description' => 'The private key for the Coolify host machine (localhost).', - 'private_key' => 'Paste here you private key!!', - ] - ); echo "No SSH key found for the Coolify host machine (localhost).\n"; echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n"; echo "Your localhost connection won't work until then."; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 9777c52dfc..95031deab8 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.2' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js index d6b820583e..8658ecdf83 100755 --- a/docker/coolify-realtime/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -132,13 +132,10 @@ async function handleCommand(ws, command, userId) { // 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'); @@ -147,6 +144,7 @@ async function handleCommand(ws, command, userId) { // when parent closes ptyProcess.onExit(({ exitCode, signal }) => { console.error(`Process exited with code ${exitCode} and signal ${signal}`); + ws.send('pty-exited'); userSession.isActive = false; }); diff --git a/openapi.yaml b/openapi.yaml index ce0503e1fa..91d5c14439 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.' + 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 configurations and volumes.' + 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.' + 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 configurations and volumes.' + 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 3eb270a2a0..29f6a0f822 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -110,7 +110,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 74cbecb196..6f7cea58e4 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.348" }, "nightly": { - "version": "4.0.0-beta.348" + "version": "4.0.0-beta.349" }, "helper": { "version": "1.0.1" }, "realtime": { - "version": "1.0.1" + "version": "1.0.2" } } } diff --git a/public/svgs/mailpit.svg b/public/svgs/mailpit.svg new file mode 100644 index 0000000000..e569e71ccd --- /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 0000000000..bd915e77a5 --- /dev/null +++ b/public/svgs/mixpost.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 21854ed638..fb69628c08 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -125,6 +125,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 +140,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 +154,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 +182,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/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index dcb760ef94..d82a046d37 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -151,9 +151,9 @@ class="relative w-auto h-auto"> @endif @endif