diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml deleted file mode 100644 index b06c9e97c..000000000 --- a/.github/workflows/browser-tests.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Dusk -on: - push: - branches: [ "not-existing" ] -jobs: - dusk: - runs-on: ubuntu-latest - - services: - redis: - image: redis - env: - REDIS_HOST: localhost - REDIS_PORT: 6379 - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v4 - - name: Set up PostgreSQL - run: | - sudo systemctl start postgresql - sudo -u postgres psql -c "CREATE DATABASE coolify;" - sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';" - sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';" - sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';" - sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';" - sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;" - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - - name: Copy .env - run: cp .env.dusk.ci .env - - name: Install Dependencies - run: composer install --no-progress --prefer-dist --optimize-autoloader - - name: Generate key - run: php artisan key:generate - - name: Install Chrome binaries - run: php artisan dusk:chrome-driver --detect - - name: Start Chrome Driver - run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 & - - name: Build assets - run: npm install && npm run build - - name: Run Laravel Server - run: php artisan serve --no-reload & - - name: Execute tests - run: php artisan dusk - - name: Upload Screenshots - if: failure() - uses: actions/upload-artifact@v4 - with: - name: screenshots - path: tests/Browser/screenshots - - name: Upload Console Logs - if: failure() - uses: actions/upload-artifact@v4 - with: - name: console - path: tests/Browser/console diff --git a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml index a3c299b5e..194984ddc 100644 --- a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml +++ b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml @@ -21,7 +21,7 @@ jobs: async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) { try { - if (isFromPR && prBaseBranch !== 'main') { + if (isFromPR && prBaseBranch !== 'v4.x') { return; } @@ -70,7 +70,7 @@ jobs: if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { const pr = context.payload.pull_request; await processIssue(pr.number); - if (pr.merged && pr.base.ref === 'main' && pr.body) { + if (pr.merged && pr.base.ref === 'v4.x' && pr.body) { const issueReferences = pr.body.match(/#(\d+)/g); if (issueReferences) { for (const reference of issueReferences) { diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 78c888a01..56c3eaa17 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -2,7 +2,7 @@ name: Coolify Helper Image on: push: - branches: [ "main" ] + branches: [ "v4.x" ] paths: - .github/workflows/coolify-helper.yml - docker/coolify-helper/Dockerfile diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index ad0878205..cd1f002b8 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -2,7 +2,7 @@ name: Production Build (v4) on: push: - branches: ["main"] + branches: ["v4.x"] paths-ignore: - .github/workflows/coolify-helper.yml - .github/workflows/coolify-helper-next.yml diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index d3af14144..d00621cc2 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -2,7 +2,7 @@ name: Coolify Realtime on: push: - branches: [ "main" ] + branches: [ "v4.x" ] paths: - .github/workflows/coolify-realtime.yml - docker/coolify-realtime/Dockerfile diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index ff6b553df..09b1e9421 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -2,7 +2,10 @@ name: Staging Build on: push: - branches-ignore: ["main", "v3"] + branches-ignore: + - v4.x + - v3.x + - '**v5.x**' paths-ignore: - .github/workflows/coolify-helper.yml - .github/workflows/coolify-helper-next.yml diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index ee5af6b3d..935a88721 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -2,7 +2,7 @@ name: Generate Changelog on: push: - branches: [ main ] + branches: [ v4.x ] workflow_dispatch: permissions: @@ -33,4 +33,4 @@ jobs: git config user.email 'github-actions[bot]@users.noreply.github.com' git add CHANGELOG.md git commit -m "docs: update changelog" - git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git main + git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git v4.x diff --git a/CHANGELOG.md b/CHANGELOG.md index eae126054..9fdf59683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,100 @@ All notable changes to this project will be documented in this file. -## [unreleased] +## [4.0.0-beta.417] - 2025-05-07 + +### 📚 Documentation + +- Update changelog + +## [4.0.0-beta.416] - 2025-05-05 + +### 📚 Documentation + +- Update changelog +- Update changelog + +## [4.0.0-beta.415] - 2025-04-29 + +### 📚 Documentation + +- Update changelog + +## [4.0.0-beta.413] - 2025-04-28 + +### 💼 Other + +- Adjust Workflows for v5 (#5689) + +### 📚 Documentation + +- Update changelog +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(workflows)* Adjust workflow for announcement + +## [4.0.0-beta.412] - 2025-04-23 + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files + +## [4.0.0-beta.411] - 2025-04-23 + +### 🚀 Features + +- *(deployment)* Add repository_project_id handling for private GitHub apps and clean up unused Caddy label logic +- *(api)* Enhance OpenAPI specifications with token variable and additional key attributes +- *(docker)* Add HTTP Basic Authentication support and enhance hostname parsing in Docker run conversion +- *(api)* Add HTTP Basic Authentication fields to OpenAPI specifications and enhance PrivateKey model descriptions ### 🐛 Bug Fixes +- *(backup-edit)* Conditionally enable S3 checkbox based on available validated S3 storage +- *(source)* Update no sources found message for clarity +- *(api)* Correct middleware for service update route to ensure proper permissions +- *(api)* Handle JSON response in service creation and update methods for improved error handling +- Add 201 json code to servers validate api response +- *(docker)* Ensure password hashing only occurs when HTTP Basic Authentication is enabled +- *(docker)* Enhance hostname and GPU option validation in Docker run to compose conversion + +### 🚜 Refactor + +- *(jobs)* Comment out unused Caddy label handling in ApplicationDeploymentJob and simplify proxy path logic in Server model +- *(database)* Simplify database type checks in ServiceDatabase and enhance image validation in Docker helper +- *(shared)* Remove unused ray debugging statement from newParser function +- *(applications)* Remove redundant error response in create_env method +- *(api)* Restructure routes to include versioning and maintain existing feedback endpoint +- *(api)* Remove token variable from OpenAPI specifications for clarity +- *(environment-variables)* Remove protected variable checks from delete methods for cleaner logic +- *(http-basic-auth)* Rename 'http_basic_auth_enable' to 'http_basic_auth_enabled' across application files for consistency +- *(docker)* Remove debug statement and enhance hostname handling in Docker run conversion +- *(server)* Simplify proxy path logic and remove unnecessary conditions + +### 📚 Documentation + +- Update changelog +- Update changelog +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files + +## [4.0.0-beta.410] - 2025-04-18 + +### 🚀 Features + +- Add HTTP Basic Authentication +- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README +- *(core)* Enable magic env variables for compose based applications + +### 🐛 Bug Fixes + +- *(application)* Append base directory to git branch URLs for improved path handling +- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON - *(navbar)* Update error message link to use route for environment variables navigation - Unsend template - Replace ports with expose @@ -19,14 +109,33 @@ All notable changes to this project will be documented in this file. - Update changelog +### ⚙️ Miscellaneous Tasks + +- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files +- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service + +## [4.0.0-beta.409] - 2025-04-16 + +### 🐛 Bug Fixes + +- *(parser)* Transform associative array labels into key=value format for better compatibility +- *(redis)* Update username and password input handling to clarify database sync requirements +- *(source)* Update connected source display to handle cases with no source connected + +### 🚜 Refactor + +- *(source)* Conditionally display connected source and change source options based on private key presence + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files + ## [4.0.0-beta.408] - 2025-04-14 ### 🚀 Features - *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation - *(subscription)* Enhance subscription management with loading states and Stripe status checks -- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README -- *(core)* Enable magic env variables for compose based applications ### 🐛 Bug Fixes @@ -36,11 +145,6 @@ All notable changes to this project will be documented in this file. - *(mongodb)* Also apply custom config when SSL is enabled - *(templates)* Correct casing of denoKV references in service templates and YAML files - *(deployment)* Handle missing destination in deployment process to prevent errors -- *(parser)* Transform associative array labels into key=value format for better compatibility -- *(redis)* Update username and password input handling to clarify database sync requirements -- *(source)* Update connected source display to handle cases with no source connected -- *(application)* Append base directory to git branch URLs for improved path handling -- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON ### 💼 Other @@ -52,7 +156,6 @@ All notable changes to this project will be documented in this file. - *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency - *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience - *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance -- *(source)* Conditionally display connected source and change source options based on private key presence ### 📚 Documentation @@ -65,9 +168,6 @@ All notable changes to this project will be documented in this file. - *(versions)* Update nightly version to 4.0.0-beta.410 - *(pre-commit)* Remove OpenAPI generation command from pre-commit hook - *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json -- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files -- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files -- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service ## [4.0.0-beta.407] - 2025-04-09 @@ -128,6 +228,7 @@ All notable changes to this project will be documented in this file. ### 🚀 Features - *(api)* Update OpenAPI spec for services (#5448) +- *(proxy)* Enhance proxy handling and port conflict detection ### 🐛 Bug Fixes @@ -140,6 +241,10 @@ All notable changes to this project will be documented in this file. ### 📚 Documentation +- Update changelog +- Update changelog +- Update changelog +- Update changelog - Update changelog - Update changelog - Update changelog @@ -155,15 +260,12 @@ All notable changes to this project will be documented in this file. ### 🚀 Features -- *(proxy)* Enhance proxy handling and port conflict detection - *(lang)* Added Azerbaijani language updated turkish language. (#5497) - *(lang)* Added Portuguese from Brazil language (#5500) - *(lang)* Add Indonesian language translations (#5513) ### 🐛 Bug Fixes -- *(database)* Custom config for MongoDB (#5471) -- *(ui)* Instance Backup settings - *(docs)* Comment out execute for now - *(installation)* Mount the docker config - *(installation)* Path to config file for docker login @@ -173,13 +275,10 @@ All notable changes to this project will be documented in this file. - *(docs)* Contribute service url (#5517) - *(proxy)* Proxy restart does not work on domain - *(ui)* Only show copy button on https +- *(database)* Custom config for MongoDB (#5471) ### 📚 Documentation -- Update changelog -- Update changelog -- Update changelog -- Update changelog - Update changelog - Update changelog - Update changelog @@ -187,10 +286,9 @@ All notable changes to this project will be documented in this file. ### ⚙️ Miscellaneous Tasks -- *(versions)* Bump version to 403 (#5520) -- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404 - *(service)* Remove unused code in Bugsink service - *(versions)* Update version to 404 +- *(versions)* Bump version to 403 (#5520) - *(versions)* Bump version to 404 ## [4.0.0-beta.402] - 2025-04-01 @@ -210,6 +308,7 @@ All notable changes to this project will be documented in this file. - *(DeployController)* Cast 'pr' query parameter to integer - *(deploy)* Validate team ID before deployment - *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424) +- *(ui)* Instance Backup settings ### 🚜 Refactor @@ -223,6 +322,7 @@ All notable changes to this project will be documented in this file. - *(service)* Add google variables to plausible.yaml (#5429) - *(service)* Update authentik.yaml versions (#5373) - *(core)* Remove redocs +- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404 ## [4.0.0-beta.401] - 2025-03-28 @@ -293,14 +393,6 @@ All notable changes to this project will be documented in this file. ### 🚀 Features -- *(github-source)* Enhance GitHub App configuration with manual and private key support -- *(ui)* Improve GitHub repository selection and styling -- *(database)* Implement two-step confirmation for database deletion -- *(assets)* Add new SVG logo for Coolify -- *(install)* Enhance Docker address pool configuration and validation -- *(install)* Improve Docker address pool management and service restart logic -- *(install)* Add missing env variable to install script -- *(LocalFileVolume)* Add binary file detection and update UI logic - *(service)* Neon - *(migration)* Add `ssl_certificates` table and model - *(migration)* Add ssl setting to `standalone_postgresqls` table @@ -342,6 +434,14 @@ All notable changes to this project will be documented in this file. - *(ssl)* Improve Redis and remove modes - Full SSL support for DrangonflyDB - SSL notification +- *(github-source)* Enhance GitHub App configuration with manual and private key support +- *(ui)* Improve GitHub repository selection and styling +- *(database)* Implement two-step confirmation for database deletion +- *(assets)* Add new SVG logo for Coolify +- *(install)* Enhance Docker address pool configuration and validation +- *(install)* Improve Docker address pool management and service restart logic +- *(install)* Add missing env variable to install script +- *(LocalFileVolume)* Add binary file detection and update UI logic - *(templates)* Change glance for v0.7 - *(templates)* Add Freescout service template - *(service)* Add Evolution API template @@ -359,18 +459,6 @@ All notable changes to this project will be documented in this file. - *(api)* Docker compose based apps creationg through api - *(database)* Improve database type detection for Supabase Postgres images -- *(ui)* Correct grammatical error in 404 page -- *(seeder)* Update GitHub app name in GithubAppSeeder -- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration -- *(domain)* Dispatch refreshStatus event after successful domain update -- *(database)* Correct container name generation for service databases -- *(database)* Limit container name length for database proxy -- *(database)* Handle unsupported database types in StartDatabaseProxy -- *(database)* Simplify container name generation in StartDatabaseProxy -- *(install)* Handle potential errors in Docker address pool configuration -- *(backups)* Retention settings -- *(redis)* Set default redis_username for new instances -- *(core)* Improve instantSave logic and error handling - *(ssl)* Permission of ssl crt and key inside the container - *(ui)* Make sure file mounts do not showing the encrypted values - *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert @@ -410,6 +498,18 @@ All notable changes to this project will be documented in this file. - *(ssl)* Add `--tls` arg to DrangflyDB - *(notification)* Always send SSL notifications - *(database)* Change default value of enable_ssl to false for multiple tables +- *(ui)* Correct grammatical error in 404 page +- *(seeder)* Update GitHub app name in GithubAppSeeder +- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration +- *(domain)* Dispatch refreshStatus event after successful domain update +- *(database)* Correct container name generation for service databases +- *(database)* Limit container name length for database proxy +- *(database)* Handle unsupported database types in StartDatabaseProxy +- *(database)* Simplify container name generation in StartDatabaseProxy +- *(install)* Handle potential errors in Docker address pool configuration +- *(backups)* Retention settings +- *(redis)* Set default redis_username for new instances +- *(core)* Improve instantSave logic and error handling - *(general)* Correct link to framework specific documentation - *(core)* Redirect healthcheck route for dockercompose applications - *(api)* Use name from request payload @@ -458,7 +558,6 @@ All notable changes to this project will be documented in this file. ### ⚙️ Miscellaneous Tasks -- *(supabase)* Update Supabase service template and Postgres image version - *(migration)* Remove unused columns - *(ssl)* Improve code in ssl helper - *(migration)* Ssl cert and key should not be nullable @@ -466,6 +565,7 @@ All notable changes to this project will be documented in this file. - Rename ca crt folder to ssl - *(ui)* Improve valid until handling - Improve code quality suggested by code rabbit +- *(supabase)* Update Supabase service template and Postgres image version - *(versions)* Update version numbers for coolify and nightly ## [4.0.0-beta.398] - 2025-03-01 @@ -858,14 +958,6 @@ All notable changes to this project will be documented in this file. ### 🚀 Features -- New ServerReachabilityChanged event -- Use new ServerReachabilityChanged event instead of isDirty -- Add infomaniak oauth -- Add server disk usage check frequency -- Add environment_uuid support and update API documentation -- Add service/resource/project labels -- Add coolify.environment label -- Add database subtype - Able to import full db backups for pg/mysql/mariadb - Restore backup from server file - Docker volume data cloning @@ -901,35 +993,6 @@ All notable changes to this project will be documented in this file. ### 🐛 Bug Fixes -- Fallback for copy button -- Copy the right text -- Maybe fallback is now working -- Only show copy button on secure context -- Render html on error page correctly -- Invalid API response on missing project -- Applications API response code + schema -- Applications API writing to unavailable models -- If an init script is renamed the old version is still on the server -- Oauthseeder -- Compose loading seq -- Resource clone name + volume name generation -- Update Dockerfile entrypoint path to /etc/entrypoint.d -- Debug mode -- Unreachable notifications -- Remove duplicated ServerCheckJob call -- Few fixes and use new ServerReachabilityChanged event -- Use serverStatus not just status -- Oauth seeder -- Service ui structure -- Check port 8080 and fallback to 80 -- Refactor database view -- Always use docker cleanup frequency -- Advanced server UI -- Html css -- Fix domain being override when update application -- Use nixpacks predefined build variables, but still could update the default values from Coolify -- Use local monaco-editor instead of Cloudflare -- N8n timezone - Compose envs - Scheduled tasks and backups are executed by server timezone. - Show backup timezone on the UI @@ -1029,7 +1092,6 @@ All notable changes to this project will be documented in this file. ### 🚜 Refactor -- Rename `coolify.environment` to `coolify.environmentName` - Rename parameter in DatabaseBackupJob for clarity - Improve checkbox component accessibility and styling - Remove unused tags method from ApplicationDeploymentJob @@ -1045,9 +1107,6 @@ All notable changes to this project will be documented in this file. ### ⚙️ Miscellaneous Tasks -- Regenerate API spec, removing notification fields -- Remove ray debugging -- Version ++ - Improve Penpot healthchecks - Switch up readonly lables to make more sense - Remove unused computed fields @@ -1071,11 +1130,44 @@ All notable changes to this project will be documented in this file. ### 🚀 Features +- New ServerReachabilityChanged event +- Use new ServerReachabilityChanged event instead of isDirty +- Add infomaniak oauth +- Add server disk usage check frequency +- Add environment_uuid support and update API documentation +- Add service/resource/project labels +- Add coolify.environment label +- Add database subtype - Migrate to new encryption options - New encryption options ### 🐛 Bug Fixes +- Render html on error page correctly +- Invalid API response on missing project +- Applications API response code + schema +- Applications API writing to unavailable models +- If an init script is renamed the old version is still on the server +- Oauthseeder +- Compose loading seq +- Resource clone name + volume name generation +- Update Dockerfile entrypoint path to /etc/entrypoint.d +- Debug mode +- Unreachable notifications +- Remove duplicated ServerCheckJob call +- Few fixes and use new ServerReachabilityChanged event +- Use serverStatus not just status +- Oauth seeder +- Service ui structure +- Check port 8080 and fallback to 80 +- Refactor database view +- Always use docker cleanup frequency +- Advanced server UI +- Html css +- Fix domain being override when update application +- Use nixpacks predefined build variables, but still could update the default values from Coolify +- Use local monaco-editor instead of Cloudflare +- N8n timezone - Smtp encryption - Bind() to 0.0.0.0:80 failed - Oauth seeder @@ -1085,11 +1177,15 @@ All notable changes to this project will be documented in this file. - Error message - Update healthcheck and port configurations to use port 8080 -## [4.0.0-beta.379] - 2024-12-13 +### 🚜 Refactor -### 🐛 Bug Fixes +- Rename `coolify.environment` to `coolify.environmentName` -- Saving oauth +### ⚙️ Miscellaneous Tasks + +- Regenerate API spec, removing notification fields +- Remove ray debugging +- Version ++ ## [4.0.0-beta.378] - 2024-12-13 @@ -1098,6 +1194,11 @@ All notable changes to this project will be documented in this file. - Monaco editor light and dark mode switching - Service status indicator + oauth saving - Socialite for azure and authentik +- Saving oauth +- Fallback for copy button +- Copy the right text +- Maybe fallback is now working +- Only show copy button on secure context ## [4.0.0-beta.377] - 2024-12-13 diff --git a/LICENSE b/LICENSE index 86c87eba2..e3cc59461 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2022] [Andras Bacsai] + Copyright [2025] [Andras Bacsai] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 5f583df75..cf3dc21c3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ Thank you so much! * [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform * [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform * [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions +* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure +* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity ## Small Sponsors diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 642b4ba45..f5f16f3fe 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -30,7 +30,7 @@ class StopApplication $application->stopContainers($containersToStop, $server); if ($application->build_pack === 'dockercompose') { - $application->delete_connected_networks($application->uuid); + $application->deleteConnectedNetworks(); } if ($dockerCleanup) { diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 926d30fe6..8ce9aa1f2 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -85,7 +85,6 @@ class RunRemoteProcess ]); $processResult = $process->wait(); - // $processResult = Process::timeout($timeout)->run($this->getCommand(), $this->handleOutput(...)); if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) { $status = ProcessStatus::ERROR; } else { diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index de4eaa31f..161e7dad7 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -11,7 +11,6 @@ use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use Illuminate\Support\Facades\Process; use Lorisleiva\Actions\Concerns\AsAction; class StopDatabase @@ -25,7 +24,7 @@ class StopDatabase return 'Server is not functional'; } - $this->stopContainer($database, $database->uuid, 300); + $this->stopContainer($database, $database->uuid, 30); if ($isDeleteOperation) { if ($dockerCleanup) { CleanupDocker::dispatch($server, true); @@ -39,37 +38,12 @@ class StopDatabase return 'Database stopped successfully'; } - private function stopContainer($database, string $containerName, int $timeout = 300): void + private function stopContainer($database, string $containerName, int $timeout = 30): void { $server = $database->destination->server; - - $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - - $startTime = time(); - while ($process->running()) { - if (time() - $startTime >= $timeout) { - $this->forceStopContainer($containerName, $server); - break; - } - usleep(100000); - } - - $this->removeContainer($containerName, $server); - } - - private function forceStopContainer(string $containerName, $server): void - { - instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); - } - - private function removeContainer(string $containerName, $server): void - { - instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); - } - - private function deleteConnectedNetworks($uuid, $server) - { - instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); - instant_remote_process(["docker network rm {$uuid}"], $server, false); + instant_remote_process(command: [ + "docker stop --time=$timeout $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); } } diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php index a5dcc6cf4..75b22d236 100644 --- a/app/Actions/Proxy/StopProxy.php +++ b/app/Actions/Proxy/StopProxy.php @@ -3,54 +3,27 @@ namespace App\Actions\Proxy; use App\Models\Server; -use Carbon\Carbon; -use Illuminate\Process\InvokedProcess; -use Illuminate\Support\Facades\Process; use Lorisleiva\Actions\Concerns\AsAction; class StopProxy { use AsAction; - public function handle(Server $server, bool $forceStop = true) + public function handle(Server $server, bool $forceStop = true, int $timeout = 30) { try { $containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; - $timeout = 30; - $process = $this->stopContainer($containerName, $timeout); + instant_remote_process(command: [ + "docker stop --time=$timeout $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); - $startTime = Carbon::now()->getTimestamp(); - while ($process->running()) { - if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { - $this->forceStopContainer($containerName, $server); - break; - } - usleep(100000); - } - - $this->removeContainer($containerName, $server); - } catch (\Throwable $e) { - return handleError($e); - } finally { $server->proxy->force_stop = $forceStop; $server->proxy->status = 'exited'; $server->save(); + } catch (\Throwable $e) { + return handleError($e); } } - - private function stopContainer(string $containerName, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - private function forceStopContainer(string $containerName, Server $server) - { - instant_remote_process(["docker kill $containerName"], $server, throwError: false); - } - - private function removeContainer(string $containerName, Server $server) - { - instant_remote_process(["docker rm -f $containerName"], $server, throwError: false); - } } diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php index 75b8501f3..41781d7fc 100644 --- a/app/Actions/Server/ServerCheck.php +++ b/app/Actions/Server/ServerCheck.php @@ -99,7 +99,8 @@ class ServerCheck return data_get($value, 'Name') === '/coolify-proxy'; } })->first(); - if (! $foundProxyContainer) { + $proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited'); + if (! $foundProxyContainer || $proxyStatus !== 'running') { try { $shouldStart = CheckProxy::run($this->server); if ($shouldStart) { diff --git a/app/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php index 0d28a0099..f72f23696 100644 --- a/app/Actions/Server/StartLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -15,19 +15,18 @@ class StartLogDrain { if ($server->settings->is_logdrain_newrelic_enabled) { $type = 'newrelic'; - StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_highlight_enabled) { $type = 'highlight'; - StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_axiom_enabled) { $type = 'axiom'; - StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_custom_enabled) { $type = 'custom'; - StopLogDrain::run($server); } else { $type = 'none'; } + if ($type !== 'none') { + StopLogDrain::run($server); + } try { if ($type === 'none') { return 'No log drain is enabled.'; @@ -186,7 +185,6 @@ Files: "echo '{$compose}' | base64 -d | tee $compose_path > /dev/null", "echo '{$readme}' | base64 -d | tee $readme_path > /dev/null", "test -f $config_path/.env && rm $config_path/.env", - ]; if ($type === 'newrelic') { $add_envs_command = [ diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 9b87454da..372ffe397 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -48,7 +48,7 @@ class DeleteService } if ($deleteConnectedNetworks) { - $service->delete_connected_networks($service->uuid); + $service->deleteConnectedNetworks(); } instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false); @@ -56,7 +56,7 @@ class DeleteService throw new \Exception($e->getMessage()); } finally { if ($deleteConfigurations) { - $service->delete_configurations(); + $service->deleteConfigurations(); } foreach ($service->applications()->get() as $application) { $application->forceDelete(); diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index e16dd5616..4bc2ecf03 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -24,7 +24,7 @@ class StopService $service->stopContainers($containersToStop, $server); if ($isDeleteOperation) { - $service->delete_connected_networks($service->uuid); + $service->deleteConnectedNetworks(); if ($dockerCleanup) { CleanupDocker::dispatch($server, true); } diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index e16a82be4..315d1adc7 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -13,17 +13,20 @@ class CleanupRedis extends Command public function handle() { - $prefix = config('database.redis.options.prefix'); - - $keys = Redis::connection()->keys('*:laravel*'); - collect($keys)->each(function ($key) use ($prefix) { + $redis = Redis::connection('horizon'); + $keys = $redis->keys('*'); + $prefix = config('horizon.prefix'); + foreach ($keys as $key) { $keyWithoutPrefix = str_replace($prefix, '', $key); - Redis::connection()->del($keyWithoutPrefix); - }); + $type = $redis->command('type', [$keyWithoutPrefix]); - $queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*'); - collect($queueOverlaps)->each(function ($key) { - Redis::connection()->del($key); - }); + if ($type === 5) { + $data = $redis->command('hgetall', [$keyWithoutPrefix]); + $status = data_get($data, 'status'); + if ($status === 'completed') { + $redis->command('del', [$keyWithoutPrefix]); + } + } + } } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a6f24aaad..c03475647 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -51,6 +51,7 @@ class Kernel extends ConsoleKernel } // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly(); + $this->scheduleInstance->command('cleanup:redis')->hourly(); if (isDev()) { // Instance Jobs diff --git a/app/Events/ApplicationStatusChanged.php b/app/Events/ApplicationStatusChanged.php index 4433248aa..a20abac0f 100644 --- a/app/Events/ApplicationStatusChanged.php +++ b/app/Events/ApplicationStatusChanged.php @@ -12,21 +12,22 @@ class ApplicationStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = auth()->user()->currentTeam()->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/BackupCreated.php b/app/Events/BackupCreated.php index 45b2aacb7..bc1ecee0d 100644 --- a/app/Events/BackupCreated.php +++ b/app/Events/BackupCreated.php @@ -12,21 +12,22 @@ class BackupCreated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = auth()->user()->currentTeam()->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/CloudflareTunnelConfigured.php b/app/Events/CloudflareTunnelConfigured.php index 3d7076d0d..b40c7d070 100644 --- a/app/Events/CloudflareTunnelConfigured.php +++ b/app/Events/CloudflareTunnelConfigured.php @@ -12,21 +12,22 @@ class CloudflareTunnelConfigured implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = auth()->user()->currentTeam()->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php index 96b35a5ca..8099b080d 100644 --- a/app/Events/DatabaseProxyStopped.php +++ b/app/Events/DatabaseProxyStopped.php @@ -7,27 +7,27 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Auth; class DatabaseProxyStopped implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = Auth::user()?->currentTeam()?->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php index 913b21bc2..d019da68c 100644 --- a/app/Events/DatabaseStatusChanged.php +++ b/app/Events/DatabaseStatusChanged.php @@ -13,28 +13,24 @@ class DatabaseStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $userId = null; + public int|string|null $userId = null; public function __construct($userId = null) { if (is_null($userId)) { $userId = Auth::id() ?? null; } - if (is_null($userId)) { - return false; - } - $this->userId = $userId; } public function broadcastOn(): ?array { - if (! is_null($this->userId)) { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if (is_null($this->userId)) { + return []; } - return null; + return [ + new PrivateChannel("user.{$this->userId}"), + ]; } } diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php index 57004cf4c..756cb1352 100644 --- a/app/Events/FileStorageChanged.php +++ b/app/Events/FileStorageChanged.php @@ -12,18 +12,22 @@ class FileStorageChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/ProxyStatusChanged.php b/app/Events/ProxyStatusChanged.php index 35eedef70..6099d25ef 100644 --- a/app/Events/ProxyStatusChanged.php +++ b/app/Events/ProxyStatusChanged.php @@ -12,21 +12,22 @@ class ProxyStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = auth()->user()->currentTeam()->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php index c8b5547f6..9884c278b 100644 --- a/app/Events/ScheduledTaskDone.php +++ b/app/Events/ScheduledTaskDone.php @@ -12,21 +12,22 @@ class ScheduledTaskDone implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct($teamId = null) { - if (is_null($teamId)) { - $teamId = auth()->user()->currentTeam()->id ?? null; - } - if (is_null($teamId)) { - throw new \Exception('Team id is null'); + if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; } $this->teamId = $teamId; } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index 3950022e1..f5a30e874 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -13,27 +13,24 @@ class ServiceStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public ?string $userId = null; + public int|string|null $userId = null; public function __construct($userId = null) { if (is_null($userId)) { $userId = Auth::id() ?? null; } - if (is_null($userId)) { - return false; - } $this->userId = $userId; } public function broadcastOn(): ?array { - if (! is_null($this->userId)) { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if (is_null($this->userId)) { + return []; } - return null; + return [ + new PrivateChannel("user.{$this->userId}"), + ]; } } diff --git a/app/Events/TestEvent.php b/app/Events/TestEvent.php index 2cc6683dc..c6669c937 100644 --- a/app/Events/TestEvent.php +++ b/app/Events/TestEvent.php @@ -12,15 +12,21 @@ class TestEvent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $teamId; + public ?int $teamId = null; public function __construct() { - $this->teamId = auth()->user()->currentTeam()->id; + if (auth()->check() && auth()->user()->currentTeam()) { + $this->teamId = auth()->user()->currentTeam()->id; + } } public function broadcastOn(): array { + if (is_null($this->teamId)) { + return []; + } + return [ new PrivateChannel("team.{$this->teamId}"), ]; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c29093ce0..bebec64cf 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -27,7 +27,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Process; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; @@ -1377,7 +1376,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function check_git_if_build_needed() { - if ($this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) { + if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) { $repository = githubApi($this->source, "repos/{$this->customRepository}"); $data = data_get($repository, 'data'); if (isset($data->id)) { @@ -2246,43 +2245,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } - private function graceful_shutdown_container(string $containerName, int $timeout = 300) + private function graceful_shutdown_container(string $containerName, int $timeout = 30) { try { - $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - - $startTime = time(); - while ($process->running()) { - if (time() - $startTime >= $timeout) { - $this->execute_remote_command( - ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true] - ); - break; - } - usleep(100000); - } - - $isRunning = $this->execute_remote_command( - ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true] - ) === 'true'; - - if ($isRunning) { - $this->execute_remote_command( - ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true] - ); - } - } catch (\Exception $error) { + $this->execute_remote_command( + ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], + ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + } catch (Exception $error) { $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); } - - $this->remove_container($containerName); - } - - private function remove_container(string $containerName) - { - $this->execute_remote_command( - ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] - ); } private function stop_running_container(bool $force = false) diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 008492342..53e344daf 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -23,7 +23,7 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho public function middleware(): array { - return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)]; + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; } public function handle(): void diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 3276711c5..5e8dc581a 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -390,7 +390,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $commands[] = 'mkdir -p '.$this->backup_dir; $backupCommand = 'docker exec'; if ($this->postgres_password) { - $backupCommand .= " -e PGPASSWORD=$this->postgres_password"; + $backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\""; } if ($this->backup->dump_all) { $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location"; diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 9fd46db77..408bb2a7a 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -42,10 +42,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public function handle() { try { - $persistentStorages = collect(); switch ($this->resource->type()) { case 'application': - $persistentStorages = $this->resource?->persistentStorages()?->get(); StopApplication::run($this->resource, previewDeployments: true); break; case 'standalone-postgresql': @@ -56,53 +54,52 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue case 'standalone-keydb': case 'standalone-dragonfly': case 'standalone-clickhouse': - $persistentStorages = $this->resource?->persistentStorages()?->get(); StopDatabase::run($this->resource, true); break; case 'service': StopService::run($this->resource, true); DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); - break; - } - if ($this->deleteVolumes && $this->resource->type() !== 'service') { - $this->resource->delete_volumes($persistentStorages); - $this->resource->persistentStorages()->delete(); + return; } - $isDatabase = $this->resource instanceof StandalonePostgresql - || $this->resource instanceof StandaloneRedis - || $this->resource instanceof StandaloneMongodb - || $this->resource instanceof StandaloneMysql - || $this->resource instanceof StandaloneMariadb - || $this->resource instanceof StandaloneKeydb - || $this->resource instanceof StandaloneDragonfly - || $this->resource instanceof StandaloneClickhouse; if ($this->deleteConfigurations) { - $this->resource->delete_configurations(); // rename to FileStorages - $this->resource->fileStorages()->delete(); + $this->resource->deleteConfigurations(); } + if ($this->deleteVolumes) { + $this->resource->deleteVolumes(); + $this->resource->persistentStorages()->delete(); + } + $this->resource->fileStorages()->delete(); + + $isDatabase = $this->resource instanceof StandalonePostgresql + || $this->resource instanceof StandaloneRedis + || $this->resource instanceof StandaloneMongodb + || $this->resource instanceof StandaloneMysql + || $this->resource instanceof StandaloneMariadb + || $this->resource instanceof StandaloneKeydb + || $this->resource instanceof StandaloneDragonfly + || $this->resource instanceof StandaloneClickhouse; + if ($isDatabase) { $this->resource->sslCertificates()->delete(); $this->resource->scheduledBackups()->delete(); - $this->resource->environment_variables()->delete(); $this->resource->tags()->detach(); } + $this->resource->environment_variables()->delete(); - $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); - if (($this->dockerCleanup || $isDatabase) && $server) { - CleanupDocker::dispatch($server, true); - } - - if ($this->deleteConnectedNetworks && ! $isDatabase) { - $this->resource?->delete_connected_networks($this->resource->uuid); + if ($this->deleteConnectedNetworks && $this->resource->type() === 'application') { + $this->resource->deleteConnectedNetworks(); } } catch (\Throwable $e) { throw $e; } finally { $this->resource->forceDelete(); if ($this->dockerCleanup) { - CleanupDocker::dispatch($server, true); + $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); + if ($server) { + CleanupDocker::dispatch($server, true); + } } Artisan::queue('cleanup:stucked-resources'); } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 7e246649d..05a4aa8de 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -31,7 +31,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping($this->server->uuid))->expireAfter(600)]; + return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; } public function __construct(public Server $server, public bool $manualCleanup = false) {} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 4d40240f9..93b203fcb 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -71,7 +71,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping($this->server->uuid))->expireAfter(30)]; + return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; } public function backoff(): int diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php index 4e1ade0da..7fc716f70 100644 --- a/app/Jobs/RestartProxyJob.php +++ b/app/Jobs/RestartProxyJob.php @@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; + return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; } public function __construct(public Server $server) {} diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index ffa298390..9818d5c6a 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; + return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; } public function __construct(public Server $server) {} diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index d89f2b970..edbdd25fe 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -51,7 +51,7 @@ class Dashboard extends Component public function navigateToProject($projectUuid) { - return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), true); + return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), navigate: false); } public function render() diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 475d2dfa8..5b0ae12ef 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -100,7 +100,7 @@ class Heading extends Component 'application_uuid' => $this->parameters['application_uuid'], 'deployment_uuid' => $this->deploymentUuid, 'environment_uuid' => $this->parameters['environment_uuid'], - ], navigate: true); + ], navigate: false); } protected function setDeploymentUuid() @@ -147,7 +147,7 @@ class Heading extends Component 'application_uuid' => $this->parameters['application_uuid'], 'deployment_uuid' => $this->deploymentUuid, 'environment_uuid' => $this->parameters['environment_uuid'], - ], navigate: true); + ], navigate: false); } public function render() diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 88ce65c53..193a22280 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -5,10 +5,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; use App\Models\Application; use App\Models\ApplicationPreview; -use Carbon\Carbon; -use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Process; use Livewire\Component; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; @@ -193,13 +190,12 @@ class Previews extends Component { try { $server = $this->application->destination->server; - $timeout = 300; if ($this->application->destination->server->isSwarm()) { instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); } else { $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); - $this->stopContainers($containers, $server, $timeout); + $this->stopContainers($containers, $server); } GetContainersStatus::run($server); @@ -215,13 +211,12 @@ class Previews extends Component { try { $server = $this->application->destination->server; - $timeout = 300; if ($this->application->destination->server->isSwarm()) { instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); } else { $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); - $this->stopContainers($containers, $server, $timeout); + $this->stopContainers($containers, $server); } ApplicationPreview::where('application_id', $this->application->id) @@ -237,48 +232,14 @@ class Previews extends Component } } - private function stopContainers(array $containers, $server, int $timeout) + private function stopContainers(array $containers, $server, int $timeout = 30) { - $processes = []; foreach ($containers as $container) { $containerName = str_replace('/', '', $container['Names']); - $processes[$containerName] = $this->stopContainer($containerName, $timeout); - } - - $startTime = Carbon::now()->getTimestamp(); - while (count($processes) > 0) { - $finishedProcesses = array_filter($processes, function ($process) { - return ! $process->running(); - }); - foreach (array_keys($finishedProcesses) as $containerName) { - unset($processes[$containerName]); - $this->removeContainer($containerName, $server); - } - - if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { - $this->forceStopRemainingContainers(array_keys($processes), $server); - break; - } - - usleep(100000); - } - } - - private function stopContainer(string $containerName, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - private function removeContainer(string $containerName, $server) - { - instant_remote_process(["docker rm -f $containerName"], $server, throwError: false); - } - - private function forceStopRemainingContainers(array $containerNames, $server) - { - foreach ($containerNames as $containerName) { - instant_remote_process(["docker kill $containerName"], $server, throwError: false); - $this->removeContainer($containerName, $server); + instant_remote_process(command: [ + "docker stop --time=$timeout $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); } } } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 4162f47b5..d512445b7 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -120,6 +120,8 @@ class General extends Component try { $this->database->save(); $this->dispatch('success', 'SSL configuration updated.'); + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; } catch (Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index 412240bd4..51d8cb33e 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -19,6 +19,8 @@ class ScheduledBackups extends Component public $s3s; + public string $custom_type = 'mysql'; + protected $listeners = ['refreshScheduledBackups']; protected $queryString = ['selectedBackupId']; @@ -49,6 +51,14 @@ class ScheduledBackups extends Component } } + public function setCustomType() + { + $this->database->custom_type = $this->custom_type; + $this->database->save(); + $this->dispatch('success', 'Database type set.'); + $this->refreshScheduledBackups(); + } + public function delete($scheduled_backup_id): void { $this->database->scheduledBackups->find($scheduled_backup_id)->delete(); @@ -62,5 +72,6 @@ class ScheduledBackups extends Component if ($id) { $this->setSelectedBackup($id); } + $this->dispatch('refreshScheduledBackups'); } } diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 8bf511a66..5347d74f0 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -35,6 +35,6 @@ class Index extends Component { $project = collect($this->projects)->firstWhere('uuid', $projectUuid); - return $this->redirect($project->navigateTo(), true); + return $this->redirect($project->navigateTo(), navigate: false); } } diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index da49dcae9..20067b1f9 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -31,8 +31,9 @@ class Configuration extends Component return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', - 'check_status', 'refreshStatus' => '$refresh', + 'check_status', + 'refreshServices', ]; } @@ -63,6 +64,13 @@ class Configuration extends Component $this->databases = $this->service->databases->sort(); } + public function refreshServices() + { + $this->service->refresh(); + $this->applications = $this->service->applications->sort(); + $this->databases = $this->service->databases->sort(); + } + public function restartApplication($id) { try { diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index c3b7577e9..0af757c8c 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -7,6 +7,7 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\InstanceSettings; use App\Models\ServiceDatabase; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Livewire\Component; @@ -83,6 +84,42 @@ class Database extends Component $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } + public function convertToApplication() + { + try { + $service = $this->database->service; + $serviceDatabase = $this->database; + + // Check if application with same name already exists + if ($service->applications()->where('name', $serviceDatabase->name)->exists()) { + throw new \Exception('An application with this name already exists.'); + } + + // Create new parameters removing database_uuid + $redirectParams = collect($this->parameters) + ->except('database_uuid') + ->all(); + + DB::transaction(function () use ($service, $serviceDatabase) { + $service->applications()->create([ + 'name' => $serviceDatabase->name, + 'human_name' => $serviceDatabase->human_name, + 'description' => $serviceDatabase->description, + 'exclude_from_status' => $serviceDatabase->exclude_from_status, + 'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled, + 'image' => $serviceDatabase->image, + 'service_id' => $service->id, + 'is_migrated' => true, + ]); + $serviceDatabase->delete(); + }); + + return redirect()->route('project.service.configuration', $redirectParams); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function instantSave() { if ($this->database->is_public && ! $this->database->public_port) { diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index ba4ebe2fc..39f4e106d 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -24,7 +24,7 @@ class Index extends Component public $s3s; - protected $listeners = ['generateDockerCompose']; + protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh']; public function mount() { diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 8324ee645..64f7ab95c 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service; use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Livewire\Component; use Spatie\Url\Url; @@ -23,7 +24,7 @@ class ServiceApplicationView extends Component 'application.human_name' => 'nullable', 'application.description' => 'nullable', 'application.fqdn' => 'nullable', - 'application.image' => 'required', + 'application.image' => 'string|nullable', 'application.exclude_from_status' => 'required|boolean', 'application.required_fqdn' => 'required|boolean', 'application.is_log_drain_enabled' => 'nullable|boolean', @@ -73,6 +74,40 @@ class ServiceApplicationView extends Component $this->parameters = get_route_parameters(); } + public function convertToDatabase() + { + try { + $service = $this->application->service; + $serviceApplication = $this->application; + + // Check if database with same name already exists + if ($service->databases()->where('name', $serviceApplication->name)->exists()) { + throw new \Exception('A database with this name already exists.'); + } + + $redirectParams = collect($this->parameters) + ->except('database_uuid') + ->all(); + DB::transaction(function () use ($service, $serviceApplication) { + $service->databases()->create([ + 'name' => $serviceApplication->name, + 'human_name' => $serviceApplication->human_name, + 'description' => $serviceApplication->description, + 'exclude_from_status' => $serviceApplication->exclude_from_status, + 'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled, + 'image' => $serviceApplication->image, + 'service_id' => $service->id, + 'is_migrated' => true, + ]); + $serviceApplication->delete(); + }); + + return redirect()->route('project.service.configuration', $redirectParams); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 368598466..a67bd9210 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -82,6 +82,7 @@ class StackForm extends Component $this->service->refresh(); $this->service->saveComposeConfigs(); $this->dispatch('refreshEnvs'); + $this->dispatch('refreshServices'); $notify && $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Models/Application.php b/app/Models/Application.php index 233c8d806..94bd5c75b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -9,9 +9,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use OpenApi\Attributes as OA; @@ -270,51 +268,17 @@ class Application extends BaseModel return $containers->pluck('Names')->toArray(); } - public function stopContainers(array $containerNames, $server, int $timeout = 600) - { - $processes = []; - foreach ($containerNames as $containerName) { - $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout); - } - - $startTime = time(); - while (count($processes) > 0) { - $finishedProcesses = array_filter($processes, function ($process) { - return ! $process->running(); - }); - foreach ($finishedProcesses as $containerName => $process) { - unset($processes[$containerName]); - $this->removeContainer($containerName, $server); - } - - if (time() - $startTime >= $timeout) { - $this->forceStopRemainingContainers(array_keys($processes), $server); - break; - } - - usleep(100000); - } - } - - public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - public function removeContainer(string $containerName, $server) - { - instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); - } - - public function forceStopRemainingContainers(array $containerNames, $server) + public function stopContainers(array $containerNames, $server, int $timeout = 30) { foreach ($containerNames as $containerName) { - instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); - $this->removeContainer($containerName, $server); + instant_remote_process(command: [ + "docker stop --time=$timeout $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); } } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -323,8 +287,9 @@ class Application extends BaseModel } } - public function delete_volumes(?Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($this->build_pack === 'dockercompose') { $server = data_get($this, 'destination.server'); instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false); @@ -339,8 +304,9 @@ class Application extends BaseModel } } - public function delete_connected_networks($uuid) + public function deleteConnectedNetworks() { + $uuid = $this->uuid; $server = data_get($this, 'destination.server'); instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); instant_remote_process(["docker network rm {$uuid}"], $server, false); diff --git a/app/Models/Server.php b/app/Models/Server.php index 89f6ad218..fb9be5de7 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -488,7 +488,7 @@ $schema://$host { $proxy_path = "$base_path/proxy"; if ($proxyType === ProxyTypes::TRAEFIK->value) { - $proxy_path = '/'; + $proxy_path = $proxy_path.'/'; } elseif ($proxyType === ProxyTypes::CADDY->value) { $proxy_path = $proxy_path.'/caddy'; } elseif ($proxyType === ProxyTypes::NGINX->value) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 23ddb5923..13e3a89c0 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -6,9 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; use OpenApi\Attributes as OA; use Spatie\Url\Url; @@ -158,51 +156,17 @@ class Service extends BaseModel return $containersToStop; } - public function stopContainers(array $containerNames, $server, int $timeout = 300) - { - $processes = []; - foreach ($containerNames as $containerName) { - $processes[$containerName] = $this->stopContainer($containerName, $timeout); - } - - $startTime = time(); - while (count($processes) > 0) { - $finishedProcesses = array_filter($processes, function ($process) { - return ! $process->running(); - }); - foreach (array_keys($finishedProcesses) as $containerName) { - unset($processes[$containerName]); - $this->removeContainer($containerName, $server); - } - - if (time() - $startTime >= $timeout) { - $this->forceStopRemainingContainers(array_keys($processes), $server); - break; - } - - usleep(100000); - } - } - - public function stopContainer(string $containerName, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - public function removeContainer(string $containerName, $server) - { - instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); - } - - public function forceStopRemainingContainers(array $containerNames, $server) + public function stopContainers(array $containerNames, $server, int $timeout = 30) { foreach ($containerNames as $containerName) { - instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); - $this->removeContainer($containerName, $server); + instant_remote_process(command: [ + "docker stop --time=$timeout $containerName", + "docker rm -f $containerName", + ], server: $server, throwError: false); } } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -211,11 +175,11 @@ class Service extends BaseModel } } - public function delete_connected_networks($uuid) + public function deleteConnectedNetworks() { $server = data_get($this, 'destination.server'); - instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); - instant_remote_process(["docker network rm {$uuid}"], $server, false); + instant_remote_process(["docker network disconnect {$this->uuid} coolify-proxy"], $server, false); + instant_remote_process(["docker network rm {$this->uuid}"], $server, false); } public function getStatusAttribute() diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 40d183033..d595721d8 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -16,6 +16,7 @@ class ServiceDatabase extends BaseModel static::deleting(function ($service) { $service->persistentStorages()->delete(); $service->fileStorages()->delete(); + $service->scheduledBackups()->delete(); }); static::saving(function ($service) { if ($service->isDirty('status')) { @@ -77,6 +78,9 @@ class ServiceDatabase extends BaseModel public function databaseType() { + if (filled($this->custom_type)) { + return 'standalone-'.$this->custom_type; + } $image = str($this->image)->before(':'); if ($image->contains('supabase/postgres')) { $finalImage = 'supabase/postgres'; @@ -141,6 +145,7 @@ class ServiceDatabase extends BaseModel str($this->databaseType())->contains('postgres') || str($this->databaseType())->contains('postgis') || str($this->databaseType())->contains('mariadb') || - str($this->databaseType())->contains('mongo'); + str($this->databaseType())->contains('mongo') || + filled($this->custom_type); } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index bc1f9b4b3..fcd81cdc9 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -94,7 +93,7 @@ class StandaloneClickhouse extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -103,8 +102,9 @@ class StandaloneClickhouse extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index a14c5e378..fdf69b834 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -94,7 +93,7 @@ class StandaloneDragonfly extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -103,8 +102,9 @@ class StandaloneDragonfly extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 2d3aea755..d52023920 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -94,7 +93,7 @@ class StandaloneKeydb extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -103,8 +102,9 @@ class StandaloneKeydb extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 7549ace3e..5a8869b41 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -3,8 +3,8 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\SoftDeletes; class StandaloneMariadb extends BaseModel @@ -94,7 +94,7 @@ class StandaloneMariadb extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -103,8 +103,9 @@ class StandaloneMariadb extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } @@ -253,7 +254,7 @@ class StandaloneMariadb extends BaseModel return $this->morphMany(LocalFileVolume::class, 'resource'); } - public function destination() + public function destination(): MorphTo { return $this->morphTo(); } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 3092216bd..88833eebe 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -98,7 +97,7 @@ class StandaloneMongodb extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -107,8 +106,9 @@ class StandaloneMongodb extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index dbb5b1ae6..dedc35f91 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -95,7 +94,7 @@ class StandaloneMysql extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -104,8 +103,9 @@ class StandaloneMysql extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index a74d567a0..689134a32 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -59,7 +58,7 @@ class StandalonePostgresql extends BaseModel ); } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -68,8 +67,9 @@ class StandalonePostgresql extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index fccbb24a5..7f6f2ad72 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -96,7 +95,7 @@ class StandaloneRedis extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } - public function delete_configurations() + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); @@ -105,8 +104,9 @@ class StandaloneRedis extends BaseModel } } - public function delete_volumes(Collection $persistentStorages) + public function deleteVolumes() { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } diff --git a/app/Traits/DeletesUserSessions.php b/app/Traits/DeletesUserSessions.php index 2581d4203..a4d3a7cfd 100644 --- a/app/Traits/DeletesUserSessions.php +++ b/app/Traits/DeletesUserSessions.php @@ -2,7 +2,7 @@ namespace App\Traits; -use DB; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Session; trait DeletesUserSessions diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 3f1e8513c..919b2bde5 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -28,6 +28,7 @@ function queue_application_deployment(Application $application, string $deployme // Check if there's already a deployment in progress or queued for this application and commit $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) ->where('commit', $commit) + ->where('pull_request_id', $pull_request_id) ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value]) ->first(); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 44e20c9b3..8b2d9fb6a 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2990,12 +2990,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $applicationFound->name, - 'image' => $applicationFound->image, - 'service_id' => $applicationFound->service_id, - ]); - $applicationFound->delete(); + // $savedService = ServiceDatabase::firstOrCreate([ + // 'name' => $applicationFound->name, + // 'image' => $applicationFound->image, + // 'service_id' => $applicationFound->service_id, + // ]); + // $applicationFound->delete(); } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, @@ -3248,12 +3248,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $applicationFound->name, - 'image' => $applicationFound->image, - 'service_id' => $applicationFound->service_id, - ]); - $applicationFound->delete(); + // $savedService = ServiceDatabase::firstOrCreate([ + // 'name' => $applicationFound->name, + // 'image' => $applicationFound->image, + // 'service_id' => $applicationFound->service_id, + // ]); + // $applicationFound->delete(); } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, diff --git a/config/constants.php b/config/constants.php index a849ae93e..be20736c0 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.413', + 'version' => '4.0.0-beta.418', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.8', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/database/migrations/2025_04_30_134146_add_is_migrated_to_services.php b/database/migrations/2025_04_30_134146_add_is_migrated_to_services.php new file mode 100644 index 000000000..23049014b --- /dev/null +++ b/database/migrations/2025_04_30_134146_add_is_migrated_to_services.php @@ -0,0 +1,36 @@ +boolean('is_migrated')->default(false); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->boolean('is_migrated')->default(false); + $table->string('custom_type')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_applications', function (Blueprint $table) { + $table->dropColumn('is_migrated'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->dropColumn('is_migrated'); + $table->dropColumn('custom_type'); + }); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index f75400ce9..2d6f52e31 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -19,7 +19,7 @@ class ApplicationSeeder extends Seeder 'fqdn' => 'http://nodejs.127.0.0.1.sslip.io', 'repository_project_id' => 603035348, 'git_repository' => 'coollabsio/coolify-examples', - 'git_branch' => 'main', + 'git_branch' => 'v4.x', 'base_directory' => '/nodejs', 'build_pack' => 'nixpacks', 'ports_exposes' => '3000', @@ -34,7 +34,7 @@ class ApplicationSeeder extends Seeder 'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io', 'repository_project_id' => 603035348, 'git_repository' => 'coollabsio/coolify-examples', - 'git_branch' => 'main', + 'git_branch' => 'v4.x', 'base_directory' => '/dockerfile', 'build_pack' => 'dockerfile', 'ports_exposes' => '80', @@ -48,7 +48,7 @@ class ApplicationSeeder extends Seeder 'name' => 'Pure Dockerfile Example', 'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io', 'git_repository' => 'coollabsio/coolify', - 'git_branch' => 'main', + 'git_branch' => 'v4.x', 'git_commit_sha' => 'HEAD', 'build_pack' => 'dockerfile', 'ports_exposes' => '80', diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 35fea6403..965fca276 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -61,7 +61,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.6' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.8' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 35fea6403..fa30677ad 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -61,7 +61,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.6' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.8' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 6dc9e7a02..106273897 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.413" + "version": "4.0.0-beta.418" }, "nightly": { - "version": "4.0.0-beta.414" + "version": "4.0.0-beta.419" }, "helper": { "version": "1.0.8" diff --git a/package-lock.json b/package-lock.json index c995800ca..16e76bfcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", - "vite": "^6.2.6", + "vite": "^6.3.4", "vue": "3.5.13" } }, @@ -2931,6 +2931,51 @@ "node": ">=0.8" } }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2994,15 +3039,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -3076,6 +3124,34 @@ "picomatch": "^2.3.1" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vue": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", diff --git a/package.json b/package.json index ed933a4cf..edc875e38 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "pusher-js": "8.4.0", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", - "vite": "^6.2.6", + "vite": "^6.3.4", "vue": "3.5.13" }, "dependencies": { diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 4148a61d3..9a75200b1 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -123,7 +123,7 @@