diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ddc913c..eae126054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,166 @@ All notable changes to this project will be documented in this file. ## [unreleased] +### πŸ› Bug Fixes + +- *(navbar)* Update error message link to use route for environment variables navigation +- Unsend template +- Replace ports with expose +- *(templates)* Update Unsend compose configuration for improved service integration + +### 🚜 Refactor + +- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management + +### πŸ“š Documentation + +- Update changelog + +## [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 + +- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command +- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans +- *(migrations)* Make stripe_comment field nullable in subscriptions table +- *(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 + +- Add missing openapi items to PrivateKey + +### 🚜 Refactor + +- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files +- *(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 + +- Update changelog +- Update changelog +- Update changelog + ### βš™οΈ Miscellaneous Tasks -- *(versions)* Bump version to 404 +- *(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 + +### πŸ“š Documentation + +- Update changelog + +## [4.0.0-beta.406] - 2025-04-05 + +### πŸš€ Features + +- *(Deploy)* Add info dispatch for proxy check initiation +- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component +- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic +- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values +- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling +- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process +- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages +- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings + +### πŸ› Bug Fixes + +- *(CheckProxy)* Update port conflict check to ensure accurate grep matching +- *(CheckProxy)* Refine port conflict detection with improved grep patterns +- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output +- *(api)* Add back validateDataApplications (#5539) +- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General +- *(Status)* Conditionally check proxy status and refresh button based on force_stop state +- *(General)* Change redis_password property to nullable string +- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint + +### πŸ’Ό Other + +- Add missing UUID to openapi spec + +### 🚜 Refactor + +- *(Server)* Use data_get for safer access to settings properties in isFunctional method +- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency +- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios +- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support +- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling +- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method +- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function +- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null +- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration +- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations +- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation + +### βš™οΈ Miscellaneous Tasks + +- *(versions)* Bump version to 406 +- *(versions)* Bump version to 407 and 408 for coolify and nightly +- *(versions)* Bump version to 408 for coolify and 409 for nightly + +## [4.0.0-beta.405] - 2025-04-04 + +### πŸš€ Features + +- *(api)* Update OpenAPI spec for services (#5448) + +### πŸ› Bug Fixes + +- *(api)* Used ssh keys can be deleted +- *(email)* Transactional emails not sending + +### 🚜 Refactor + +- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks + +### πŸ“š Documentation + +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog + +### βš™οΈ Miscellaneous Tasks + +- *(versions)* Bump version to 406 +- *(versions)* Bump version to 407 ## [4.0.0-beta.404] - 2025-04-03 ### πŸš€ 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 @@ -27,10 +173,13 @@ 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 @@ -38,9 +187,11 @@ 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 @@ -59,7 +210,6 @@ 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 @@ -73,7 +223,6 @@ 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 @@ -144,6 +293,14 @@ 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 @@ -185,14 +342,6 @@ 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 @@ -210,6 +359,18 @@ 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 @@ -249,18 +410,6 @@ 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 @@ -309,6 +458,7 @@ 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 @@ -316,7 +466,6 @@ 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 @@ -709,6 +858,14 @@ 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 @@ -744,6 +901,35 @@ 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 @@ -843,6 +1029,7 @@ 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 @@ -858,6 +1045,9 @@ 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 @@ -881,44 +1071,11 @@ 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 @@ -928,15 +1085,11 @@ All notable changes to this project will be documented in this file. - Error message - Update healthcheck and port configurations to use port 8080 -### 🚜 Refactor +## [4.0.0-beta.379] - 2024-12-13 -- Rename `coolify.environment` to `coolify.environmentName` +### πŸ› Bug Fixes -### βš™οΈ Miscellaneous Tasks - -- Regenerate API spec, removing notification fields -- Remove ray debugging -- Version ++ +- Saving oauth ## [4.0.0-beta.378] - 2024-12-13 @@ -945,11 +1098,6 @@ 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/README.md b/README.md index 8670e9c76..6e245aa22 100644 --- a/README.md +++ b/README.md @@ -29,99 +29,6 @@ You can find the installation script source [here](./scripts/install.sh). Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). -# Donations -To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. - -[coolify.io/sponsorships](https://coolify.io/sponsorships) - -Thank you so much! - -Special thanks to our biggest sponsors! - -### Special Sponsors - -![image](https://github.com/user-attachments/assets/6022bc9c-8435-4d14-9497-8be230ed8cb1) - - -* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry. -* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions. -* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities. -* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform. -* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies. -* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution. -* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks. -* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. -* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. -* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. -* [Convex](https://convex.link/coolify.io) - Convex is the open-source reactive database for web app developers. -* [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. -* [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers. -* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang. -* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets. -* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. -* [Brand Dev](https://brand.dev/?ref=coolify.io) - The #1 Brand API for B2B software startups - instantly pull logos, fonts, descriptions, social links, slogans, and so much more from any domain via a single api call. -* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries. -* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools. -* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services. -* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services. -* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses. -* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly. -* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. -* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - A Fast web hosting provider. - - -## Github Sponsors ($40+) -SerpAPI -typebot - - -Lightspeed.run -DartNode - FlintCompany -American Cloud -CryptoJobsList -Codext -Thompson Edolo -UXWizz -Younes Barrad -Automaze -Corentin Clichy -Niklas Lausch -Pixel Infinito -Tyler Whitesides -NiftyCo -Imre Ujlaki -Ilias Ism -Breakcold -PaweΕ‚ PierΕ›cionek -Michael Mazurczak -Formbricks -StartupFame -jyc.dev -BitLaunch -Internet Garden -Jonas Jaeger -JP -Evercam -Web3 Career - -## Organizations - - - - - - - - - - - - -## Individuals - - - # Cloud If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) @@ -137,6 +44,96 @@ By subscribing to the cloud version, you get the Coolify server for the same pri - Better support - Less maintenance for you +# Donations +To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. + +[coolify.io/sponsorships](https://coolify.io/sponsorships) + +Thank you so much! + +## Big Sponsors + +* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management +* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform +* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform +* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions +* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers +* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions +* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers +* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase +* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers +* [Trieve](https://trieve.ai?ref=coolify.io) - AI-powered search and analytics +* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data +* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions +* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions +* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor +* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform +* [WZ-IT](https://wz-it.com/?ref=coolify.io) - German agency for customised cloud solutions +* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner +* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform +* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions +* [QuantCDN](https://www.quantcdn.io?ref=coolify.io) - Enterprise-grade content delivery network +* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang +* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers +* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital transformation and web solutions +* [Cloudify.ro](https://cloudify.ro?ref=coolify.io) - Cloud hosting solutions +* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half +* [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services +* [MassiveGrid](https://massivegrid.com?ref=coolify.io) - Enterprise cloud hosting solutions +* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers +* [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 + +## Small Sponsors + +UXWizz +Evercam +Imre Ujlaki +jyc.dev +TheRealJP +360Creators +NiftyCo +Dry Software +Lightspeed.run +LinkDr +Gravity Wiz +BitLaunch +Best for Android +Ilias Ism +Formbricks +Server Searcher +Reshot +Cirun +Typebot +Creating Coding Careers +Internet Garden +Web3 Jobs +Codext +Michael Mazurczak +Fider +Flint +PaweΕ‚ PierΕ›cionek +RunPod +DartNode +Tyler Whitesides +SerpAPI +Aquarela +Crypto Jobs List +Alfred Nutile +Startup Fame +Younes Barrad +Jonas Jaeger +Pixel Infinito +Corentin Clichy +Thompson Edolo +Devhuset +Arvensis Systems +Niklas Lausch +Cap-go + +...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio) + # Recognitions

diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index a42f03eb5..870b5b7e5 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -217,6 +217,10 @@ class StartMongodb if ($this->database->enable_ssl) { $commandParts = ['mongod']; + if (! empty($this->database->mongo_conf)) { + $commandParts = ['mongod', '--config', '/etc/mongo/mongod.conf']; + } + $sslConfig = match ($this->database->ssl_mode) { 'allow' => [ '--tlsMode=allowTLS', diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 27608547a..5a2562073 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -27,7 +27,7 @@ class CheckProxy return false; } $proxyType = $server->proxyType(); - if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { + if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) { return false; } if (! $server->isProxyShouldRun()) { @@ -37,8 +37,12 @@ class CheckProxy return false; } } + + // Determine proxy container name based on environment + $proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; + if ($server->isSwarm()) { - $status = getContainerStatus($server, 'coolify-proxy_traefik'); + $status = getContainerStatus($server, $proxyContainerName); $server->proxy->set('status', $status); $server->save(); if ($status === 'running') { @@ -47,7 +51,7 @@ class CheckProxy return true; } else { - $status = getContainerStatus($server, 'coolify-proxy'); + $status = getContainerStatus($server, $proxyContainerName); if ($status === 'running') { $server->proxy->set('status', 'running'); $server->save(); @@ -61,34 +65,11 @@ class CheckProxy if ($server->id === 0) { $ip = 'host.docker.internal'; } - $portsToCheck = ['80', '443']; foreach ($portsToCheck as $port) { - // Try multiple methods to check port availability - $commands = [ - // Method 1: Check /proc/net/tcp directly (convert port to hex) - "cat /proc/net/tcp | grep -q '00000000:".str_pad(dechex($port), 4, '0', STR_PAD_LEFT)."'", - // Method 2: Use ss command (modern alternative to netstat) - "ss -tuln | grep -q ':$port '", - // Method 3: Use lsof if available - "lsof -i :$port >/dev/null 2>&1", - // Method 4: Use fuser if available - "fuser $port/tcp >/dev/null 2>&1", - ]; - - $portInUse = false; - foreach ($commands as $command) { - try { - instant_remote_process([$command], $server); - $portInUse = true; - break; - } catch (\Throwable $e) { - - continue; - } - } - if ($portInUse) { + // Use the smart port checker that handles dual-stack properly + if ($this->isPortConflict($server, $port, $proxyContainerName)) { if ($fromUI) { throw new \Exception("Port $port is in use.
You must stop the process using this port.

Docs: https://coolify.io/docs
Discord: https://coolify.io/discord"); } else { @@ -126,4 +107,144 @@ class CheckProxy return true; } } + + /** + * Smart port checker that handles dual-stack configurations + * Returns true only if there's a real port conflict (not just dual-stack) + */ + private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool + { + // First check if our own proxy is using this port (which is fine) + try { + $getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'"; + $containerId = trim(instant_remote_process([$getProxyContainerId], $server)); + + if (! empty($containerId)) { + $checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'"; + try { + instant_remote_process([$checkProxyPort], $server); + + // Our proxy is using the port, which is fine + return false; + } catch (\Throwable $e) { + // Our container exists but not using this port + } + } + } catch (\Throwable $e) { + // Container not found or error checking, continue with regular checks + } + + // Command sets for different ways to check ports, ordered by preference + $commandSets = [ + // Set 1: Use ss to check listener counts by protocol stack + [ + 'available' => 'command -v ss >/dev/null 2>&1', + 'check' => [ + // Get listening process details + "ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"", + // Count IPv4 listeners + "echo \"\$ss_output\" | grep -c ':$port '", + ], + ], + // Set 2: Use netstat as alternative to ss + [ + 'available' => 'command -v netstat >/dev/null 2>&1', + 'check' => [ + // Get listening process details + "netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '", + // Count listeners + "echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'", + ], + ], + // Set 3: Use lsof as last resort + [ + 'available' => 'command -v lsof >/dev/null 2>&1', + 'check' => [ + // Get process using the port + "lsof -i :$port -P -n | grep 'LISTEN'", + // Count listeners + "lsof -i :$port -P -n | grep 'LISTEN' | wc -l", + ], + ], + ]; + + // Try each command set until we find one available + foreach ($commandSets as $set) { + try { + // Check if the command is available + instant_remote_process([$set['available']], $server); + + // Run the actual check commands + $output = instant_remote_process($set['check'], $server, true); + + // Parse the output lines + $lines = explode("\n", trim($output)); + + // Get the detailed output and listener count + $details = trim($lines[0] ?? ''); + $count = intval(trim($lines[1] ?? '0')); + + // If no listeners or empty result, port is free + if ($count == 0 || empty($details)) { + return false; + } + + // Try to detect if this is our coolify-proxy + if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) { + // It's likely our docker or proxy, which is fine + return false; + } + + // Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6) + // If exactly 2 listeners and both have same port, likely dual-stack + if ($count <= 2) { + // Check if it looks like a standard dual-stack setup + $isDualStack = false; + + // Look for IPv4 and IPv6 in the listing (ss output format) + if (preg_match('/LISTEN.*:'.$port.'\s/', $details) && + (preg_match('/\*:'.$port.'\s/', $details) || + preg_match('/:::'.$port.'\s/', $details))) { + $isDualStack = true; + } + + // For netstat format + if (strpos($details, '0.0.0.0:'.$port) !== false && + strpos($details, ':::'.$port) !== false) { + $isDualStack = true; + } + + // For lsof format (IPv4 and IPv6) + if (strpos($details, '*:'.$port) !== false && + preg_match('/\*:'.$port.'.*IPv4/', $details) && + preg_match('/\*:'.$port.'.*IPv6/', $details)) { + $isDualStack = true; + } + + if ($isDualStack) { + return false; // This is just a normal dual-stack setup + } + } + + // If we get here, it's likely a real port conflict + return true; + + } catch (\Throwable $e) { + // This command set failed, try the next one + continue; + } + } + + // Fallback to simpler check if all above methods fail + try { + // Just try to bind to the port directly to see if it's available + $checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'"; + $result = instant_remote_process([$checkCommand], $server, true); + + return trim($result) === 'in-use'; + } catch (\Throwable $e) { + // If everything fails, assume the port is free to avoid false positives + return false; + } + } } diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index ba4c2311a..754feecb1 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -14,15 +14,26 @@ class CleanupDocker public function handle(Server $server) { $settings = instanceSettings(); + $realtimeImage = config('constants.coolify.realtime_image'); + $realtimeImageVersion = config('constants.coolify.realtime_version'); + $realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion"; + $realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime'; + $realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion"; + $helperImageVersion = data_get($settings, 'helper_version'); $helperImage = config('constants.coolify.helper_image'); $helperImageWithVersion = "$helperImage:$helperImageVersion"; + $helperImageWithoutPrefix = 'coollabsio/coolify-helper'; + $helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion"; $commands = [ 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', 'docker image prune -af --filter "label!=coolify.managed=true"', 'docker builder prune -af', "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$helperImageWithoutPrefixVersion --filter reference=$helperImageWithoutPrefix | grep $helperImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", + "docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", ]; if ($server->settings->delete_unused_volumes) { diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/Generate/OpenApi.php similarity index 89% rename from app/Console/Commands/OpenApi.php rename to app/Console/Commands/Generate/OpenApi.php index 3cef85477..2b266c258 100644 --- a/app/Console/Commands/OpenApi.php +++ b/app/Console/Commands/Generate/OpenApi.php @@ -1,6 +1,6 @@ json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1004,12 +1009,17 @@ class ApplicationsController extends Controller if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1101,12 +1111,17 @@ class ApplicationsController extends Controller if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } else { if ($application->build_pack === 'dockercompose') { LoadComposeFile::dispatch($application); @@ -1190,12 +1205,17 @@ class ApplicationsController extends Controller if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json(serializeApiResponse([ @@ -1254,12 +1274,17 @@ class ApplicationsController extends Controller if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, no_questions_asked: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json(serializeApiResponse([ @@ -1610,6 +1635,18 @@ class ApplicationsController extends Controller ['bearerAuth' => []], ], tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], requestBody: new OA\RequestBody( description: 'Application updated.', required: true, @@ -1884,11 +1921,16 @@ class ApplicationsController extends Controller if ($instantDeploy) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } } return response()->json([ @@ -2520,10 +2562,6 @@ class ApplicationsController extends Controller ])->setStatusCode(201); } } - - return response()->json([ - 'message' => 'Something went wrong.', - ], 500); } #[OA\Delete( @@ -2705,13 +2743,21 @@ class ApplicationsController extends Controller $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: $force, is_api: true, no_questions_asked: $instant_deploy ); + if ($result['status'] === 'skipped') { + return response()->json( + [ + 'message' => $result['message'], + ], + 200 + ); + } return response()->json( [ @@ -2866,12 +2912,17 @@ class ApplicationsController extends Controller $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, restart_only: true, is_api: true, ); + if ($result['status'] === 'skipped') { + return response()->json([ + 'message' => $result['message'], + ], 200); + } return response()->json( [ @@ -3006,73 +3057,73 @@ class ApplicationsController extends Controller // ]); // } - // private function validateDataApplications(Request $request, Server $server) - // { - // $teamId = getTeamIdFromToken(); + private function validateDataApplications(Request $request, Server $server) + { + $teamId = getTeamIdFromToken(); - // // Validate ports_mappings - // if ($request->has('ports_mappings')) { - // $ports = []; - // foreach (explode(',', $request->ports_mappings) as $portMapping) { - // $port = explode(':', $portMapping); - // if (in_array($port[0], $ports)) { - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => [ - // 'ports_mappings' => 'The first number before : should be unique between mappings.', - // ], - // ], 422); - // } - // $ports[] = $port[0]; - // } - // } - // // Validate custom_labels - // if ($request->has('custom_labels')) { - // if (! isBase64Encoded($request->custom_labels)) { - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => [ - // 'custom_labels' => 'The custom_labels should be base64 encoded.', - // ], - // ], 422); - // } - // $customLabels = base64_decode($request->custom_labels); - // if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => [ - // 'custom_labels' => 'The custom_labels should be base64 encoded.', - // ], - // ], 422); - // } - // } - // if ($request->has('domains') && $server->isProxyShouldRun()) { - // $uuid = $request->uuid; - // $fqdn = $request->domains; - // $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - // $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - // $errors = []; - // $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - // if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - // $errors[] = 'Invalid domain: '.$domain; - // } + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $uuid = $request->uuid; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } - // return str($domain)->trim()->lower(); - // }); - // if (count($errors) > 0) { - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => $errors, - // ], 422); - // } - // if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { - // return response()->json([ - // 'message' => 'Validation failed.', - // 'errors' => [ - // 'domains' => 'One of the domain is already used.', - // ], - // ], 422); - // } - // } - // } + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 424c2cc76..46606e24a 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Controller; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\Server; +use App\Models\Service; use App\Models\Tag; use Illuminate\Http\Request; use OpenApi\Attributes as OA; @@ -132,7 +133,7 @@ class DeployController extends Controller #[OA\Get( summary: 'Deploy', - description: 'Deploy by tag or uuid. `Post` request also accepted.', + description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.', path: '/deploy', operationId: 'deploy-by-tag-or-uuid', security: [ @@ -191,10 +192,10 @@ class DeployController extends Controller return invalidTokenResponse(); } - $uuids = $request->query->get('uuid'); - $tags = $request->query->get('tag'); - $force = $request->query->get('force') ?? false; - $pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0; + $uuids = $request->input('uuid'); + $tags = $request->input('tag'); + $force = $request->input('force') ?? false; + $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0; if ($uuids && $tags) { return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); @@ -297,17 +298,21 @@ class DeployController extends Controller return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } switch ($resource?->getMorphClass()) { - case \App\Models\Application::class: + case Application::class: $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $resource, deployment_uuid: $deployment_uuid, force_rebuild: $force, pull_request_id: $pr, ); - $message = "Application {$resource->name} deployment queued."; + if ($result['status'] === 'skipped') { + $message = $result['message']; + } else { + $message = "Application {$resource->name} deployment queued."; + } break; - case \App\Models\Service::class: + case Service::class: StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; break; @@ -333,6 +338,40 @@ class DeployController extends Controller ['bearerAuth' => []], ], tags: ['Deployments'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'skip', + in: 'query', + description: 'Number of records to skip.', + required: false, + schema: new OA\Schema( + type: 'integer', + minimum: 0, + default: 0, + ) + ), + new OA\Parameter( + name: 'take', + in: 'query', + description: 'Number of records to take.', + required: false, + schema: new OA\Schema( + type: 'integer', + minimum: 1, + default: 10, + ) + ), + ], responses: [ new OA\Response( response: 200, diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index b94ce9c67..98637c3e8 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -267,6 +267,18 @@ class ProjectController extends Controller ['bearerAuth' => []], ], tags: ['Projects'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the project.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], requestBody: new OA\RequestBody( required: true, description: 'Project updated.', diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index a9a0a2e53..cbd20400a 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -809,6 +809,6 @@ class ServersController extends Controller } ValidateServer::dispatch($server); - return response()->json(['message' => 'Validation started.']); + return response()->json(['message' => 'Validation started.'], 201); } } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 027bd5c1c..e792779e1 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -380,6 +380,9 @@ class ServicesController extends Controller $service = new Service; $result = $this->upsert_service($request, $service, $teamId); + if ($result instanceof \Illuminate\Http\JsonResponse) { + return $result; + } return response()->json(serializeApiResponse($result))->setStatusCode(201); } else { @@ -527,6 +530,18 @@ class ServicesController extends Controller ['bearerAuth' => []], ], tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], requestBody: new OA\RequestBody( description: 'Service updated.', required: true, @@ -596,12 +611,14 @@ class ServicesController extends Controller } $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); - if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } $result = $this->upsert_service($request, $service, $teamId); + if ($result instanceof \Illuminate\Http\JsonResponse) { + return $result; + } return response()->json(serializeApiResponse($result))->setStatusCode(200); } diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 33d8f8532..490b66e58 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -100,18 +100,26 @@ class Bitbucket extends Controller if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: $commit, force_rebuild: false, is_webhook: true ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, @@ -143,7 +151,7 @@ class Bitbucket extends Controller ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -152,11 +160,19 @@ class Bitbucket extends Controller is_webhook: true, git_type: 'bitbucket' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 87fd2255f..3c3d6e0b6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -116,19 +116,27 @@ class Gitea extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: false, commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -175,7 +183,7 @@ class Gitea extends Controller ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -184,11 +192,19 @@ class Gitea extends Controller is_webhook: true, git_type: 'gitea' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 882f2be8b..597ec023f 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -122,19 +122,29 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, force_rebuild: false, commit: data_get($payload, 'after', 'HEAD'), is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -181,7 +191,8 @@ class Github extends Controller ]); } } - queue_application_deployment( + + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -190,11 +201,19 @@ class Github extends Controller is_webhook: true, git_type: 'github' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, @@ -341,7 +360,7 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: data_get($payload, 'after', 'HEAD'), @@ -349,10 +368,11 @@ class Github extends Controller is_webhook: true, ); $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', + 'status' => $result['status'], + 'message' => $result['message'], 'application_uuid' => $application->uuid, 'application_name' => $application->name, + 'deployment_uuid' => $result['deployment_uuid'], ]); } else { $paths = str($application->watch_paths)->explode("\n"); @@ -389,7 +409,7 @@ class Github extends Controller 'pull_request_html_url' => $pull_request_html_url, ]); } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -398,11 +418,19 @@ class Github extends Controller is_webhook: true, git_type: 'github' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index cf6874b8c..d6d12a05f 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -142,19 +142,28 @@ class Gitlab extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { $deployment_uuid = new Cuid2; - queue_application_deployment( + $result = queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, commit: data_get($payload, 'after', 'HEAD'), force_rebuild: false, is_webhook: true, ); - $return_payloads->push([ - 'status' => 'success', - 'message' => 'Deployment queued.', - 'application_uuid' => $application->uuid, - 'application_name' => $application->name, - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'status' => $result['status'], + 'message' => $result['message'], + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } else { + $return_payloads->push([ + 'status' => 'success', + 'message' => 'Deployment queued.', + 'application_uuid' => $application->uuid, + 'application_name' => $application->name, + ]); + } } else { $paths = str($application->watch_paths)->explode("\n"); $return_payloads->push([ @@ -201,7 +210,7 @@ class Gitlab extends Controller ]); } } - queue_application_deployment( + $result = queue_application_deployment( application: $application, pull_request_id: $pull_request_id, deployment_uuid: $deployment_uuid, @@ -210,11 +219,19 @@ class Gitlab extends Controller is_webhook: true, git_type: 'gitlab' ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview Deployment queued', - ]); + if ($result['status'] === 'skipped') { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'skipped', + 'message' => $result['message'], + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview Deployment queued', + ]); + } } else { $return_payloads->push([ 'application' => $application->name, diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5dbdbf215..c29093ce0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); } - $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); + $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); @@ -899,100 +899,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id'); } $ports = $this->application->main_port(); - if ($this->pull_request_id !== 0) { - $this->env_filename = ".env-pr-$this->pull_request_id"; - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $envs->push("SOURCE_COMMIT={$this->commit}"); - } else { - $envs->push('SOURCE_COMMIT=unknown'); - } - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { - $envs->push("COOLIFY_FQDN={$this->preview->fqdn}"); - $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}"); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); - $envs->push("COOLIFY_URL={$url}"); - $envs->push("COOLIFY_DOMAIN_FQDN={$url}"); - } - if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { - $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}"); - } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); - } - } - - add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview); - - foreach ($sorted_environment_variables_preview as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); - } - // Add PORT if not exists, use the first port as default - if ($this->build_pack !== 'dockercompose') { - if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); - } - } - // Add HOST if not exists - if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { - $envs->push('HOST=0.0.0.0'); - } - } else { + $coolify_envs = $this->generate_coolify_env_variables(); + $coolify_envs->each(function ($item, $key) use ($envs) { + $envs->push($key.'='.$item); + }); + if ($this->pull_request_id === 0) { $this->env_filename = '.env'; - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $envs->push("SOURCE_COMMIT={$this->commit}"); - } else { - $envs->push('SOURCE_COMMIT=unknown'); - } - } - if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { - if ((int) $this->application->compose_parsing_version >= 3) { - $envs->push("COOLIFY_URL={$this->application->fqdn}"); - } else { - $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); - } - } - if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { - $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); - if ((int) $this->application->compose_parsing_version >= 3) { - $envs->push("COOLIFY_FQDN={$url}"); - } else { - $envs->push("COOLIFY_URL={$url}"); - } - } - if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { - if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); - } - if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { - $envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}"); - } - if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); - } - } - - add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables); foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; @@ -1017,6 +929,32 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } + } else { + $this->env_filename = ".env-pr-$this->pull_request_id"; + foreach ($sorted_environment_variables_preview as $env) { + $real_value = $env->real_value; + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->real_value; + } else { + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + } + $envs->push($env->key.'='.$real_value); + } + // Add PORT if not exists, use the first port as default + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } + } + // Add HOST if not exists + if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { + $envs->push('HOST=0.0.0.0'); + } + } if ($envs->isEmpty()) { $this->env_filename = null; @@ -1361,7 +1299,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); - $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); $this->execute_remote_command( [ @@ -1394,6 +1331,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } foreach ($destination_ids as $destination_id) { $destination = StandaloneDocker::find($destination_id); + if (! $destination) { + continue; + } $server = $destination->server; if ($server->team_id !== $this->mainServer->team_id) { $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); @@ -1437,6 +1377,17 @@ 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) { + $repository = githubApi($this->source, "repos/{$this->customRepository}"); + $data = data_get($repository, 'data'); + if (isset($data->id)) { + $repository_project_id = $data->id; + if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) { + $this->application->repository_project_id = $repository_project_id; + $this->application->save(); + } + } + } $this->generate_git_import_commands(); $local_branch = $this->branch; if ($this->pull_request_id !== 0) { @@ -1626,20 +1577,128 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->env_nixpacks_args = $this->env_nixpacks_args->implode(' '); } + private function generate_coolify_env_variables(): Collection + { + $coolify_envs = collect([]); + $local_branch = $this->branch; + if ($this->pull_request_id !== 0) { + // Add SOURCE_COMMIT if not exists + if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { + $coolify_envs->put('COOLIFY_FQDN', $this->preview->fqdn); + $coolify_envs->put('COOLIFY_DOMAIN_URL', $this->preview->fqdn); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) { + $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); + $coolify_envs->put('COOLIFY_URL', $url); + $coolify_envs->put('COOLIFY_DOMAIN_FQDN', $url); + } + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolify_envs->put('COOLIFY_BRANCH', $local_branch); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name); + } + } + + add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview); + + } else { + // Add SOURCE_COMMIT if not exists + if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } + } + if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { + if ((int) $this->application->compose_parsing_version >= 3) { + $coolify_envs->put('COOLIFY_URL', $this->application->fqdn); + } else { + $coolify_envs->put('COOLIFY_FQDN', $this->application->fqdn); + } + } + if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { + $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); + if ((int) $this->application->compose_parsing_version >= 3) { + $coolify_envs->put('COOLIFY_FQDN', $url); + } else { + $coolify_envs->put('COOLIFY_URL', $url); + } + } + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolify_envs->put('COOLIFY_BRANCH', $local_branch); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) { + $coolify_envs->put('COOLIFY_RESOURCE_UUID', $this->application->uuid); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolify_envs->put('COOLIFY_CONTAINER_NAME', $this->container_name); + } + } + + add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables); + + } + + return $coolify_envs; + } + private function generate_env_variables() { $this->env_args = collect([]); $this->env_args->put('SOURCE_COMMIT', $this->commit); + $coolify_envs = $this->generate_coolify_env_variables(); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); + if (str($env->real_value)->startsWith('$')) { + $variable_key = str($env->real_value)->after('$'); + if ($variable_key->startsWith('COOLIFY_')) { + $variable = $coolify_envs->get($variable_key->value()); + if (filled($variable)) { + $this->env_args->prepend($variable, $variable_key->value()); + } + } else { + $variable = $this->application->environment_variables()->where('key', $variable_key)->first(); + if ($variable) { + $this->env_args->prepend($variable->real_value, $env->key); + } + } + } } } } else { foreach ($this->application->build_environment_variables_preview as $env) { if (! is_null($env->real_value)) { $this->env_args->put($env->key, $env->real_value); + if (str($env->real_value)->startsWith('$')) { + $variable_key = str($env->real_value)->after('$'); + if ($variable_key->startsWith('COOLIFY_')) { + $variable = $coolify_envs->get($variable_key->value()); + if (filled($variable)) { + $this->env_args->prepend($variable, $variable_key->value()); + } + } else { + $variable = $this->application->environment_variables_preview()->where('key', $variable_key)->first(); + if ($variable) { + $this->env_args->prepend($variable->real_value, $env->key); + } + } + } } } } @@ -1664,25 +1723,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $labels = $labels->filter(function ($value, $key) { return ! Str::startsWith($value, 'coolify.'); }); - $found_caddy_labels = $labels->filter(function ($value, $key) { - return Str::startsWith($value, 'caddy_'); - }); - if ($found_caddy_labels->count() === 0) { - if ($this->pull_request_id !== 0) { - $domains = str(data_get($this->preview, 'fqdn'))->explode(','); - } else { - $domains = str(data_get($this->application, 'fqdn'))->explode(','); - } - $labels = $labels->merge(fqdnLabelsForCaddy( - network: $this->application->destination->network, - uuid: $this->application->uuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $this->application->isForceHttpsEnabled(), - is_gzip_enabled: $this->application->isGzipEnabled(), - is_stripprefix_enabled: $this->application->isStripprefixEnabled() - )); - } $this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->save(); } else { @@ -1710,6 +1750,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ]); $this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo')); } + $custom_network_aliases = []; + if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) { + $custom_network_aliases = $this->application->custom_network_aliases; + } $docker_compose = [ 'services' => [ $this->container_name => [ @@ -1719,9 +1763,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue 'expose' => $ports, 'networks' => [ $this->destination->network => [ - 'aliases' => [ - $this->container_name, - ], + 'aliases' => array_merge( + [$this->container_name], + $custom_network_aliases + ), ], ], 'mem_limit' => $this->application->limits_memory, @@ -2409,20 +2454,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function next(string $status) { queue_next_deployment($this->application); - // If the deployment is cancelled by the user, don't update the status - if ( - $this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && - $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value - ) { - $this->application_deployment_queue->update([ - 'status' => $status, - ]); + + // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else + if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value || + $this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + return; } - if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) { + + $this->application_deployment_queue->update([ + 'status' => $status, + ]); + + if ($status === ApplicationDeploymentStatus::FAILED->value) { $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); return; } + if ($status === ApplicationDeploymentStatus::FINISHED->value) { if (! $this->only_this_server) { $this->deploy_to_additional_destinations(); diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 84f14ed02..008492342 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -17,11 +17,13 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + public $timeout = 60; + public function __construct() {} public function middleware(): array { - return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)]; } public function handle(): void diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 05a4aa8de..7e246649d 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))->dontRelease()]; + return [(new WithoutOverlapping($this->server->uuid))->expireAfter(600)]; } public function __construct(public Server $server, public bool $manualCleanup = false) {} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 93b203fcb..4d40240f9 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))->dontRelease()]; + return [(new WithoutOverlapping($this->server->uuid))->expireAfter(30)]; } public function backoff(): int diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php index 7fc716f70..4e1ade0da 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))->dontRelease()]; + return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; } public function __construct(public Server $server) {} diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 9818d5c6a..ffa298390 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))->dontRelease()]; + return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; } public function __construct(public Server $server) {} diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 3ed20f907..c5f518e16 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -269,7 +269,7 @@ class Email extends Component } catch (\Throwable $e) { $this->smtpEnabled = false; - return handleError($e); + return handleError($e, $this); } } @@ -337,32 +337,29 @@ class Email extends Component public function copyFromInstanceSettings() { $settings = instanceSettings(); + $this->smtpFromAddress = $settings->smtp_from_address; + $this->smtpFromName = $settings->smtp_from_name; if ($settings->smtp_enabled) { $this->smtpEnabled = true; - $this->smtpFromAddress = $settings->smtp_from_address; - $this->smtpFromName = $settings->smtp_from_name; - $this->smtpRecipients = $settings->smtp_recipients; - $this->smtpHost = $settings->smtp_host; - $this->smtpPort = $settings->smtp_port; - $this->smtpEncryption = $settings->smtp_encryption; - $this->smtpUsername = $settings->smtp_username; - $this->smtpPassword = $settings->smtp_password; - $this->smtpTimeout = $settings->smtp_timeout; $this->resendEnabled = false; - $this->saveModel(); - - return; } + + $this->smtpRecipients = $settings->smtp_recipients; + $this->smtpHost = $settings->smtp_host; + $this->smtpPort = $settings->smtp_port; + $this->smtpEncryption = $settings->smtp_encryption; + $this->smtpUsername = $settings->smtp_username; + $this->smtpPassword = $settings->smtp_password; + $this->smtpTimeout = $settings->smtp_timeout; + if ($settings->resend_enabled) { $this->resendEnabled = true; - $this->resendApiKey = $settings->resend_api_key; $this->smtpEnabled = false; - $this->saveModel(); - - return; } - $this->dispatch('error', 'Instance SMTP/Resend settings are not enabled.'); + $this->resendApiKey = $settings->resend_api_key; + $this->saveModel(); + } public function render() diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index b85023a0c..b7cb693b6 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -68,6 +68,7 @@ class General extends Component 'application.publish_directory' => 'nullable', 'application.ports_exposes' => 'required', 'application.ports_mappings' => 'nullable', + 'application.custom_network_aliases' => 'nullable', 'application.dockerfile' => 'nullable', 'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_tag' => 'nullable', @@ -93,6 +94,9 @@ class General extends Component 'application.settings.is_preserve_repository_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', 'application.redirect' => 'string|required', + 'application.http_basic_auth_enabled' => 'boolean|required', + 'application.http_basic_auth_username' => 'nullable', + 'application.http_basic_auth_password' => 'nullable', ]; protected $validationAttributes = [ @@ -121,6 +125,7 @@ class General extends Component 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', 'application.custom_docker_run_options' => 'Custom docker run commands', + 'application.custom_network_aliases' => 'Custom docker network aliases', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.custom_nginx_configuration' => 'Custom Nginx configuration', @@ -455,7 +460,6 @@ class General extends Component { $config = GenerateConfig::run($this->application, true); $fileName = str($this->application->name)->slug()->append('_config.json'); - dd($config); return response()->streamDownload(function () use ($config) { echo $config; diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 0d7d7755f..475d2dfa8 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -84,11 +84,16 @@ class Heading extends Component return; } $this->setDeploymentUuid(); - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deploymentUuid, force_rebuild: $force_rebuild, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], @@ -126,11 +131,16 @@ class Heading extends Component return; } $this->setDeploymentUuid(); - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deploymentUuid, restart_only: true, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return $this->redirectRoute('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index bdf62706c..88ce65c53 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -159,13 +159,18 @@ class Previews extends Component 'pull_request_html_url' => $pull_request_html_url, ]); } - queue_application_deployment( + $result = queue_application_deployment( application: $this->application, deployment_uuid: $this->deployment_uuid, force_rebuild: false, pull_request_id: $pull_request_id, git_type: $found->git_type ?? null, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index ade297d50..e27a550c1 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -30,11 +30,15 @@ class Source extends Component #[Validate(['nullable', 'string'])] public ?string $gitCommitSha = null; + #[Locked] + public $sources; + public function mount() { try { $this->syncData(); $this->getPrivateKeys(); + $this->getSources(); } catch (\Throwable $e) { handleError($e, $this); } @@ -66,6 +70,14 @@ class Source extends Component }); } + private function getSources() + { + // filter the current source out + $this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) { + return $source->id === $this->application->source_id; + })->sortBy('name'); + } + public function setPrivateKey(int $privateKeyId) { try { @@ -92,4 +104,20 @@ class Source extends Component return handleError($e, $this); } } + + public function changeSource($sourceId, $sourceType) + { + try { + $this->application->update([ + 'source_id' => $sourceId, + 'source_type' => $sourceType, + 'repository_project_id' => null, + ]); + $this->application->refresh(); + $this->getSources(); + $this->dispatch('success', 'Source updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index f301d912e..f03f1256d 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -21,7 +21,7 @@ class General extends Component public string $redis_username; - public string $redis_password; + public ?string $redis_password; public string $redis_version; diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 1759fe08a..71a913add 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -79,7 +79,7 @@ class Destination extends Component $deployment_uuid = new Cuid2; $server = Server::ownedByCurrentTeam()->findOrFail($server_id); $destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail(); - queue_application_deployment( + $result = queue_application_deployment( deployment_uuid: $deployment_uuid, application: $this->resource, server: $server, @@ -87,6 +87,11 @@ class Destination extends Component only_this_server: true, no_questions_asked: true, ); + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } return redirect()->route('project.application.deployment.show', [ 'project_uuid' => data_get($this->resource, 'environment.project.uuid'), diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 35e585c82..699dca187 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -3,10 +3,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable; +use App\Traits\EnvironmentVariableProtection; use Livewire\Component; class All extends Component { + use EnvironmentVariableProtection; + public $resource; public string $resourceClass; @@ -138,17 +141,57 @@ class All extends Component private function handleBulkSubmit() { $variables = parseEnvFormatToArray($this->variables); + $changesMade = false; + $errorOccurred = false; - $this->deleteRemovedVariables(false, $variables); - $this->updateOrCreateVariables(false, $variables); + // Try to delete removed variables + $deletedCount = $this->deleteRemovedVariables(false, $variables); + if ($deletedCount > 0) { + $changesMade = true; + } elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) { + // If we tried to delete but couldn't (due to Docker Compose), mark as error + $errorOccurred = true; + } + + // Update or create variables + $updatedCount = $this->updateOrCreateVariables(false, $variables); + if ($updatedCount > 0) { + $changesMade = true; + } if ($this->showPreview) { $previewVariables = parseEnvFormatToArray($this->variablesPreview); - $this->deleteRemovedVariables(true, $previewVariables); - $this->updateOrCreateVariables(true, $previewVariables); + + // Try to delete removed preview variables + $deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables); + if ($deletedPreviewCount > 0) { + $changesMade = true; + } elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) { + // If we tried to delete but couldn't (due to Docker Compose), mark as error + $errorOccurred = true; + } + + // Update or create preview variables + $updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables); + if ($updatedPreviewCount > 0) { + $changesMade = true; + } } - $this->dispatch('success', 'Environment variables updated.'); + // Debug information + \Log::info('Environment variables update status', [ + 'deletedCount' => $deletedCount, + 'updatedCount' => $updatedCount, + 'deletedPreviewCount' => $deletedPreviewCount ?? 0, + 'updatedPreviewCount' => $updatedPreviewCount ?? 0, + 'changesMade' => $changesMade, + 'errorOccurred' => $errorOccurred, + ]); + + // Only show success message if changes were actually made and no errors occurred + if ($changesMade && ! $errorOccurred) { + $this->dispatch('success', 'Environment variables updated.'); + } } private function handleSingleSubmit($data) @@ -184,11 +227,37 @@ class All extends Component private function deleteRemovedVariables($isPreview, $variables) { $method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; + + // Get all environment variables that will be deleted + $variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get(); + + // If there are no variables to delete, return 0 + if ($variablesToDelete->isEmpty()) { + return 0; + } + + // Check if any of these variables are used in Docker Compose + if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') { + foreach ($variablesToDelete as $envVar) { + [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose); + + if ($isUsed) { + $this->dispatch('error', "Cannot delete environment variable '{$envVar->key}'

Please remove it from the Docker Compose file first."); + + return 0; + } + } + } + + // If we get here, no variables are used in Docker Compose, so we can delete them $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete(); + + return $variablesToDelete->count(); } private function updateOrCreateVariables($isPreview, $variables) { + $count = 0; foreach ($variables as $key => $value) { if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) { continue; @@ -198,8 +267,12 @@ class All extends Component if ($found) { if (! $found->is_shown_once && ! $found->is_multiline) { - $found->value = $value; - $found->save(); + // Only count as a change if the value actually changed + if ($found->value !== $value) { + $found->value = $value; + $found->save(); + $count++; + } } } else { $environment = new EnvironmentVariable; @@ -212,8 +285,11 @@ class All extends Component $environment->resourceable_type = $this->resource->getMorphClass(); $environment->save(); + $count++; } } + + return $count; } public function refreshEnvs() diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 3a7d0faa5..535ac6c67 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\SharedEnvironmentVariable; +use App\Traits\EnvironmentVariableProtection; use Livewire\Component; class Show extends Component { + use EnvironmentVariableProtection; + public $parameters; public ModelsEnvironmentVariable|SharedEnvironmentVariable $env; @@ -40,6 +43,8 @@ class Show extends Component public bool $is_really_required = false; + public bool $is_redis_credential = false; + protected $listeners = [ 'refreshEnvs' => 'refresh', 'refresh', @@ -65,7 +70,9 @@ class Show extends Component } $this->parameters = get_route_parameters(); $this->checkEnvs(); - + if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) { + $this->is_redis_credential = true; + } } public function refresh() @@ -171,6 +178,17 @@ class Show extends Component public function delete() { try { + // Check if the variable is used in Docker Compose + if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) { + [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose); + + if ($isUsed) { + $this->dispatch('error', "Cannot delete environment variable '{$this->env->key}'

Please remove it from the Docker Compose file first."); + + return; + } + } + $this->env->delete(); $this->dispatch('environmentVariableDeleted'); $this->dispatch('success', 'Environment variable deleted successfully.'); diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 6277a24bd..7db890638 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -38,7 +38,8 @@ class DynamicConfigurations extends Component $contents = collect([]); foreach ($files as $file) { $without_extension = str_replace('.', '|', $file); - $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); + $content = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); + $contents[$without_extension] = $content ?? ''; } $this->contents = $contents; $this->dispatch('$refresh'); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index b2394d7b0..73e8c7398 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -177,7 +177,7 @@ class SettingsEmail extends Component } catch (\Throwable $e) { $this->smtpEnabled = false; - return handleError($e); + return handleError($e, $this); } } @@ -207,7 +207,7 @@ class SettingsEmail extends Component } catch (\Throwable $e) { $this->resendEnabled = false; - return handleError($e); + return handleError($e, $this); } } diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index df450cf7e..8a9cc456f 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -12,19 +12,30 @@ class Index extends Component public bool $alreadySubscribed = false; + public bool $isUnpaid = false; + + public bool $isCancelled = false; + + public bool $isMember = false; + + public bool $loading = true; + public function mount() { if (! isCloud()) { return redirect(RouteServiceProvider::HOME); } if (auth()->user()?->isMember()) { - return redirect()->route('dashboard'); + $this->isMember = true; } if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { return redirect()->route('subscription.show'); } $this->settings = instanceSettings(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); + if (! $this->alreadySubscribed) { + $this->loading = false; + } } public function stripeCustomerPortal() @@ -37,6 +48,41 @@ class Index extends Component return redirect($session->url); } + public function getStripeStatus() + { + try { + $subscription = currentTeam()->subscription; + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $customer = $stripe->customers->retrieve(currentTeam()->subscription->stripe_customer_id); + if ($customer) { + $subscriptions = $stripe->subscriptions->all(['customer' => $customer->id]); + $currentTeam = currentTeam()->id ?? null; + if (count($subscriptions->data) > 0 && $currentTeam) { + $foundSubscription = collect($subscriptions->data)->firstWhere('metadata.team_id', $currentTeam); + if ($foundSubscription) { + $status = data_get($foundSubscription, 'status'); + $subscription->update([ + 'stripe_subscription_id' => $foundSubscription->id, + ]); + if ($status === 'unpaid') { + $this->isUnpaid = true; + } + } + } + if (count($subscriptions->data) === 0) { + $this->isCancelled = true; + } + } + } catch (\Exception $e) { + // Log the error + logger()->error('Stripe API error: ' . $e->getMessage()); + // Set a flag to show an error message to the user + $this->addError('stripe', 'Could not retrieve subscription information. Please try again later.'); + } finally { + $this->loading = false; + } + } + public function render() { return view('livewire.subscription.index'); diff --git a/app/Models/Application.php b/app/Models/Application.php index d07577cc7..3306510d1 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2; 'start_command' => ['type' => 'string', 'description' => 'Start command.'], 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'], 'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'], + 'custom_network_aliases' => ['type' => 'string', 'nullable' => true, 'description' => 'Network aliases for Docker container.'], 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'], 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'], 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], @@ -102,6 +103,9 @@ use Visus\Cuid2\Cuid2; 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'], 'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'], 'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'], + 'http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], + 'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'], + 'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'], ] )] @@ -115,6 +119,68 @@ class Application extends BaseModel protected $appends = ['server_status']; + protected $casts = ['custom_network_aliases' => 'array']; + + public function customNetworkAliases(): Attribute + { + return Attribute::make( + set: function ($value) { + if (is_null($value) || $value === '') { + return null; + } + + // If it's already a JSON string, decode it + if (is_string($value) && $this->isJson($value)) { + $value = json_decode($value, true); + } + + // If it's a string but not JSON, treat it as a comma-separated list + if (is_string($value) && ! is_array($value)) { + $value = explode(',', $value); + } + + $value = collect($value) + ->map(function ($alias) { + if (is_string($alias)) { + return str_replace(' ', '-', trim($alias)); + } + + return null; + }) + ->filter() + ->unique() // Remove duplicate values + ->values() + ->toArray(); + + return empty($value) ? null : json_encode($value); + }, + get: function ($value) { + if (is_null($value)) { + return null; + } + + if (is_string($value) && $this->isJson($value)) { + return json_decode($value, true); + } + + return is_array($value) ? $value : []; + } + ); + } + + /** + * Check if a string is a valid JSON + */ + private function isJson($string) + { + if (! is_string($string)) { + return false; + } + json_decode($string); + + return json_last_error() === JSON_ERROR_NONE; + } + protected static function booted() { static::addGlobalScope('withRelations', function ($builder) { @@ -392,22 +458,23 @@ class Application extends BaseModel { return Attribute::make( get: function () { + $base_dir = $this->base_directory ?? '/'; if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { if (str($this->git_repository)->contains('bitbucket')) { - return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}"; + return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}{$base_dir}"; } - return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; + return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}{$base_dir}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); if (str($this->git_repository)->contains('bitbucket')) { - return "https://{$git_repository}/src/{$this->git_branch}"; + return "https://{$git_repository}/src/{$this->git_branch}{$base_dir}"; } - return "https://{$git_repository}/tree/{$this->git_branch}"; + return "https://{$git_repository}/tree/{$this->git_branch}{$base_dir}"; } return $this->git_repository; diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index fd8f1cba2..2a9bea67a 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; use OpenApi\Attributes as OA; #[OA\Schema( @@ -101,17 +102,23 @@ class ApplicationDeploymentQueue extends Model 'hidden' => $hidden, 'batch' => 1, ]; - if ($this->logs) { - $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR); - $newLogEntry['order'] = count($previousLogs) + 1; - $previousLogs[] = $newLogEntry; - $this->update([ - 'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR), - ]); - } else { - $this->update([ - 'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR), - ]); - } + + // Use a transaction to ensure atomicity + DB::transaction(function () use ($newLogEntry) { + // Reload the model to get the latest logs + $this->refresh(); + + if ($this->logs) { + $previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR); + $newLogEntry['order'] = count($previousLogs) + 1; + $previousLogs[] = $newLogEntry; + $this->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR); + } else { + $this->logs = json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR); + } + + // Save without triggering events to prevent potential race conditions + $this->saveQuietly(); + }); } } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 0e702e460..97c32fa31 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -17,6 +17,8 @@ use phpseclib3\Crypt\PublicKeyLoader; 'name' => ['type' => 'string'], 'description' => ['type' => 'string'], 'private_key' => ['type' => 'string', 'format' => 'private-key'], + 'public_key' => ['type' => 'string'], + 'fingerprint' => ['type' => 'string'], 'is_git_related' => ['type' => 'boolean'], 'team_id' => ['type' => 'integer'], 'created_at' => ['type' => 'string'], diff --git a/app/Models/Server.php b/app/Models/Server.php index 56aa58e87..caf65cc58 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -20,7 +20,6 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Stringable; use OpenApi\Attributes as OA; @@ -493,11 +492,7 @@ $schema://$host { if ($proxyType === ProxyTypes::TRAEFIK->value) { // Do nothing } elseif ($proxyType === ProxyTypes::CADDY->value) { - if (isDev()) { - $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'; - } else { - $proxy_path = $proxy_path.'/caddy'; - } + $proxy_path = $proxy_path.'/caddy'; } elseif ($proxyType === ProxyTypes::NGINX->value) { if (isDev()) { $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; @@ -925,7 +920,7 @@ $schema://$host { public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4'; + $isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4'; if ($isFunctional === false) { Storage::disk('ssh-mux')->delete($this->muxFilename()); @@ -1026,22 +1021,11 @@ $schema://$host { $this->refresh(); $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; $isReachable = (bool) $this->settings->is_reachable; - - Log::debug('Server reachability check', [ - 'server_id' => $this->id, - 'is_reachable' => $isReachable, - 'notification_sent' => $unreachableNotificationSent, - 'unreachable_count' => $this->unreachable_count, - ]); - if ($isReachable === true) { $this->unreachable_count = 0; $this->save(); if ($unreachableNotificationSent === true) { - Log::debug('Server is now reachable, sending notification', [ - 'server_id' => $this->id, - ]); $this->sendReachableNotification(); } @@ -1049,17 +1033,10 @@ $schema://$host { } $this->increment('unreachable_count'); - Log::debug('Incremented unreachable count', [ - 'server_id' => $this->id, - 'new_count' => $this->unreachable_count, - ]); if ($this->unreachable_count === 1) { $this->settings->is_reachable = true; $this->settings->save(); - Log::debug('First unreachable attempt, marking as reachable', [ - 'server_id' => $this->id, - ]); return; } @@ -1068,11 +1045,6 @@ $schema://$host { $failedChecks = 0; for ($i = 0; $i < 3; $i++) { $status = $this->serverStatus(); - Log::debug('Additional reachability check', [ - 'server_id' => $this->id, - 'attempt' => $i + 1, - 'status' => $status, - ]); sleep(5); if (! $status) { $failedChecks++; @@ -1080,9 +1052,6 @@ $schema://$host { } if ($failedChecks === 3 && ! $unreachableNotificationSent) { - Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [ - 'server_id' => $this->id, - ]); $this->sendUnreachableNotification(); } } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index c2a0df8cd..40d183033 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -141,6 +141,6 @@ class ServiceDatabase extends BaseModel str($this->databaseType())->contains('postgres') || str($this->databaseType())->contains('postgis') || str($this->databaseType())->contains('mariadb') || - str($this->databaseType())->contains('mongodb'); + str($this->databaseType())->contains('mongo'); } } diff --git a/app/Models/Team.php b/app/Models/Team.php index d36f8c1ab..42b88f9e7 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -192,8 +192,6 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen public function subscriptionEnded() { $this->subscription->update([ - 'stripe_subscription_id' => null, - 'stripe_plan_id' => null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, 'stripe_trial_already_ended' => false, diff --git a/app/Traits/EnvironmentVariableProtection.php b/app/Traits/EnvironmentVariableProtection.php new file mode 100644 index 000000000..b6b8d2687 --- /dev/null +++ b/app/Traits/EnvironmentVariableProtection.php @@ -0,0 +1,63 @@ +startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL'); + } + + /** + * Check if an environment variable is used in Docker Compose + * + * @param string $key The environment variable key to check + * @param string|null $dockerCompose The Docker Compose YAML content + * @return array [bool $isUsed, string $reason] Whether the variable is used and the reason if it is + */ + protected function isEnvironmentVariableUsedInDockerCompose(string $key, ?string $dockerCompose): array + { + if (empty($dockerCompose)) { + return [false, '']; + } + + try { + $dockerComposeData = Yaml::parse($dockerCompose); + $dockerEnvVars = data_get($dockerComposeData, 'services.*.environment'); + + foreach ($dockerEnvVars as $serviceEnvs) { + if (! is_array($serviceEnvs)) { + continue; + } + + // Check for direct variable usage + foreach ($serviceEnvs as $env => $value) { + if ($env === $key) { + return [true, "Environment variable '{$key}' is used directly in the Docker Compose file."]; + } + } + + // Check for variable references in values + foreach ($serviceEnvs as $env => $value) { + if (is_string($value) && str_contains($value, '$'.$key)) { + return [true, "Environment variable '{$key}' is referenced in the Docker Compose file."]; + } + } + } + } catch (\Exception $e) { + // If there's an error parsing the Docker Compose file, we'll assume it's not used + return [false, '']; + } + + return [false, '']; + } +} diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index d5283898e..3f1e8513c 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -24,6 +24,26 @@ function queue_application_deployment(Application $application, string $deployme if ($destination) { $destination_id = $destination->id; } + + // 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) + ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value]) + ->first(); + + if ($existing_deployment) { + // If force_rebuild is true or rollback is true or no_questions_asked is true, we'll still create a new deployment + if (! $force_rebuild && ! $rollback && ! $no_questions_asked) { + // Return the existing deployment's details + return [ + 'status' => 'skipped', + 'message' => 'Deployment already queued for this commit.', + 'deployment_uuid' => $existing_deployment->deployment_uuid, + 'existing_deployment' => $existing_deployment, + ]; + } + } + $deployment = ApplicationDeploymentQueue::create([ 'application_id' => $application_id, 'application_name' => $application->name, @@ -47,11 +67,17 @@ function queue_application_deployment(Application $application, string $deployme ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); - } elseif (next_queuable($server_id, $application_id)) { + } elseif (next_queuable($server_id, $application_id, $commit)) { ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, ); } + + return [ + 'status' => 'queued', + 'message' => 'Deployment queued.', + 'deployment_uuid' => $deployment_uuid, + ]; } function force_start_deployment(ApplicationDeploymentQueue $deployment) { @@ -78,20 +104,35 @@ function queue_next_deployment(Application $application) } } -function next_queuable(string $server_id, string $application_id): bool +function next_queuable(string $server_id, string $application_id, string $commit = 'HEAD'): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); - $same_application_deployments = $deployments->where('application_id', $application_id); - $in_progress = $same_application_deployments->filter(function ($value, $key) { - return $value->status === 'in_progress'; - }); - if ($in_progress->count() > 0) { + // Check if there's already a deployment in progress for this application and commit + $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('commit', $commit) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->first(); + + if ($existing_deployment) { return false; } + + // Check if there's any deployment in progress for this application + $in_progress = ApplicationDeploymentQueue::where('application_id', $application_id) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->exists(); + + if ($in_progress) { + return false; + } + + // Check server's concurrent build limit $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; + $active_deployments = ApplicationDeploymentQueue::where('server_id', $server_id) + ->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value) + ->count(); - if ($deployments->count() > $concurrent_builds) { + if ($active_deployments >= $concurrent_builds) { return false; } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 8bad79708..d094b0f57 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -296,7 +296,8 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) return $payload; } -function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null) + +function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null) { $labels = collect([]); if ($serviceLabels) { @@ -304,6 +305,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, } else { $labels->push("caddy_ingress_network={$network}"); } + + $http_basic_auth_enabled = $http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; + foreach ($domains as $loop => $domain) { $url = Url::fromString($domain); $host = $url->getHost(); @@ -340,20 +344,30 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { $labels->push("caddy_{$loop}.redir={$schema}://{$host_without_www}{uri}"); } - if (isDev()) { - // $labels->push("caddy_{$loop}.tls=internal"); + if ($http_basic_auth_enabled) { + $http_basic_auth_password = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]); + $labels->push("caddy_{$loop}.basicauth.{$http_basic_auth_username}=\"{$http_basic_auth_password}\""); } } return $labels->sort(); } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both') + +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null) { $labels = collect([]); $labels->push('traefik.enable=true'); $labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); + $http_basic_auth_enabled = $http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; + $http_basic_auth_label = "http-basic-auth-{$uuid}"; + + if ($http_basic_auth_enabled) { + $http_basic_auth_password = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]); + $labels->push("traefik.http.middlewares.{$http_basic_auth_label}.basicauth.users={$http_basic_auth_username}:{$http_basic_auth_password}"); + } + $middlewares_from_labels = collect([]); if ($serviceLabels) { @@ -511,6 +525,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + if ($http_basic_auth_enabled) { + $middlewares->push($http_basic_auth_label); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -534,6 +551,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + if ($http_basic_auth_enabled) { + $middlewares->push($http_basic_auth_label); + } $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { $middlewares->push($middleware_name); }); @@ -562,6 +582,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview if ($pull_request_id !== 0) { $appUuid = $appUuid.'-pr-'.$pull_request_id; } + ray($application); $labels = collect([]); if ($pull_request_id === 0) { if ($application->fqdn) { @@ -577,7 +598,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + redirect_direction: $application->redirect, + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); break; case ProxyTypes::CADDY->value: @@ -589,7 +613,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + redirect_direction: $application->redirect, + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); break; } @@ -601,7 +628,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + redirect_direction: $application->redirect, + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -611,7 +641,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + redirect_direction: $application->redirect, + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); } } @@ -631,7 +664,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); break; case ProxyTypes::CADDY->value: @@ -642,7 +678,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); break; } @@ -653,7 +692,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, @@ -662,7 +704,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() + is_stripprefix_enabled: $application->isStripprefixEnabled(), + http_basic_auth_enabled: $application->http_basic_auth_enabled, + http_basic_auth_username: $application->http_basic_auth_username, + http_basic_auth_password: $application->http_basic_auth_password, )); } } @@ -682,8 +727,10 @@ function isDatabaseImage(?string $image = null) $image = str($image)->append(':latest'); } $imageName = $image->before(':'); - if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { - return true; + foreach (DATABASE_DOCKER_IMAGES as $database_docker_image) { + if (str($imageName)->contains($database_docker_image)) { + return true; + } } return false; diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 81f8ff18a..0de2f2fd9 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -52,6 +52,9 @@ function generateGithubToken(GithubApp $source, string $type) if (! $response->successful()) { $error = data_get($response->json(), 'message', 'no error message found'); + if ($error === 'Not Found') { + $error = 'Repository not found. Is it moved or deleted?'; + } throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index b90de4dbc..44e20c9b3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2987,7 +2987,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $predefinedPort = '8000'; } if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; $savedService = ServiceDatabase::firstOrCreate([ @@ -2999,178 +2999,174 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } - $environment = collect(data_get($service, 'environment', [])); - $buildArgs = collect(data_get($service, 'build.args', [])); - $environment = $environment->merge($buildArgs); - // convert environment variables to one format - $environment = convertToKeyValueCollection($environment); + // Check if image changed + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + } + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); - // Add Coolify defined environments - $allEnvironments = $resource->environment_variables()->get(['key', 'value']); + // convert environment variables to one format + $environment = convertToKeyValueCollection($environment); - $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { - return [$item['key'] => $item['value']]; - }); - // filter and add magic environments - foreach ($environment as $key => $value) { - // Get all SERVICE_ variables from keys and values - $key = str($key); - $value = str($value); + // Add Coolify defined environments + $allEnvironments = $resource->environment_variables()->get(['key', 'value']); - $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; - preg_match_all($regex, $value, $valueMatches); - if (count($valueMatches[1]) > 0) { - foreach ($valueMatches[1] as $match) { - $match = replaceVariables($match); - if ($match->startsWith('SERVICE_')) { - if ($magicEnvironments->has($match->value())) { - continue; - } - $magicEnvironments->put($match->value(), ''); + $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { + return [$item['key'] => $item['value']]; + }); + // filter and add magic environments + foreach ($environment as $key => $value) { + // Get all SERVICE_ variables from keys and values + $key = str($key); + $value = str($value); + + $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; + preg_match_all($regex, $value, $valueMatches); + if (count($valueMatches[1]) > 0) { + foreach ($valueMatches[1] as $match) { + $match = replaceVariables($match); + if ($match->startsWith('SERVICE_')) { + if ($magicEnvironments->has($match->value())) { + continue; } - } - } - - // Get magic environments where we need to preset the FQDN - if ($key->startsWith('SERVICE_FQDN_')) { - // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 - if (substr_count(str($key)->value(), '_') === 3) { - $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); - $port = $key->afterLast('_')->value(); - } else { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - $port = null; - } - if ($isApplication) { - $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); - } elseif ($isService) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); - } - } - - if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { - $path = $value->value(); - if ($path !== '/') { - $fqdn = "$fqdn$path"; - } - } - $fqdnWithPort = $fqdn; - if ($port) { - $fqdnWithPort = "$fqdn:$port"; - } - if ($isApplication && is_null($resource->fqdn)) { - data_forget($resource, 'environment_variables'); - data_forget($resource, 'environment_variables_preview'); - $resource->fqdn = $fqdnWithPort; - $resource->save(); - } elseif ($isService && is_null($savedService->fqdn)) { - $savedService->fqdn = $fqdnWithPort; - $savedService->save(); - } - - if (substr_count(str($key)->value(), '_') === 2) { - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } - if (substr_count(str($key)->value(), '_') === 3) { - $newKey = str($key)->beforeLast('_'); - $resource->environment_variables()->firstOrCreate([ - 'key' => $newKey->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); + $magicEnvironments->put($match->value(), ''); } } } - $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); - if ($magicEnvironments->count() > 0) { - foreach ($magicEnvironments as $key => $value) { - $key = str($key); - $value = replaceVariables($value); - $command = parseCommandFromMagicEnvVariable($key); - $found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first(); - if ($found) { - continue; - } - if ($command->value() === 'FQDN') { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - if ($isApplication) { - $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); - } elseif ($isService) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); - } elseif ($command->value() === 'URL') { - $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - if ($isApplication) { - $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); - } elseif ($isService) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); - } - $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $fqdn, - 'is_build_time' => false, - 'is_preview' => false, - ]); + // Get magic environments where we need to preset the FQDN + if ($key->startsWith('SERVICE_FQDN_')) { + // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 + if (substr_count(str($key)->value(), '_') === 3) { + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); + } else { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + $port = null; + } + if ($isApplication) { + $fqdn = generateFqdn($server, "$uuid"); + } elseif ($isService) { + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); } else { - $value = generateEnvValue($command, $resource); - $resource->environment_variables()->firstOrCreate([ - 'key' => $key->value(), - 'resourceable_type' => get_class($resource), - 'resourceable_id' => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, - ]); + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); } } + + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + $path = $value->value(); + if ($path !== '/') { + $fqdn = "$fqdn$path"; + } + } + $fqdnWithPort = $fqdn; + if ($port) { + $fqdnWithPort = "$fqdn:$port"; + } + if ($isApplication && is_null($resource->fqdn)) { + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->fqdn = $fqdnWithPort; + $resource->save(); + } elseif ($isService && is_null($savedService->fqdn)) { + $savedService->fqdn = $fqdnWithPort; + $savedService->save(); + } + + if (substr_count(str($key)->value(), '_') === 2) { + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + if (substr_count(str($key)->value(), '_') === 3) { + $newKey = str($key)->beforeLast('_'); + $resource->environment_variables()->firstOrCreate([ + 'key' => $newKey->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + + $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); + if ($magicEnvironments->count() > 0) { + foreach ($magicEnvironments as $key => $value) { + $key = str($key); + $value = replaceVariables($value); + $command = parseCommandFromMagicEnvVariable($key); + $found = $resource->environment_variables()->where('key', $key->value())->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first(); + if ($found) { + continue; + } + if ($command->value() === 'FQDN') { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } elseif ($command->value() === 'URL') { + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + $value = generateEnvValue($command, $resource); + $resource->environment_variables()->firstOrCreate([ + 'key' => $key->value(), + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } } } } @@ -3201,6 +3197,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $use_network_mode = data_get($service, 'network_mode') !== null; $depends_on = collect(data_get($service, 'depends_on', [])); $labels = collect(data_get($service, 'labels', [])); + if ($labels->count() > 0) { + if (isAssociativeArray($labels)) { + $newLabels = collect([]); + $labels->each(function ($value, $key) use ($newLabels) { + $newLabels->push("$key=$value"); + }); + $labels = $newLabels; + } + } $environment = collect(data_get($service, 'environment', [])); $ports = collect(data_get($service, 'ports', [])); $buildArgs = collect(data_get($service, 'build.args', [])); diff --git a/config/constants.php b/config/constants.php index 3f23a191b..c057a85db 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,14 +2,15 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.405', + 'version' => '4.0.0-beta.410', 'helper_version' => '1.0.8', - 'realtime_version' => '1.0.6', + 'realtime_version' => '1.0.7', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'registry_url' => env('REGISTRY_URL', 'ghcr.io'), 'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'), + 'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), ], diff --git a/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php new file mode 100644 index 000000000..61fadd0e5 --- /dev/null +++ b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php @@ -0,0 +1,22 @@ +text('custom_network_aliases')->nullable(); + }); + } + + public function down() + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_network_aliases'); + }); + } +}; diff --git a/database/migrations/2025_04_01_124212_stripe_comment_nullable.php b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php new file mode 100644 index 000000000..7f61c202e --- /dev/null +++ b/database/migrations/2025_04_01_124212_stripe_comment_nullable.php @@ -0,0 +1,28 @@ +longText('stripe_comment')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->longText('stripe_comment')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php b/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php new file mode 100644 index 000000000..247300abd --- /dev/null +++ b/database/migrations/2025_04_17_110026_add_application_http_basic_auth_fields.php @@ -0,0 +1,32 @@ +boolean('http_basic_auth_enabled')->default(false); + $table->string('http_basic_auth_username')->nullable(true)->default(null); + $table->string('http_basic_auth_password')->nullable(true)->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('http_basic_auth_enabled'); + $table->dropColumn('http_basic_auth_username'); + $table->dropColumn('http_basic_auth_password'); + }); + } +}; diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json index aea3952c0..1c329e47f 100644 --- a/docker/coolify-realtime/package-lock.json +++ b/docker/coolify-realtime/package-lock.json @@ -7,9 +7,9 @@ "dependencies": { "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "5.5.0", - "axios": "1.7.9", + "axios": "1.8.4", "cookie": "1.0.2", - "dotenv": "16.4.7", + "dotenv": "16.5.0", "node-pty": "1.0.0", "ws": "8.18.1" } @@ -36,9 +36,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -90,9 +90,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 0a9b80cb5..7851d7f4d 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -5,9 +5,9 @@ "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "5.5.0", "cookie": "1.0.2", - "axios": "1.7.9", - "dotenv": "16.4.7", + "axios": "1.8.4", + "dotenv": "16.5.0", "node-pty": "1.0.0", "ws": "8.18.1" } -} +} \ No newline at end of file diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 38bb50f3f..8d74ba107 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -89,9 +89,9 @@ RUN echo "alias ll='ls -al'" >> /etc/profile && \ # Install Cloudflared based on architecture RUN mkdir -p /usr/local/bin && \ if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \ + curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" -o /usr/local/bin/cloudflared; \ elif [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \ + curl -sSL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64" -o /usr/local/bin/cloudflared; \ fi && \ chmod +x /usr/local/bin/cloudflared diff --git a/hooks/pre-commit b/hooks/pre-commit index 69a5a9d41..029f67917 100644 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,7 +1,7 @@ #!/bin/sh # Detect whether /dev/tty is available & functional if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then - exec < /dev/tty + exec Dashboard

Your self-hosted infrastructure.
@if (request()->query->get('success')) -
- - - - Your subscription has been activated! Welcome onboard!
It could take a few seconds before your +
+ Your subscription has been activated! Welcome onboard! It could take a few seconds before your subscription is activated.
Please be patient.
@endif diff --git a/resources/views/livewire/navbar-delete-team.blade.php b/resources/views/livewire/navbar-delete-team.blade.php index 60b25a3d5..d0c47b874 100644 --- a/resources/views/livewire/navbar-delete-team.blade.php +++ b/resources/views/livewire/navbar-delete-team.blade.php @@ -1,13 +1,6 @@ -
- + + shortConfirmationLabel="Team Name" step3ButtonText="Permanently Delete" />
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 8bf41a986..994d95bb8 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -342,6 +342,24 @@ @endif + @if (!$application->destination->server->isSwarm()) + + @endif +
+ +

HTTP Basic Authentication

+
+
+ +
+ +
+ + +
@if ($application->settings->is_container_label_readonly_enabled) diff --git a/resources/views/livewire/project/application/source.blade.php b/resources/views/livewire/project/application/source.blade.php index b542d5428..85382055e 100644 --- a/resources/views/livewire/project/application/source.blade.php +++ b/resources/views/livewire/project/application/source.blade.php @@ -26,6 +26,11 @@
Code source of your application.
+ @if (!$privateKeyId) +
Currently connected source: {{ data_get($application, 'source.name', 'No source connected') }} +
+ @endif
@@ -34,6 +39,7 @@
+ @if ($privateKeyId)

Deploy Key

Currently attached Private Key: @endforeach
+ @else +
+

Change Git Source

+
+ @forelse ($sources as $source) +
+ + +
+
+ {{ $source->name }} + @if ($application->source_id === $source->id) + (current) + @endif +
+
+ {{ $source->organization ?? 'Personal Account' }} +
+
+
+
+
+ @empty +
No other sources found
+ @endforelse +
+
@endif diff --git a/resources/views/livewire/project/database/backup-edit.blade.php b/resources/views/livewire/project/database/backup-edit.blade.php index 603983864..f83af91a0 100644 --- a/resources/views/livewire/project/database/backup-edit.blade.php +++ b/resources/views/livewire/project/database/backup-edit.blade.php @@ -20,7 +20,12 @@
- + @if ($s3s->count() > 0) + + @else + + @endif
@if ($backup->save_s3)
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 20acf52c0..577c0d3e9 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -13,22 +13,41 @@ helper="For all available images, check here:

https://hub.docker.com/_/redis" />
- @if (version_compare($redis_version, '6.0', '>=')) - If you change the values in the database, please sync it here, + otherwise + automations won't work.
Changing them here will not change the values in the database. +
+
+ @if (version_compare($redis_version, '6.0', '>=')) + + @endif + +
+ @else +
You can only change the username and password in the database after + initial start.
+
+ @if (version_compare($redis_version, '6.0', '>=')) + - @endif - isSharedVariable('REDIS_USERNAME')" /> + @endif + + :disabled="$this->isSharedVariable('REDIS_PASSWORD')" /> +
+ @endif
Unable to deploy. + href="{{ route('project.service.environment-variables', $parameters) }}" wire:navigate> Required environment variables missing.
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index e3130e805..4d54df3bd 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -31,46 +31,48 @@ @else
@if ($is_multiline) - - + + @else - - + + @endif @if ($is_shared) - + @endif
@endif
- @if ($type === 'service') - - - - @else - @if ($is_shared) + @if (!$is_redis_credential) + @if ($type === 'service') + @else - @if ($isSharedVariable) - - @else + @if ($is_shared) - - @if ($is_multiline === false) - + + @else + @if ($isSharedVariable) + + @else + + + @if ($is_multiline === false) + + @endif @endif @endif @endif diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php index 5be8d36a5..367820491 100644 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ b/resources/views/livewire/server/proxy/deploy.blade.php @@ -72,6 +72,7 @@ @script