diff --git a/.env.development.example b/.env.development.example index 0f9e4c72e..3ad26fa50 100644 --- a/.env.development.example +++ b/.env.development.example @@ -6,7 +6,7 @@ APP_KEY= APP_URL=http://localhost APP_PORT=8000 APP_DEBUG=true -MUX_ENABLED=false +SSH_MUX_ENABLED=false # Enable Laravel Telescope for debugging TELESCOPE_ENABLED=false @@ -23,9 +23,22 @@ DB_PASSWORD=password DB_HOST=host.docker.internal DB_PORT=5432 -#Set custom ray port +# Ray Configuration +# Set to true to enable Ray +RAY_ENABLED=false +# Set custom ray port RAY_PORT= +# Clockwork Configuration +CLOCKWORK_ENABLED=false +CLOCKWORK_QUEUE_COLLECT=true + +# Enable Laravel Telescope for debugging +TELESCOPE_ENABLED=false + +# Selenium Driver URL for Dusk +DUSK_DRIVER_URL=http://selenium:4444 + # Special Keys for Andras # For cache purging BUNNY_API_KEY= diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index d9921b363..4add8516e 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -25,6 +25,10 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - name: Build image and push to registry uses: docker/build-push-action@v5 with: @@ -33,7 +37,9 @@ jobs: file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -47,6 +53,10 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - name: Build image and push to registry uses: docker/build-push-action@v5 with: @@ -55,7 +65,9 @@ jobs: file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + labels: | + coolify.managed=true merge-manifest: runs-on: ubuntu-latest permissions: @@ -75,10 +87,15 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - name: Create & publish manifest run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next - uses: sarisia/actions-status-discord@v1 if: always() with: webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} + diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 37199919a..830b36d28 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -2,7 +2,7 @@ name: Coolify Helper Image (v4) on: push: - branches: [ "main" ] + branches: [ "main", "next" ] paths: - .github/workflows/coolify-helper.yml - docker/coolify-helper/Dockerfile @@ -38,6 +38,8 @@ jobs: platforms: linux/amd64 push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -64,6 +66,8 @@ jobs: platforms: linux/aarch64 push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + labels: | + coolify.managed=true merge-manifest: runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index cf2fae8f3..d7a680170 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -16,6 +16,12 @@ env: jobs: amd64: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + actions: write steps: - uses: actions/checkout@v4 - name: Login to ghcr.io @@ -37,6 +43,9 @@ jobs: permissions: contents: read packages: write + attestations: write + id-token: write + actions: write steps: - uses: actions/checkout@v4 - name: Login to ghcr.io @@ -58,6 +67,9 @@ jobs: permissions: contents: read packages: write + attestations: write + id-token: write + actions: write needs: [amd64, aarch64] steps: - name: Checkout diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml index e4bad6a65..c78c865bf 100644 --- a/.github/workflows/production-build.yml +++ b/.github/workflows/production-build.yml @@ -4,6 +4,8 @@ on: push: branches: ["main"] paths-ignore: + - .github/workflows/coolify-helper.yml + - docker/coolify-helper/Dockerfile - templates/service-templates.json env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9618bfae5..590360ddb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,27 @@ -# Contributing +# Contributing to Coolify > "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai) You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. +## Table of Contents -## Code Contribution +1. [Setup Development Environment](#1-setup-development-environment) +2. [Verify Installation](#2-verify-installation-optional) +3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) +4. [Set up Environment Variables](#4-set-up-environment-variables) +5. [Start Coolify](#5-start-coolify) +6. [Start Development](#6-start-development) +7. [Development Notes](#7-development-notes) +8. [Create a Pull Request](#8-create-a-pull-request) +9. [Additional Contribution Guidelines](#additional-contribution-guidelines) -## 1. Setup your development environment +## 1. Setup Development Environment Follow the steps below for your operating system: -### Windows +
+Windows 1. Install `docker-ce`, Docker Desktop (or similar): - Docker CE (recommended): @@ -25,7 +35,10 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2) -### MacOS +
+ +
+MacOS 1. Install Orbstack, Docker Desktop (or similar): - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): @@ -36,7 +49,10 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin) -### Linux +
+ +
+Linux 1. Install Docker Engine, Docker Desktop (or similar): - Docker Engine (recommended, as there is no VM overhead): @@ -47,8 +63,9 @@ Follow the steps below for your operating system: 2. Install Spin: - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions) +
-## 2. Verify installation (optional) +## 2. Verify Installation (Optional) After installing Docker (or Orbstack) and Spin, verify the installation: @@ -60,25 +77,20 @@ After installing Docker (or Orbstack) and Spin, verify the installation: ``` You should see version information for both Docker and Spin. - -## 3. Fork the Coolify repository and setup your local repository +## 3. Fork and Setup Local Repository 1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account. -2. Install a code editor on your machine (below are some popular choices, choose one): +2. Install a code editor on your machine (choose one): - - Visual Studio Code (recommended free): - - Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download) - - - Cursor (recommended but paid for getting the full benefits): - - Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/) - - - Zed (very fast code editor): - - macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download) - - Windows: Not available yet + | Editor | Platform | Download Link | + |--------|----------|---------------| + | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) | + | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) | + | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) | 3. Clone the Coolify Repository from your fork to your local machine - - Use `git clone` in the command line + - Use `git clone` in the command line, or - Use GitHub Desktop (recommended): - Download and install from [https://desktop.github.com/](https://desktop.github.com/) - Open GitHub Desktop and login with your GitHub account @@ -86,37 +98,32 @@ After installing Docker (or Orbstack) and Spin, verify the installation: 4. Open the cloned Coolify Repository in your chosen code editor. - ## 4. Set up Environment Variables 1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository. - 2. Duplicate the `.env.development.example` file and rename the copy to `.env`. - 3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup. - 4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`. - 5. Save the changes to your `.env` file. - ## 5. Start Coolify 1. Open a terminal in the local Coolify directory. - 2. Run the following command in the terminal (leave that terminal open): - ``` + ```bash spin up ``` - Note: You may see some errors, but don't worry; this is expected. + +> [!NOTE] +> You may see some errors, but don't worry; this is expected. 3. If you encounter permission errors, especially on macOS, use: - ``` + ```bash sudo spin up ``` -Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. - +> [!NOTE] +> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. ## 6. Start Development @@ -126,15 +133,17 @@ Note: If you change environment variables afterwards or anything seems broken, p - Password: `password` 2. Additional development tools: - - Laravel Horizon (scheduler): `http://localhost:8000/horizon` - Note: Only accessible when logged in as root user - - Mailpit (email catcher): `http://localhost:8025` - - Telescope (debugging tool): `http://localhost:8000/telescope` - Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file: - ```env - TELESCOPE_ENABLED=true - ``` + | Tool | URL | Note | + |------|-----|------| + | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | + | Mailpit (email catcher) | `http://localhost:8025` | | + | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | +> [!NOTE] +> To enable Telescope, add the following to your `.env` file: +> ```env +> TELESCOPE_ENABLED=true +> ``` ## 7. Development Notes @@ -150,18 +159,12 @@ When working on Coolify, keep the following in mind: docker exec -it coolify php artisan migrate:fresh --seed ``` -3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement specific issues. +3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues. -Remember, forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. +> [!IMPORTANT] +> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. - -## 8. Contributing a New Service - -To add a new service to Coolify, please refer to our documentation: -[Adding a New Service](https://coolify.io/docs/knowledge-base/add-a-service) - - -## 9. Create a Pull Request +## 8. Create a Pull Request 1. After making changes or adding a new service: - Commit your changes to your forked repository. @@ -179,11 +182,26 @@ To add a new service to Coolify, please refer to our documentation: - In the description, explain the changes you've made. - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". -4. Important note: - Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. +> [!IMPORTANT] +> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. -5. Submit your PR: +4. Submit your PR: - Review your changes one last time. - Click "Create pull request" to submit. +> [!NOTE] +> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers. + After submission, maintainers will review your PR and may request changes or provide feedback. + +## Additional Contribution Guidelines + +### Contributing a New Service + +To add a new service to Coolify, please refer to our documentation: +[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service) + +### Contributing to Documentation + +To contribute to the Coolify documentation, please refer to this guide: +[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) diff --git a/README.md b/README.md index 05f2e2b3c..14a741088 100644 --- a/README.md +++ b/README.md @@ -39,27 +39,27 @@ Special thanks to our biggest sponsors! ![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1) -* [CCCareers](https://cccareers.org/) - A career development platform for coding bootcamp graduates. -* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering dedicated servers and cloud services. -* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization platform. -* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions. -* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) for fast content delivery. -* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform for data analytics and visualization. -* [SupaGuide](https://supa.guide/?ref=coolify.io) - A platform offering guides and resources for web development and design. -* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A data integration platform for connecting and managing data sources. -* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure for secure data exchange. -* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising. -* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered data analytics platform for business insights. -* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-based platform for automating DevOps and infrastructure management. -* [Latitude](https://latitude.sh/?ref=coolify.io) - A platform offering location-based services and geospatial data. -* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in brand identity and digital presence. -* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform specializing in remote and flexible work opportunities. -* [Hostinger](https://hostinger.com?ref=coolify.io) - A web hosting company offering shared, VPS, and cloud hosting services. -* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps and cloud consulting company offering infrastructure automation services. -* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - A cloud-based platform for IoT device management and data analytics. -* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and marketing services. -* [Saasykit](https://saasykit.com/?ref=coolify.io) - SaaSykit is a Laravel-based boilerplate with everything you need to build an awesome SaaS. -* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud-based platform for data storage and processing. +* [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. +* [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. +* [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. +* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions. +* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies. +* [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. +* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses. +* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. +* [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. ## Github Sponsors ($40+) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..2cb96b72b --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,45 @@ +# Coolify Release Guide + +This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed. + +## Release Process + +1. **Development on `next` or separate branches** + - Changes, fixes and new features are developed on the `next` or even separate branches. + +2. **Merging to `main`** + - Once changes are ready, they are merged from `next` into the `main` branch. + +3. **Building the release** + - After merging to `main`, a new release is built. + - Note: A push to `main` does not automatically mean a new version is released. + +4. **Creating a GitHub release** + - A new release is created on GitHub with the new version details. + +5. **Updating the CDN** + - The final step is updating the version information on the CDN: + [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json) + +> [!NOTE] +> The CDN update may not occur immediately after the GitHub release. It can happen hours or even days later due to additional testing, stability checks, or potential hotfixes. + + +## Version Availability + +It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update). + +> [!IMPORTANT] +> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released. + +## Manually Update to Specific Versions + +> [!CAUTION] +> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk! + +To update your Coolify instance to a specific (unreleased) version, use the following command: + +```bash +curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s +``` +-> Replace `` with the version you want to update to (for example `4.0.0-beta.332`). diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index a24ac6b29..1034c13d6 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -21,10 +22,16 @@ class CleanupDocker private function getCommands(): array { + $settings = InstanceSettings::get(); + $helperImageVersion = data_get($settings, 'helper_version'); + $helperImage = config('coolify.helper_image'); + $helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion; + $commonCommands = [ 'docker container prune -f --filter "label=coolify.managed=true"', - 'docker image prune -af', + '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", ]; return $commonCommands; diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 09d6471e8..c4af6bb21 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -4,8 +4,6 @@ namespace App\Actions\Server; use App\Models\InstanceSettings; use App\Models\Server; -use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Http; use Lorisleiva\Actions\Concerns\AsAction; class UpdateCoolify @@ -27,11 +25,6 @@ class UpdateCoolify return; } CleanupDocker::dispatch($this->server)->onQueue('high'); - $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('version'); if (! $manual_update) { @@ -62,10 +55,11 @@ class UpdateCoolify return; } + instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false); + remote_process([ 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', "bash /data/coolify/source/upgrade.sh $this->latestVersion", ], $this->server); - } } diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 4b6a25dcc..7aef457a1 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -16,8 +16,10 @@ class StartService $service->saveComposeConfigs(); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; - $commands[] = "echo 'Creating Docker network.'"; - $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; + if($service->networks()->count() > 0){ + $commands[] = "echo 'Creating Docker network.'"; + $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; + } $commands[] = 'echo Starting service.'; $commands[] = "echo 'Pulling images.'"; $commands[] = 'docker compose pull'; diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 0a544d2ff..68beb448a 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Service; use App\Models\ServiceApplication; @@ -165,6 +166,18 @@ class CleanupStuckedResources extends Command echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n"; } + try { + $scheduled_backups = ScheduledDatabaseBackup::all(); + foreach ($scheduled_backups as $scheduled_backup) { + if (! $scheduled_backup->server()) { + echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n"; + $scheduled_backup->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n"; + } + // Cleanup any resources that are not attached to any environment or destination or server try { $applications = Application::all(); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c2c787be4..b960a4a8b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,9 +4,9 @@ namespace App\Console; use App\Jobs\CheckForUpdatesJob; use App\Jobs\CleanupInstanceStuffsJob; +use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; -use App\Jobs\PullCoolifyImageJob; use App\Jobs\PullHelperImageJob; use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; @@ -30,7 +30,8 @@ class Kernel extends ConsoleKernel $this->all_servers = Server::all(); $settings = InstanceSettings::get(); - $schedule->command('telescope:prune')->daily(); + $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); + if (isDev()) { // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); @@ -40,11 +41,12 @@ class Kernel extends ConsoleKernel $this->check_resources($schedule); $this->check_scheduled_tasks($schedule); $schedule->command('uploads:clear')->everyTwoMinutes(); + + $schedule->command('telescope:prune')->daily(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); - $schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $this->schedule_updates($schedule); @@ -139,6 +141,10 @@ class Kernel extends ConsoleKernel } $server = $scheduled_backup->server(); + + if (! $server) { + continue; + } $serverTimezone = $server->settings->server_timezone; if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { @@ -181,6 +187,9 @@ class Kernel extends ConsoleKernel } $server = $scheduled_task->server(); + if (! $server) { + continue; + } $serverTimezone = $server->settings->server_timezone ?: config('app.timezone'); if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 96f98d844..d1c8f5ea6 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -86,7 +86,7 @@ class DeployController extends Controller ], tags: ['Deployments'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -150,7 +150,7 @@ class DeployController extends Controller responses: [ new OA\Response( response: 200, - description: 'Get deployment(s) Uuid\'s', + description: 'Get deployment(s) UUID\'s', content: [ new OA\MediaType( mediaType: 'application/json', diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 4d7ef3920..f1958de2c 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -11,7 +11,7 @@ class ProjectController extends Controller { #[OA\Get( summary: 'List', - description: 'list projects.', + description: 'List projects.', path: '/projects', operationId: 'list-projects', security: [ @@ -47,7 +47,7 @@ class ProjectController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } - $projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get(); + $projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get(); return response()->json(serializeApiResponse($projects), ); @@ -55,7 +55,7 @@ class ProjectController extends Controller #[OA\Get( summary: 'Get', - description: 'Get project by Uuid.', + description: 'Get project by UUID.', path: '/projects/{uuid}', operationId: 'get-project-by-uuid', security: [ @@ -139,12 +139,15 @@ class ProjectController extends Controller return invalidTokenResponse(); } if (! $request->uuid) { - return response()->json(['message' => 'Uuid is required.'], 422); + return response()->json(['message' => 'UUID is required.'], 422); } if (! $request->environment_name) { return response()->json(['message' => 'Environment name is required.'], 422); } $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } $environment = $project->environments()->whereName($request->environment_name)->first(); if (! $environment) { return response()->json(['message' => 'Environment not found.'], 404); @@ -171,7 +174,7 @@ class ProjectController extends Controller schema: new OA\Schema( type: 'object', properties: [ - 'uuid' => ['type' => 'string', 'description' => 'The name of the project.'], + 'name' => ['type' => 'string', 'description' => 'The name of the project.'], 'description' => ['type' => 'string', 'description' => 'The description of the project.'], ], ), @@ -338,7 +341,7 @@ class ProjectController extends Controller } $uuid = $request->uuid; if (! $uuid) { - return response()->json(['message' => 'Uuid is required.'], 422); + return response()->json(['message' => 'UUID is required.'], 422); } $project = Project::whereTeamId($teamId)->whereUuid($uuid)->first(); @@ -414,7 +417,7 @@ class ProjectController extends Controller } if (! $request->uuid) { - return response()->json(['message' => 'Uuid is required.'], 422); + return response()->json(['message' => 'UUID is required.'], 422); } $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); if (! $project) { diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index 3a489f647..bb474aed3 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -75,7 +75,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -323,7 +323,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index e2bde52f7..5f0d6bb12 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -107,7 +107,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -185,7 +185,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -263,7 +263,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index c39778698..0a6154410 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -378,7 +378,7 @@ class ServicesController extends Controller responses: [ new OA\Response( response: 200, - description: 'Get a service by Uuid.', + description: 'Get a service by UUID.', content: [ new OA\MediaType( mediaType: 'application/json', @@ -436,7 +436,7 @@ class ServicesController extends Controller responses: [ new OA\Response( response: 200, - description: 'Delete a service by Uuid', + description: 'Delete a service by UUID', content: [ new OA\MediaType( mediaType: 'application/json', @@ -483,6 +483,516 @@ class ServicesController extends Controller ]); } + #[OA\Get( + summary: 'List Envs', + description: 'List all envs by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'list-envs-by-service-uuid', + security: [ + ['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', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'All environment variables by service UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $envs = $service->environment_variables->map(function ($env) { + $env->makeHidden([ + 'application_id', + 'standalone_clickhouse_id', + 'standalone_dragonfly_id', + 'standalone_keydb_id', + 'standalone_mariadb_id', + 'standalone_mongodb_id', + 'standalone_mysql_id', + 'standalone_postgresql_id', + 'standalone_redis_id', + ]); + $env = $this->removeSensitiveData($env); + + return $env; + }); + + return response()->json($envs); + } + + #[OA\Patch( + summary: 'Update Env', + description: 'Update env by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'update-env-by-service-uuid', + security: [ + ['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: 'Env updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['key', 'value'], + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $env = $service->environment_variables()->where('key', $request->key)->first(); + if (! $env) { + return response()->json(['message' => 'Environment variable not found.'], 404); + } + + $env->fill($request->all()); + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update Envs (Bulk)', + description: 'Update multiple envs by service UUID.', + path: '/services/{uuid}/envs/bulk', + operationId: 'update-envs-by-service-uuid', + security: [ + ['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: 'Bulk envs updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['data'], + properties: [ + 'data' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variables updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variables updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_bulk_envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $bulk_data = $request->get('data'); + if (! $bulk_data) { + return response()->json(['message' => 'Bulk data is required.'], 400); + } + + $updatedEnvs = collect(); + foreach ($bulk_data as $item) { + $validator = customApiValidator($item, [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $env = $service->environment_variables()->updateOrCreate( + ['key' => $item['key']], + $item + ); + + $updatedEnvs->push($this->removeSensitiveData($env)); + } + + return response()->json($updatedEnvs)->setStatusCode(201); + } + + #[OA\Post( + summary: 'Create Env', + description: 'Create env by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'create-env-by-service-uuid', + security: [ + ['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( + required: true, + description: 'Env created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_env(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $existingEnv = $service->environment_variables()->where('key', $request->key)->first(); + if ($existingEnv) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } + + $env = $service->environment_variables()->create($request->all()); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete Env', + description: 'Delete env by UUID.', + path: '/services/{uuid}/envs/{env_uuid}', + operationId: 'delete-env-by-service-uuid', + security: [ + ['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', + ) + ), + new OA\Parameter( + name: 'env_uuid', + in: 'path', + description: 'UUID of the environment variable.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Environment variable deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $env = EnvironmentVariable::where('uuid', $request->env_uuid) + ->where('service_id', $service->id) + ->first(); + + if (! $env) { + return response()->json(['message' => 'Environment variable not found.'], 404); + } + + $env->forceDelete(); + + return response()->json(['message' => 'Environment variable deleted.']); + } + #[OA\Get( summary: 'Start', description: 'Start service. `Post` request is also accepted.', diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 508164afe..718cea639 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -12,6 +12,7 @@ use App\Models\ApplicationPreview; use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; +use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -1065,15 +1066,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->environment_variables = $envs; } + private function elixir_finetunes() + { + if ($this->pull_request_id === 0) { + $envType = 'environment_variables'; + } else { + $envType = 'environment_variables_preview'; + } + $mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first(); + if ($mix_env) { + if ($mix_env->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + $secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first(); + if ($secret_key_base) { + if ($secret_key_base->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + $database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first(); + if ($database_url) { + if ($database_url->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } + private function laravel_finetunes() { if ($this->pull_request_id === 0) { - $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); - $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + $envType = 'environment_variables'; } else { - $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); - $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + $envType = 'environment_variables_preview'; } + $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); + $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + if (! $nixpacks_php_fallback_path) { $nixpacks_php_fallback_path = new EnvironmentVariable; $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; @@ -1293,7 +1334,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function prepare_builder_image() { + $settings = InstanceSettings::get(); $helperImage = config('coolify.helper_image'); + $helperImage = "{$helperImage}:{$settings->helper_version}"; // Get user home directory $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); @@ -1530,6 +1573,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); } + if ($this->nixpacks_type === 'elixir') { + $this->elixir_finetunes(); + } $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); if ($this->nixpacks_type === 'rust') { diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php deleted file mode 100644 index 54c062197..000000000 --- a/app/Jobs/ApplicationRestartJob.php +++ /dev/null @@ -1,32 +0,0 @@ -applicationDeploymentQueueId = $applicationDeploymentQueueId; - } - - public function handle() - { - ray('Restarting application'); - } -} diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index 86b66fbfb..ddc264839 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\File; class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue { @@ -25,12 +26,14 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); + $latest_version = data_get($versions, 'coolify.v4.version'); $current_version = config('version'); if (version_compare($latest_version, $current_version, '>')) { // New version available $settings->update(['new_version_available' => true]); + File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); } else { $settings->update(['new_version_available' => false]); } diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php deleted file mode 100644 index 16ef85192..000000000 --- a/app/Jobs/CheckLogDrainContainerJob.php +++ /dev/null @@ -1,93 +0,0 @@ -server->id))->dontRelease()]; - } - - public function uniqueId(): int - { - return $this->server->id; - } - - public function healthcheck() - { - $status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false); - if (str($status)->contains('running')) { - return true; - } else { - return false; - } - } - - public function handle() - { - // ray("checking log drain statuses for {$this->server->id}"); - try { - if (! $this->server->isFunctional()) { - return; - } - $containers = instant_remote_process(['docker container ls -q'], $this->server, false); - if (! $containers) { - return; - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server); - $containers = format_docker_command_output_to_json($containers); - - $foundLogDrainContainer = $containers->filter(function ($value, $key) { - return data_get($value, 'Name') === '/coolify-log-drain'; - })->first(); - if (! $foundLogDrainContainer || ! $this->healthcheck()) { - ray('Log drain container not found or unhealthy. Restarting...'); - InstallLogDrain::run($this->server); - Sleep::for(10)->seconds(); - if ($this->healthcheck()) { - if ($this->server->log_drain_notification_sent) { - $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server)); - $this->server->update(['log_drain_notification_sent' => false]); - } - - return; - } - if (! $this->server->log_drain_notification_sent) { - ray('Log drain container still unhealthy. Sending notification...'); - // $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null)); - $this->server->update(['log_drain_notification_sent' => true]); - } - } else { - if ($this->server->log_drain_notification_sent) { - $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server)); - $this->server->update(['log_drain_notification_sent' => false]); - } - } - } catch (\Throwable $e) { - if (! isCloud()) { - send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage()); - } - ray($e->getMessage()); - - return handleError($e); - } - } -} diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php new file mode 100644 index 000000000..bcca77c18 --- /dev/null +++ b/app/Jobs/CleanupStaleMultiplexedConnections.php @@ -0,0 +1,37 @@ +cleanupStaleConnection($server); + } + }); + } + + private function cleanupStaleConnection(Server $server) + { + $muxSocket = "/tmp/mux_{$server->id}"; + $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null"; + $checkProcess = Process::run($checkCommand); + + if ($checkProcess->exitCode() !== 0) { + $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null"; + Process::run($closeCommand); + } + } +} diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 0666f78c3..e6fa05b55 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -25,6 +25,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; +use Visus\Cuid2\Cuid2; class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { @@ -398,6 +399,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; $commands[] = $backupCommand; + ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { @@ -479,6 +481,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue private function upload_to_s3(): void { try { + ray($this->backup_location); if (is_null($this->s3)) { return; } @@ -488,23 +491,20 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { - $network = $this->database->service->destination->network; - } else { - $network = $this->database->destination->network; - } - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; - $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; - $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; + $configName = new Cuid2; + + $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/'); + $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'"; + $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'"; instant_remote_process($commands, $this->server); $this->add_to_backup_output('Uploaded to S3.'); - ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); throw $e; } finally { - $command = "docker rm -f backup-of-{$this->backup->uuid}"; - instant_remote_process([$command], $this->server); + $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'"; + $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'"; + instant_remote_process($removeConfigCommands, $this->server, false); } } } diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php deleted file mode 100644 index 1bbfcf8cb..000000000 --- a/app/Jobs/InstanceAutoUpdateJob.php +++ /dev/null @@ -1,28 +0,0 @@ -get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } - $latest_version = get_latest_version_of_coolify(); - instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false); - - $current_version = config('version'); - if (! $settings->is_auto_update_enabled) { - return; - } - if ($latest_version === $current_version) { - return; - } - if (version_compare($latest_version, $current_version, '<')) { - return; - } - } catch (\Throwable $e) { - throw $e; - } - } -} diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 30a1b8026..420119069 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -10,6 +11,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue { @@ -32,10 +34,20 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue public function handle(): void { try { - $helperImage = config('coolify.helper_image'); - ray("Pulling {$helperImage}"); - instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false); - ray('PullHelperImageJob done'); + $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $settings = InstanceSettings::get(); + $latest_version = data_get($versions, 'coolify.helper.version'); + $current_version = $settings->helper_version; + if (version_compare($latest_version, $current_version, '>')) { + // New version available + $helperImage = config('coolify.helper_image'); + instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); + $settings->update(['helper_version' => $latest_version]); + } + } + } catch (\Throwable $e) { send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 703b199c5..540085385 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -26,6 +26,8 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public $tries = 3; + public $timeout = 60; + public $containers; public $applications; @@ -43,15 +45,15 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public function __construct(public Server $server) {} - // public function middleware(): array - // { - // return [(new WithoutOverlapping($this->server->uuid))]; - // } + public function middleware(): array + { + return [(new WithoutOverlapping($this->server->id))]; + } - // public function uniqueId(): int - // { - // return $this->server->uuid; - // } + public function uniqueId(): int + { + return $this->server->id; + } public function handle() { @@ -124,6 +126,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue private function checkLogDrainContainer() { + if (! $this->server->isLogDrainEnabled()) { + return; + } $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { return data_get($value, 'Name') === '/coolify-log-drain'; })->first(); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 147a1ad6f..af05ad767 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -73,6 +73,8 @@ class Index extends Component } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); + $this->remoteServerPort = $this->remoteServerPort; + $this->remoteServerUser = $this->remoteServerUser; if (isDev()) { $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); if ($this->servers->count() > 0) { $this->selectedExistingServer = $this->servers->first()->id; + $this->updateServerDetails(); $this->currentState = 'select-existing-server'; return; @@ -173,9 +176,18 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== } $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); + $this->updateServerDetails(); $this->currentState = 'validate-server'; } + private function updateServerDetails() + { + if ($this->createdServer) { + $this->remoteServerPort = $this->createdServer->port; + $this->remoteServerUser = $this->createdServer->user; + } + } + public function getProxyType() { // Set Default Proxy Type @@ -235,11 +247,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function saveServer() { $this->validate([ - 'remoteServerName' => 'required', - 'remoteServerHost' => 'required', + 'remoteServerName' => 'required|string', + 'remoteServerHost' => 'required|string', 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => 'required', + 'remoteServerUser' => 'required|string', ]); + $this->privateKey = formatPrivateKey($this->privateKey); $foundServer = Server::whereIp($this->remoteServerHost)->first(); if ($foundServer) { @@ -269,7 +282,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function validateServer() { try { - config()->set('coolify.mux_enabled', false); + config()->set('constants.ssh.mux_enabled', false); // EC2 does not have `uptime` command, lol instant_remote_process(['ls /'], $this->createdServer, true); @@ -277,9 +290,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->createdServer->settings()->update([ 'is_reachable' => true, ]); + $this->serverReachable = true; } catch (\Throwable $e) { $this->serverReachable = false; - $this->createdServer->delete(); + $this->createdServer->settings()->update([ + 'is_reachable' => false, + ]); return handleError(error: $e, livewire: $this); } @@ -296,6 +312,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ]); $this->getProxyType(); } catch (\Throwable $e) { + $this->createdServer->settings()->update([ + 'is_usable' => false, + ]); + return handleError(error: $e, livewire: $this); } } @@ -349,6 +369,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ); } + public function saveAndValidateServer() + { + $this->validate([ + 'remoteServerPort' => 'required|integer|min:1|max:65535', + 'remoteServerUser' => 'required|string', + ]); + + $this->createdServer->update([ + 'port' => $this->remoteServerPort, + 'user' => $this->remoteServerUser, + 'timezone' => 'UTC', + ]); + $this->validateServer(); + } + private function createNewPrivateKey() { $this->privateKeyName = generate_random_name(); diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 84a24255c..f2968f6d9 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Application\Deployment; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; +use Illuminate\Support\Collection; use Livewire\Component; class Show extends Component @@ -69,6 +70,20 @@ class Show extends Component } } + public function getLogLinesProperty() + { + return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) { + $logLine['line'] = e($logLine['line']); + $logLine['line'] = preg_replace( + '/(https?:\/\/[^\s]+)/', + '$1', + $logLine['line'], + ); + + return $logLine; + }); + } + public function render() { return view('livewire.project.application.deployment.show'); diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 8a4efb21d..3b3747a81 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -24,7 +24,11 @@ class Form extends Component public $timezones; - protected $listeners = ['serverInstalled', 'revalidate' => '$refresh']; + protected $listeners = [ + 'serverInstalled', + 'refreshServerShow' => 'serverInstalled', + 'revalidate' => '$refresh', + ]; protected $rules = [ 'server.name' => 'required', diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 5f69835d7..f80152435 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -2,10 +2,10 @@ namespace App\Livewire\Server\New; -use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; use App\Models\Server; use App\Models\Team; +use Illuminate\Support\Collection; use Livewire\Component; class ByIp extends Component @@ -40,7 +40,7 @@ class ByIp extends Component public bool $is_build_server = false; - public $swarm_managers = []; + public Collection $swarm_managers; protected $rules = [ 'name' => 'required|string', @@ -102,11 +102,6 @@ class ByIp extends Component 'port' => $this->port, 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, - 'proxy' => [ - // set default proxy type to traefik v2 - 'type' => ProxyTypes::TRAEFIK->value, - 'status' => ProxyStatus::EXITED->value, - ], ]; if ($this->is_swarm_worker) { $payload['swarm_cluster'] = $this->selected_swarm_cluster; @@ -115,6 +110,9 @@ class ByIp extends Component data_forget($payload, 'proxy'); } $server = Server::create($payload); + $server->proxy->set('status', 'exited'); + $server->proxy->set('type', ProxyTypes::TRAEFIK->value); + $server->save(); if ($this->is_build_server) { $this->is_swarm_manager = false; $this->is_swarm_worker = false; diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 0751b186e..a5e94a19a 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -14,7 +14,7 @@ class Show extends Component public $parameters = []; - protected $listeners = ['refreshServerShow' => '$refresh']; + protected $listeners = ['refreshServerShow']; public function mount() { @@ -29,6 +29,12 @@ class Show extends Component } } + public function refreshServerShow() + { + $this->server->refresh(); + $this->dispatch('$refresh'); + } + public function submit() { $this->dispatch('serverRefresh', false); diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index da7b5860d..dfbd945f5 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -4,7 +4,6 @@ namespace App\Livewire; use App\Actions\Server\UpdateCoolify; use App\Models\InstanceSettings; -use Illuminate\Support\Facades\Http; use Livewire\Component; class Upgrade extends Component @@ -22,13 +21,8 @@ class Upgrade extends Component public function checkUpdate() { try { - $settings = InstanceSettings::get(); - $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - $this->latestVersion = data_get($versions, 'coolify.v4.version'); - } - $this->isUpgradeAvailable = $settings->new_version_available; + $this->latestVersion = get_latest_version_of_coolify(); + $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Models/Project.php b/app/Models/Project.php index 77f62d770..18481751c 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -11,6 +11,7 @@ use OpenApi\Attributes as OA; 'id' => ['type' => 'integer'], 'uuid' => ['type' => 'string'], 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], 'environments' => new OA\Property( property: 'environments', type: 'array', diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 2d0e200da..50a0c8173 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -22,7 +22,8 @@ class ScheduledDatabaseBackup extends BaseModel public function executions(): HasMany { - return $this->hasMany(ScheduledDatabaseBackupExecution::class); + // Last execution first + return $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc'); } public function s3() diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 7b1c0d275..82f0036a5 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -28,6 +28,7 @@ class ScheduledTask extends BaseModel public function executions(): HasMany { + // Last execution first return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc'); } diff --git a/app/Models/Server.php b/app/Models/Server.php index 3a1c0eabe..65d70083f 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -112,6 +112,16 @@ class Server extends BaseModel 'proxy', ]; + protected $fillable = [ + 'name', + 'ip', + 'port', + 'user', + 'description', + 'private_key_id', + 'team_id', + ]; + protected $guarded = []; public static function isReachable() @@ -880,7 +890,7 @@ $schema://$host { public function muxFilename() { - return "{$this->ip}_{$this->port}_{$this->user}"; + return $this->uuid; } public function team() @@ -957,7 +967,7 @@ $schema://$host { public function validateConnection() { - config()->set('coolify.mux_enabled', false); + config()->set('constants.ssh.mux_enabled', false); $server = Server::find($this->id); if (! $server) { diff --git a/app/Models/Service.php b/app/Models/Service.php index a16220604..d8def6663 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -667,7 +667,7 @@ class Service extends BaseModel } $data = $data->merge([ 'Root User' => [ - 'key' => 'N/A', + 'key' => 'GITLAB_ROOT_USER', 'value' => 'root', 'rules' => 'required', 'isPassword' => true, @@ -1016,10 +1016,20 @@ class Service extends BaseModel $commands[] = 'rm -f .env || true'; $envs_from_coolify = $this->environment_variables()->get(); - foreach ($envs_from_coolify as $env) { + $sorted = $envs_from_coolify->sortBy(function ($env) { + if (str($env->key)->startsWith('SERVICE_')) { + return 1; + } + if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->startsWith('${SERVICE_')) { + return 2; + } + + return 3; + }); + foreach ($sorted as $env) { $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; } - if ($envs_from_coolify->count() === 0) { + if ($sorted->count() === 0) { $commands[] = 'touch .env'; } instant_remote_process($commands, $this->server); diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 607cacade..7ecb00348 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -209,7 +209,7 @@ class StandaloneKeydb extends BaseModel protected function internalDbUrl(): Attribute { return new Attribute( - get: fn () => "redis://{$this->keydb_password}@{$this->uuid}:6379/0", + get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0", ); } @@ -218,7 +218,7 @@ class StandaloneKeydb extends BaseModel return new Attribute( get: function () { if ($this->is_public && $this->public_port) { - return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; } return null; diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index f94c9bc20..1eeec8f94 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -46,6 +46,7 @@ const SUPPORTED_OS = [ 'centos fedora rhel ol rocky amzn almalinux', 'sles opensuse-leap opensuse-tumbleweed', 'arch', + 'alpine', ]; const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment']; diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 6ba7caeef..4ba378e67 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -95,8 +95,24 @@ function generateScpCommand(Server $server, string $source, string $dest) $timeout = config('constants.ssh.command_timeout'); $connectionTimeout = config('constants.ssh.connection_timeout'); $serverInterval = config('constants.ssh.server_interval'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); $scp_command = "timeout $timeout scp "; + $muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false; + // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); + + if ($muxEnabled) { + $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; + $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + ensureMultiplexedConnection($server); + // ray('Using SSH Multiplexing')->green(); + } else { + // ray('Not using SSH Multiplexing')->red(); + } + + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; + } $scp_command .= "-i {$privateKeyLocation} " .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' .'-o PasswordAuthentication=no ' @@ -145,9 +161,18 @@ function generateSshCommand(Server $server, string $command) $ssh_command = "timeout $timeout ssh "; - if (config('coolify.mux_enabled') && config('coolify.is_windows_docker_desktop') == false) { - $ssh_command .= "-o ControlMaster=auto -o ControlPersist={$muxPersistTime} -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r "; + $muxEnabled = config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false; + // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue(); + if ($muxEnabled) { + // Always use multiplexing when enabled + $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; + $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + ensureMultiplexedConnection($server); + // ray('Using SSH Multiplexing')->green(); + } else { + // ray('Not using SSH Multiplexing')->red(); } + if (data_get($server, 'settings.is_cloudflare_tunnel')) { $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; } @@ -167,11 +192,120 @@ function generateSshCommand(Server $server, string $command) .$command.PHP_EOL .$delimiter; - // ray($ssh_command); return $ssh_command; } + +function ensureMultiplexedConnection(Server $server) +{ + if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) { + return; + } + + static $ensuredConnections = []; + + if (isset($ensuredConnections[$server->id])) { + if (! shouldResetMultiplexedConnection($server)) { + // ray('Using Existing Multiplexed Connection')->green(); + + return; + } + } + + $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}"; + $checkCommand = "ssh -O check -o ControlPath=$muxSocket "; + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $checkCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; + } + $checkCommand .= " {$server->user}@{$server->ip}"; + + $process = Process::run($checkCommand); + + if ($process->exitCode() === 0) { + // ray('Existing Multiplexed Connection is Valid')->green(); + $ensuredConnections[$server->id] = [ + 'timestamp' => now(), + 'muxSocket' => $muxSocket, + ]; + + return; + } + + // ray('Establishing New Multiplexed Connection')->orange(); + + $privateKeyLocation = savePrivateKeyToFs($server); + $connectionTimeout = config('constants.ssh.connection_timeout'); + $serverInterval = config('constants.ssh.server_interval'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); + + $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $establishCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; + } + $establishCommand .= "-i {$privateKeyLocation} " + .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' + .'-o PasswordAuthentication=no ' + ."-o ConnectTimeout=$connectionTimeout " + ."-o ServerAliveInterval=$serverInterval " + .'-o RequestTTY=no ' + .'-o LogLevel=ERROR ' + ."-p {$server->port} " + ."{$server->user}@{$server->ip}"; + + $establishProcess = Process::run($establishCommand); + + if ($establishProcess->exitCode() !== 0) { + throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput()); + } + + $ensuredConnections[$server->id] = [ + 'timestamp' => now(), + 'muxSocket' => $muxSocket, + ]; + + // ray('Established New Multiplexed Connection')->green(); +} + +function shouldResetMultiplexedConnection(Server $server) +{ + if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) { + return false; + } + + static $ensuredConnections = []; + + if (! isset($ensuredConnections[$server->id])) { + return true; + } + + $lastEnsured = $ensuredConnections[$server->id]['timestamp']; + $muxPersistTime = config('constants.ssh.mux_persist_time'); + $resetInterval = strtotime($muxPersistTime) - time(); + + return $lastEnsured->addSeconds($resetInterval)->isPast(); +} + +function resetMultiplexedConnection(Server $server) +{ + if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) { + return; + } + + static $ensuredConnections = []; + + if (isset($ensuredConnections[$server->id])) { + $muxSocket = $ensuredConnections[$server->id]['muxSocket']; + $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; + Process::run($closeCommand); + unset($ensuredConnections[$server->id]); + } +} + function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string { + static $processCount = 0; + $processCount++; + $timeout = config('constants.ssh.command_timeout'); if ($command instanceof Collection) { $command = $command->toArray(); @@ -180,10 +314,18 @@ function instant_remote_process(Collection|array $command, Server $server, bool $command = parseCommandsByLineForSudo(collect($command), $server); } $command_string = implode("\n", $command); - $ssh_command = generateSshCommand($server, $command_string, $no_sudo); - $process = Process::timeout($timeout)->run($ssh_command); + + $start_time = microtime(true); + $sshCommand = generateSshCommand($server, $command_string); + $process = Process::timeout($timeout)->run($sshCommand); + $end_time = microtime(true); + + $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds + // ray('SSH command execution time:', $execution_time.' ms')->orange(); + $output = trim($process->output()); $exitCode = $process->exitCode(); + if ($exitCode !== 0) { if (! $throwError) { return null; @@ -223,7 +365,6 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d if (is_null($application_deployment_queue)) { return collect([]); } - // ray(data_get($application_deployment_queue, 'logs')); try { $decoded = json_decode( data_get($application_deployment_queue, 'logs'), @@ -233,7 +374,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } catch (\JsonException $exception) { return collect([]); } - // ray($decoded ); + $seenCommands = collect(); $formatted = collect($decoded); if (! $is_debug_enabled) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); @@ -244,7 +385,42 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u')); return $i; - }); + }) + ->reduce(function ($deploymentLogLines, $logItem) use ($seenCommands) { + $command = data_get($logItem, 'command'); + $isStderr = data_get($logItem, 'type') === 'stderr'; + $isNewCommand = ! is_null($command) && ! $seenCommands->first(function ($seenCommand) use ($logItem) { + return data_get($seenCommand, 'command') === data_get($logItem, 'command') && data_get($seenCommand, 'batch') === data_get($logItem, 'batch'); + }); + + if ($isNewCommand) { + $deploymentLogLines->push([ + 'line' => $command, + 'timestamp' => data_get($logItem, 'timestamp'), + 'stderr' => $isStderr, + 'hidden' => data_get($logItem, 'hidden'), + 'command' => true, + ]); + + $seenCommands->push([ + 'command' => $command, + 'batch' => data_get($logItem, 'batch'), + ]); + } + + $lines = explode(PHP_EOL, data_get($logItem, 'output')); + + foreach ($lines as $line) { + $deploymentLogLines->push([ + 'line' => $line, + 'timestamp' => data_get($logItem, 'timestamp'), + 'stderr' => $isStderr, + 'hidden' => data_get($logItem, 'hidden'), + ]); + } + + return $deploymentLogLines; + }, collect()); return $formatted; } @@ -258,6 +434,10 @@ function remove_mux_and_private_key(Server $server) { $muxFilename = $server->muxFilename(); $privateKeyLocation = savePrivateKeyToFs($server); + + $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}"; + Process::run($closeCommand); + Storage::disk('ssh-mux')->delete($muxFilename); Storage::disk('ssh-keys')->delete($privateKeyLocation); } @@ -267,7 +447,10 @@ function refresh_server_connection(?PrivateKey $private_key = null) return; } foreach ($private_key->servers as $server) { - Storage::disk('ssh-mux')->delete($server->muxFilename()); + $muxFilename = $server->muxFilename(); + $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}"; + Process::run($closeCommand); + Storage::disk('ssh-mux')->delete($muxFilename); } } @@ -277,24 +460,17 @@ function checkRequiredCommands(Server $server) foreach ($commands as $command) { $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); if ($commandFound) { - ray($command.' found'); - continue; } try { instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); } catch (\Throwable $e) { - ray('could not install '.$command); - ray($e); break; } $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); if ($commandFound) { - ray($command.' found'); - continue; } - ray('could not install '.$command); break; } } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 78086a131..eba88d000 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -4,6 +4,7 @@ use App\Models\Application; use App\Models\EnvironmentVariable; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use Illuminate\Support\Stringable; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; @@ -15,9 +16,9 @@ function collectRegex(string $name) { return "/{$name}\w+/"; } -function replaceVariables($variable) +function replaceVariables(string $variable): Stringable { - return $variable->before('}')->replaceFirst('$', '')->replaceFirst('{', ''); + return str($variable)->before('}')->replaceFirst('$', '')->replaceFirst('{', ''); } function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cc5c13407..028d20f33 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1866,7 +1866,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'service_id' => $resource->id, ])->first(); - $value = str(replaceVariables($value)); + $value = replaceVariables($value); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -2627,7 +2627,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, 'is_preview' => false, ])->first(); - $value = str(replaceVariables($value)); + $value = replaceVariables($value); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -2864,6 +2864,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return collect($finalServices); } } + function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection { $isApplication = $resource instanceof Application; @@ -2920,6 +2921,182 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } $parsedServices = collect([]); + ray()->clearAll(); + + $allMagicEnvironments = collect([]); + foreach ($services as $serviceName => $service) { + $magicEnvironments = collect([]); + $image = data_get_str($service, 'image'); + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); + + if ($isService) { + if ($isDatabase) { + $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 = convertComposeEnvironmentToArray($environment); + + // Add Coolify defined environments + $allEnvironments = $resource->environment_variables()->get(['key', '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; + } + $magicEnvironments->put($match->value(), ''); + } + } + } + + // Get magic environments where we need to preset the FQDN + if ($key->startsWith('SERVICE_FQDN_')) { + // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + 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' && $value->startsWith('/')) { + $path = $value->value(); + if ($path !== '/') { + $fqdn = "$fqdn$path"; + } + } + if ($isApplication && is_null($resource->fqdn)) { + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->fqdn = $fqdn; + $resource->save(); + } elseif ($isService && is_null($savedService->fqdn)) { + $savedService->fqdn = $fqdn; + $savedService->save(); + } + + if (substr_count(str($key)->value(), '_') === 2) { + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $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()->where('key', $newKey->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $newKey->value(), + $nameOfId => $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 = $key->after('SERVICE_')->before('_'); + $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $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()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $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"); + } + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + } else { + $value = generateEnvValue($command, $resource); + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + } + } + + // Parse the rest of the services foreach ($services as $serviceName => $service) { $image = data_get_str($service, 'image'); $restart = data_get_str($service, 'restart', RESTART_MODE); @@ -2932,12 +3109,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } $volumes = collect(data_get($service, 'volumes', [])); $networks = collect(data_get($service, 'networks', [])); + $use_network_mode = data_get($service, 'network_mode') !== null; $depends_on = collect(data_get($service, 'depends_on', [])); $labels = collect(data_get($service, 'labels', [])); $environment = collect(data_get($service, 'environment', [])); $ports = collect(data_get($service, 'ports', [])); $buildArgs = collect(data_get($service, 'build.args', [])); $environment = $environment->merge($buildArgs); + + $environment = convertComposeEnvironmentToArray($environment); + $coolifyEnvironments = collect([]); + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); $volumesParsed = collect([]); @@ -3069,10 +3251,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($topLevel->get('volumes')->has($source->value())) { $temp = $topLevel->get('volumes')->get($source->value()); if (data_get($temp, 'driver_opts.type') === 'cifs') { - return $volume; + continue; } if (data_get($temp, 'driver_opts.type') === 'nfs') { - return $volume; + continue; } } $slugWithoutUuid = Str::slug($source, '-'); @@ -3127,32 +3309,34 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $depends_on = $newDependsOn; } } - if ($topLevel->get('networks')?->count() > 0) { - foreach ($topLevel->get('networks') as $networkName => $network) { - if ($networkName === 'default') { - continue; - } - // ignore aliases - if ($network['aliases'] ?? false) { - continue; - } - $networkExists = $networks->contains(function ($value, $key) use ($networkName) { - return $value == $networkName || $key == $networkName; - }); - if (! $networkExists) { - $networks->put($networkName, null); + if (! $use_network_mode) { + if ($topLevel->get('networks')?->count() > 0) { + foreach ($topLevel->get('networks') as $networkName => $network) { + if ($networkName === 'default') { + continue; + } + // ignore aliases + if ($network['aliases'] ?? false) { + continue; + } + $networkExists = $networks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $networks->put($networkName, null); + } } } - } - $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { - return $value == $baseNetwork; - }); - if (! $baseNetworkExists) { - foreach ($baseNetwork as $network) { - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); + $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { + return $value == $baseNetwork; + }); + if (! $baseNetworkExists) { + foreach ($baseNetwork as $network) { + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } } } @@ -3178,180 +3362,46 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $networks_temp = collect(); - foreach ($networks as $key => $network) { - if (gettype($network) === 'string') { - // networks: - // - appwrite - $networks_temp->put($network, null); - } elseif (gettype($network) === 'array') { - // networks: - // default: - // ipv4_address: 192.168.203.254 - $networks_temp->put($key, $network); + if (! $use_network_mode) { + foreach ($networks as $key => $network) { + if (gettype($network) === 'string') { + // networks: + // - appwrite + $networks_temp->put($network, null); + } elseif (gettype($network) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + $networks_temp->put($key, $network); + } } - } - foreach ($baseNetwork as $key => $network) { - $networks_temp->put($network, null); - } - - if ($isApplication) { - if (data_get($resource, 'settings.connect_to_docker_network')) { - $network = $resource->destination->network; + foreach ($baseNetwork as $key => $network) { $networks_temp->put($network, null); - $topLevel->get('networks')->put($network, [ - 'name' => $network, - 'external' => true, - ]); } - } - // convert environment variables to one format - $environment = convertComposeEnvironmentToArray($environment); - // Add Coolify defined environments - $allEnvironments = $resource->environment_variables()->get(['key', 'value']); - - $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { - return [$item['key'] => $item['value']]; - }); - - // remove $environment from $allEnvironments - $coolifyDefinedEnvironments = $allEnvironments->diffKeys($environment); - - // filter magic environments - $magicEnvironments = $environment->filter(function ($value, $key) { - $value = str(replaceVariables(str($value))); - - return str($key)->startsWith('SERVICE_') || str($value)->startsWith('SERVICE_'); - }); - $normalEnvironments = $environment->diffKeys($magicEnvironments); - if ($magicEnvironments->count() > 0) { - foreach ($magicEnvironments as $key => $value) { - $key = str($key); - $value = str(replaceVariables(str($value))); - $originalValue = $value; - $keyCommand = $key->after('SERVICE_')->before('_'); - $valueCommand = $value->after('SERVICE_')->before('_'); - if ($key->startsWith('SERVICE_FQDN_')) { - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - } elseif ($value->startsWith('SERVICE_FQDN_')) { - $fqdnFor = $value->after('SERVICE_FQDN_')->lower()->value(); - if (str($fqdnFor)->contains('_')) { - $fqdnFor = str($fqdnFor)->before('_'); - } - } else { - $fqdnFor = null; - } - if ($keyCommand->value() === 'FQDN' || $valueCommand->value() === 'FQDN') { - 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' && $value->startsWith('/')) { - $path = $value->value(); - if ($value === '/') { - $value = "$fqdn"; - } else { - $value = "$fqdn$path"; - } - } else { - $value = $fqdn; - } - if (! $isDatabase) { - if ($isApplication && is_null($resource->fqdn)) { - data_forget($resource, 'environment_variables'); - data_forget($resource, 'environment_variables_preview'); - $resource->fqdn = $value; - $resource->save(); - } elseif ($isService && is_null($savedService->fqdn)) { - if ($key->startsWith('SERVICE_FQDN_')) { - $savedService->fqdn = $value; - $savedService->save(); - } - } - } - - } elseif ($keyCommand->value() === 'URL' || $valueCommand->value() === 'URL') { - if ($isApplication) { - $fqdn = generateFqdn($server, "{$resource->name}-{$uuid}"); - } elseif ($isService) { - $fqdn = generateFqdn($server, "{$savedService->name}-{$uuid}"); - } - if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) { - $path = $value->value(); - $value = "$fqdn$path"; - } else { - $value = $fqdn; - } - $value = str($fqdn)->replace('http://', '')->replace('https://', ''); - } else { - $generatedValue = generateEnvValue($valueCommand, $resource); - if ($generatedValue) { - $value = $generatedValue; - } - } - if (str($fqdnFor)->startsWith('/')) { - $fqdnFor = null; - } - // Lets save the magic value to the environment variables - if (! $originalValue->startsWith('/')) { - if ($key->startsWith('SERVICE_')) { - $originalValue = $key; - } - $resource->environment_variables()->where('key', $originalValue->value())->where($nameOfId, $resource->id)->firstOrCreate([ - 'key' => $originalValue->value(), - $nameOfId => $resource->id, - ], [ - 'value' => $value, - 'is_build_time' => false, - 'is_preview' => false, + if ($isApplication) { + if (data_get($resource, 'settings.connect_to_docker_network')) { + $network = $resource->destination->network; + $networks_temp->put($network, null); + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, ]); } - // Save the original value to the environment variables - if ($originalValue->startsWith('SERVICE_')) { - $value = "$$originalValue"; - } - $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ - 'key' => $key->value(), - $nameOfId => $resource->id, - ], [ - 'value' => "$value", - 'is_build_time' => false, - 'is_preview' => false, - ]); } } + + $normalEnvironments = $environment->diffKeys($allMagicEnvironments); + $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { + return ! str($value)->startsWith('SERVICE_'); + }); + foreach ($normalEnvironments as $key => $value) { $key = str($key); $value = str($value); - if ($value->startsWith('$') || $value->contains('${')) { - if ($value->contains('${')) { - $value = $value->after('${')->before('}'); - } - $value = str(replaceVariables(str($value))); - if ($value->contains(':-')) { - $key = $value->before(':'); - $value = $value->after(':-'); - } elseif ($value->contains('-')) { - $key = $value->before('-'); - $value = $value->after('-'); - } elseif ($value->contains(':?')) { - $key = $value->before(':'); - $value = $value->after(':?'); - } elseif ($value->contains('?')) { - $key = $value->before('?'); - $value = $value->after('?'); - } else { - $key = $value; - $value = null; - } + $originalValue = $value; + $parsedValue = replaceVariables($value); + if ($value->startsWith('$SERVICE_')) { $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ 'key' => $key, $nameOfId => $resource->id, @@ -3360,6 +3410,57 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_build_time' => false, 'is_preview' => false, ]); + + continue; + } + if (! $value->startsWith('$')) { + continue; + } + if ($key->value() === $parsedValue->value()) { + $value = null; + $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key, + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + if ($value->startsWith('$')) { + if ($value->contains(':-')) { + $value = replaceVariables($value); + $key = $value->before(':'); + $value = $value->after(':-'); + } elseif ($value->contains('-')) { + $value = replaceVariables($value); + + $key = $value->before('-'); + $value = $value->after('-'); + } elseif ($value->contains(':?')) { + $value = replaceVariables($value); + + $key = $value->before(':'); + $value = $value->after(':?'); + } elseif ($value->contains('?')) { + $value = replaceVariables($value); + + $key = $value->before('?'); + $value = $value->after('?'); + } + if ($originalValue->value() === $value->value()) { + continue; + } + $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key, + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } } if ($isApplication) { @@ -3368,13 +3469,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $branch = "pull/{$pullRequestId}/head"; } if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $environment->put('COOLIFY_BRANCH', $branch); + $coolifyEnvironments->put('COOLIFY_BRANCH', $branch); } } // Add COOLIFY_CONTAINER_NAME to environment if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $environment->put('COOLIFY_CONTAINER_NAME', $containerName); + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', $containerName); } if ($isApplication) { @@ -3428,15 +3529,20 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } // Add COOLIFY_FQDN & COOLIFY_URL to environment if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { - $environment->put('COOLIFY_URL', $fqdns->implode(',')); + $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); $urls = $fqdns->map(function ($fqdn) { return str($fqdn)->replace('http://', '')->replace('https://', ''); }); - $environment->put('COOLIFY_FQDN', $urls->implode(',')); + $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); } - add_coolify_default_environment_variables($resource, $environment, $resource->environment_variables); + add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); + if ($environment->count() > 0) { + $environment = $environment->filter(function ($value, $key) { + return ! str($key)->startsWith('SERVICE_FQDN_'); + }); + } $serviceLabels = $labels->merge($defaultLabels); if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if ($isApplication) { @@ -3522,17 +3628,19 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $payload = collect($service)->merge([ 'container_name' => $containerName, 'restart' => $restart->value(), - 'networks' => $networks_temp, 'labels' => $serviceLabels, ]); + if (! $use_network_mode) { + $payload['networks'] = $networks_temp; + } if ($ports->count() > 0) { $payload['ports'] = $ports; } if ($volumesParsed->count() > 0) { $payload['volumes'] = $volumesParsed; } - if ($environment->count() > 0 || $coolifyDefinedEnvironments->count() > 0) { - $payload['environment'] = $environment->merge($coolifyDefinedEnvironments); + if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { + $payload['environment'] = $environment->merge($coolifyEnvironments); } if ($logging) { $payload['logging'] = $logging; diff --git a/composer.json b/composer.json index 7ca65babe..e8b46105d 100644 --- a/composer.json +++ b/composer.json @@ -110,4 +110,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 4deaa42de..fffb320d3 100644 --- a/composer.lock +++ b/composer.lock @@ -921,16 +921,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.321.2", + "version": "3.321.9", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c04f8f30891cee8480c132778cd4cc486467e77a" + "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c04f8f30891cee8480c132778cd4cc486467e77a", - "reference": "c04f8f30891cee8480c132778cd4cc486467e77a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", + "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", "shasum": "" }, "require": { @@ -1013,9 +1013,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.321.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.321.9" }, - "time": "2024-08-30T18:14:40+00:00" + "time": "2024-09-11T18:15:49+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1518,16 +1518,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6" + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d8f68ea6cc00912e5313237130b8c8decf4d28c6", - "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", "shasum": "" }, "require": { @@ -1543,7 +1543,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.11.7", + "phpstan/phpstan": "1.12.0", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -1611,7 +1611,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.0" + "source": "https://github.com/doctrine/dbal/tree/3.9.1" }, "funding": [ { @@ -1627,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T07:34:42+00:00" + "time": "2024-09-01T13:49:23+00:00" }, { "name": "doctrine/deprecations", @@ -2789,16 +2789,16 @@ }, { "name": "laravel/fortify", - "version": "v1.24.0", + "version": "v1.24.1", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "fbe67f018c1fe26d00913de56a6d60589b4be9b2" + "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/fbe67f018c1fe26d00913de56a6d60589b4be9b2", - "reference": "fbe67f018c1fe26d00913de56a6d60589b4be9b2", + "url": "https://api.github.com/repos/laravel/fortify/zipball/8158ba0960bb5f4aae509d01d74a95e16e30de20", + "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20", "shasum": "" }, "require": { @@ -2850,20 +2850,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-08-20T14:43:56+00:00" + "time": "2024-09-03T10:02:14+00:00" }, { "name": "laravel/framework", - "version": "v11.21.0", + "version": "v11.23.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9d9d36708d56665b12185493f684abce38ad2d30" + "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9d9d36708d56665b12185493f684abce38ad2d30", - "reference": "9d9d36708d56665b12185493f684abce38ad2d30", + "url": "https://api.github.com/repos/laravel/framework/zipball/d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", + "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", "shasum": "" }, "require": { @@ -2925,6 +2925,7 @@ "illuminate/bus": "self.version", "illuminate/cache": "self.version", "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", "illuminate/conditionable": "self.version", "illuminate/config": "self.version", "illuminate/console": "self.version", @@ -2967,7 +2968,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.1.5", + "orchestra/testbench-core": "^9.4.0", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", @@ -3025,6 +3026,7 @@ "src/Illuminate/Events/functions.php", "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -3056,20 +3058,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-20T15:00:52+00:00" + "time": "2024-09-11T21:59:23+00:00" }, { "name": "laravel/horizon", - "version": "v5.27.1", + "version": "v5.28.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "184449be3eb296ab16c13a69ce22049f32d0fc2c" + "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/184449be3eb296ab16c13a69ce22049f32d0fc2c", - "reference": "184449be3eb296ab16c13a69ce22049f32d0fc2c", + "url": "https://api.github.com/repos/laravel/horizon/zipball/9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", + "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", "shasum": "" }, "require": { @@ -3133,9 +3135,9 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.27.1" + "source": "https://github.com/laravel/horizon/tree/v5.28.1" }, - "time": "2024-08-05T14:23:30+00:00" + "time": "2024-09-04T14:06:50+00:00" }, { "name": "laravel/prompts", @@ -3322,16 +3324,16 @@ }, { "name": "laravel/socialite", - "version": "v5.15.1", + "version": "v5.16.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029" + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/cc02625f0bd1f95dc3688eb041cce0f1e709d029", - "reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", "shasum": "" }, "require": { @@ -3390,7 +3392,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2024-06-28T20:09:34+00:00" + "time": "2024-09-03T09:46:57+00:00" }, { "name": "laravel/telescope", @@ -4532,16 +4534,16 @@ }, { "name": "lorisleiva/laravel-actions", - "version": "v2.8.3", + "version": "v2.8.4", "source": { "type": "git", "url": "https://github.com/lorisleiva/laravel-actions.git", - "reference": "4507d5bc7b28d168881a799081e60c245b3449db" + "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/4507d5bc7b28d168881a799081e60c245b3449db", - "reference": "4507d5bc7b28d168881a799081e60c245b3449db", + "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/5a168bfdd3b75dd6ff259019d4aeef784bbd5403", + "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403", "shasum": "" }, "require": { @@ -4596,7 +4598,7 @@ ], "support": { "issues": "https://github.com/lorisleiva/laravel-actions/issues", - "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.3" + "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.4" }, "funding": [ { @@ -4604,7 +4606,7 @@ "type": "github" } ], - "time": "2024-08-20T17:08:48+00:00" + "time": "2024-09-10T09:57:29+00:00" }, { "name": "lorisleiva/lody", @@ -4781,16 +4783,16 @@ }, { "name": "mtdowling/jmespath.php", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", - "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", "shasum": "" }, "require": { @@ -4807,7 +4809,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -4841,9 +4843,9 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" }, - "time": "2023-08-25T10:54:48+00:00" + "time": "2024-09-04T18:46:31+00:00" }, { "name": "nesbot/carbon", @@ -5212,16 +5214,16 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.0.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a" + "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/58c4c58cf23df7f498daeb97092e34f5259feb6a", - "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/e5f21eade88689536c0cdad4c3cd75f3ed26e01a", + "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a", "shasum": "" }, "require": { @@ -5231,11 +5233,11 @@ }, "require-dev": { "ergebnis/phpstan-rules": "^2.2.0", - "illuminate/console": "^11.0.0", - "laravel/pint": "^1.14.0", - "mockery/mockery": "^1.6.7", - "pestphp/pest": "^2.34.1", - "phpstan/phpstan": "^1.10.59", + "illuminate/console": "^11.1.1", + "laravel/pint": "^1.15.0", + "mockery/mockery": "^1.6.11", + "pestphp/pest": "^2.34.6", + "phpstan/phpstan": "^1.10.66", "phpstan/phpstan-strict-rules": "^1.5.2", "symfony/var-dumper": "^7.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" @@ -5280,7 +5282,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.0.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.1.0" }, "funding": [ { @@ -5296,20 +5298,20 @@ "type": "github" } ], - "time": "2024-03-06T16:17:14+00:00" + "time": "2024-09-05T15:25:50+00:00" }, { "name": "nyholm/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { @@ -5362,7 +5364,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.1" + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { @@ -5374,28 +5376,28 @@ "type": "github" } ], - "time": "2023-11-13T09:31:12+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v2.7.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", "shasum": "" }, "require": { - "php": "^7|^8" + "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", - "vimeo/psalm": "^1|^2|^3|^4" + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" }, "type": "library", "autoload": { @@ -5441,7 +5443,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:18:48+00:00" + "time": "2024-05-08T12:36:18+00:00" }, { "name": "paragonie/random_compat", @@ -6005,16 +6007,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.0", + "version": "1.30.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" + "reference": "51b95ec8670af41009e2b2b56873bad96682413e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", - "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", + "reference": "51b95ec8670af41009e2b2b56873bad96682413e", "shasum": "" }, "require": { @@ -6046,22 +6048,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" }, - "time": "2024-08-29T09:54:52+00:00" + "time": "2024-09-07T20:13:05+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.0", + "version": "1.12.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "384af967d35b2162f69526c7276acadce534d0e1" + "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1", - "reference": "384af967d35b2162f69526c7276acadce534d0e1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0fcbf194ab63d8159bb70d9aa3e1350051632009", + "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009", "shasum": "" }, "require": { @@ -6106,7 +6108,7 @@ "type": "github" } ], - "time": "2024-08-27T09:18:05+00:00" + "time": "2024-09-09T08:10:35+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6220,24 +6222,24 @@ }, { "name": "pragmarx/google2fa", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1.0|^2.0", + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.18", + "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^7.5.15|^8.5|^9.0" }, "type": "library", @@ -6266,9 +6268,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" }, - "time": "2022-06-13T21:57:56+00:00" + "time": "2024-09-05T11:56:40+00:00" }, { "name": "psr/cache", @@ -6632,16 +6634,16 @@ }, { "name": "psr/log", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "79dff0b268932c640297f5208d6298f71855c03e" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", - "reference": "79dff0b268932c640297f5208d6298f71855c03e", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -6676,9 +6678,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.1" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2024-08-21T13:31:24+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", @@ -7146,21 +7148,21 @@ }, { "name": "rector/rector", - "version": "1.2.4", + "version": "1.2.5", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "42a4aa23b48b4cfc8ebfeac2b570364e27744381" + "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/42a4aa23b48b4cfc8ebfeac2b570364e27744381", - "reference": "42a4aa23b48b4cfc8ebfeac2b570364e27744381", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/e98aa793ca3fcd17e893cfaf9103ac049775d339", + "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.11.11" + "phpstan/phpstan": "^1.12.2" }, "conflict": { "rector/rector-doctrine": "*", @@ -7193,7 +7195,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.4" + "source": "https://github.com/rectorphp/rector/tree/1.2.5" }, "funding": [ { @@ -7201,7 +7203,7 @@ "type": "github" } ], - "time": "2024-08-23T09:03:01+00:00" + "time": "2024-09-08T17:43:24+00:00" }, { "name": "resend/resend-laravel", @@ -9520,20 +9522,20 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -9579,7 +9581,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -9595,24 +9597,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "c027e6a3c6aee334663ec21f5852e89738abc805" + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c027e6a3c6aee334663ec21f5852e89738abc805", - "reference": "c027e6a3c6aee334663ec21f5852e89738abc805", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-iconv": "*" @@ -9659,7 +9661,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" }, "funding": [ { @@ -9675,24 +9677,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -9737,7 +9739,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -9753,26 +9755,25 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", - "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" @@ -9821,7 +9822,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -9837,24 +9838,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -9902,7 +9903,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -9918,24 +9919,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -9982,7 +9983,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -9998,97 +9999,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.30.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "10112722600777e02d2745716b70c5db4ca70442" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", - "reference": "10112722600777e02d2745716b70c5db4ca70442", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -10135,7 +10063,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -10151,24 +10079,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", - "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -10211,7 +10139,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -10227,24 +10155,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:35:24+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9" + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/2ba1f33797470debcda07fe9dce20a0003df18e9", - "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-uuid": "*" @@ -10290,7 +10218,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" }, "funding": [ { @@ -10306,7 +10234,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", @@ -12309,16 +12237,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.2", + "version": "v1.17.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" + "reference": "9d77be916e145864f10788bb94531d03e1f7b482" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", + "reference": "9d77be916e145864f10788bb94531d03e1f7b482", "shasum": "" }, "require": { @@ -12329,13 +12257,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1", - "illuminate/view": "^10.48.18", + "friendsofphp/php-cs-fixer": "^3.64.0", + "illuminate/view": "^10.48.20", "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.0" + "pestphp/pest": "^2.35.1" }, "bin": [ "builds/pint" @@ -12371,7 +12299,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-06T15:11:54+00:00" + "time": "2024-09-03T15:00:28+00:00" }, { "name": "mockery/mockery", diff --git a/config/clockwork.php b/config/clockwork.php new file mode 100644 index 000000000..ce880464a --- /dev/null +++ b/config/clockwork.php @@ -0,0 +1,424 @@ + env('CLOCKWORK_ENABLE', null), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Features + |------------------------------------------------------------------------------------------------------------------ + | + | You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query + | threshold for database queries). + | + */ + + 'features' => [ + + // Cache usage stats and cache queries including results + 'cache' => [ + 'enabled' => env('CLOCKWORK_CACHE_ENABLED', true), + + // Collect cache queries + 'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true), + + // Collect values from cache queries (high performance impact with a very high number of queries) + 'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false) + ], + + // Database usage stats and queries + 'database' => [ + 'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true), + + // Collect database queries (high performance impact with a very high number of queries) + 'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true), + + // Collect details of models updates (high performance impact with a lot of model updates) + 'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true), + + // Collect details of retrieved models (very high performance impact with a lot of models retrieved) + 'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false), + + // Query execution time threshold in milliseconds after which the query will be marked as slow + 'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'), + + // Collect only slow database queries + 'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false), + + // Detect and report duplicate queries + 'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false) + ], + + // Dispatched events + 'events' => [ + 'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true), + + // Ignored events (framework events are ignored by default) + 'ignored_events' => [ + // App\Events\UserRegistered::class, + // 'user.registered' + ], + ], + + // Laravel log (you can still log directly to Clockwork with laravel log disabled) + 'log' => [ + 'enabled' => env('CLOCKWORK_LOG_ENABLED', true) + ], + + // Sent notifications + 'notifications' => [ + 'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true), + ], + + // Performance metrics + 'performance' => [ + // Allow collecting of client metrics. Requires separate clockwork-browser npm package. + 'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true) + ], + + // Dispatched queue jobs + 'queue' => [ + 'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true) + ], + + // Redis commands + 'redis' => [ + 'enabled' => env('CLOCKWORK_REDIS_ENABLED', true) + ], + + // Routes list + 'routes' => [ + 'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false), + + // Collect only routes from particular namespaces (only application routes by default) + 'only_namespaces' => [ 'App' ] + ], + + // Rendered views + 'views' => [ + 'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true), + + // Collect views including view data (high performance impact with a high number of views) + 'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false), + + // Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does + // not support collecting view data) + 'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false) + ] + + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Enable web UI + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this + | feature. You can also set a custom path for the web UI. + | + */ + + 'web' => env('CLOCKWORK_WEB', true), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Enable toolbar + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature. + | Requires a separate clockwork-browser npm library. + | For installation instructions see https://underground.works/clockwork/#docs-viewing-data + | + */ + + 'toolbar' => env('CLOCKWORK_TOOLBAR', true), + + /* + |------------------------------------------------------------------------------------------------------------------ + | HTTP requests collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected. + | + */ + + 'requests' => [ + // With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you + // manually pass a "clockwork-profile" cookie or get/post data key. + // Optionally you can specify a "secret" that has to be passed as the value to enable profiling. + 'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false), + + // Collect only errors (requests with HTTP 4xx and 5xx responses) + 'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false), + + // Response time threshold in milliseconds after which the request will be marked as slow + 'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'), + + // Collect only slow requests + 'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false), + + // Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests) + 'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false), + + // List of URIs that should not be collected + 'except' => [ + '/horizon/.*', // Laravel Horizon requests + '/telescope/.*', // Laravel Telescope requests + '/_tt/.*', // Laravel Telescope toolbar + '/_debugbar/.*', // Laravel DebugBar requests + ], + + // List of URIs that should be collected, any other URI will not be collected if not empty + 'only' => [ + // '/api/.*' + ], + + // Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest + 'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true) + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Artisan commands collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands + | should be collected. + | + */ + + 'artisan' => [ + // Enable or disable collection of executed Artisan commands + 'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false), + + // List of commands that should not be collected (built-in commands are not collected by default) + 'except' => [ + // 'inspire' + ], + + // List of commands that should be collected, any other command will not be collected if not empty + 'only' => [ + // 'inspire' + ], + + // Enable or disable collection of command output + 'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false), + + // Enable or disable collection of built-in Laravel commands + 'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true) + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Queue jobs collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should + | be collected. + | + */ + + 'queue' => [ + // Enable or disable collection of executed queue jobs + 'collect' => env('CLOCKWORK_QUEUE_COLLECT', false), + + // List of queue jobs that should not be collected + 'except' => [ + // App\Jobs\ExpensiveJob::class + ], + + // List of queue jobs that should be collected, any other queue job will not be collected if not empty + 'only' => [ + // App\Jobs\BuggyJob::class + ] + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Tests collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can collect data about executed tests. Here you can enable and configure which tests should be + | collected. + | + */ + + 'tests' => [ + // Enable or disable collection of ran tests + 'collect' => env('CLOCKWORK_TESTS_COLLECT', false), + + // List of tests that should not be collected + 'except' => [ + // Tests\Unit\ExampleTest::class + ] + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Enable data collection when Clockwork is disabled + |------------------------------------------------------------------------------------------------------------------ + | + | You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis. + | + */ + + 'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Metadata storage + |------------------------------------------------------------------------------------------------------------------ + | + | Configure how is the metadata collected by Clockwork stored. Three options are available: + | - files - A simple fast storage implementation storing data in one-per-request files. + | - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO. + | - redis - Stores requests in redis. Requires phpredis. + */ + + 'storage' => env('CLOCKWORK_STORAGE', 'files'), + + // Path where the Clockwork metadata is stored + 'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')), + + // Compress the metadata files using gzip, trading a little bit of performance for lower disk usage + 'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false), + + // SQL database to use, can be a name of database configured in database.php or a path to a SQLite file + 'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')), + + // SQL table name to use, the table is automatically created and updated when needed + 'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'), + + // Redis connection, name of redis connection or cluster configured in database.php + 'storage_redis' => env('CLOCKWORK_STORAGE_REDIS', 'default'), + + // Redis prefix for Clockwork keys ("clockwork" if not set) + 'storage_redis_prefix' => env('CLOCKWORK_STORAGE_REDIS_PREFIX', 'clockwork'), + + // Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable + 'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Authentication + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can be configured to require authentication before allowing access to the collected data. This might be + | useful when the application is publicly accessible. Setting to true will enable a simple authentication with a + | pre-configured password. You can also pass a class name of a custom implementation. + | + */ + + 'authentication' => env('CLOCKWORK_AUTHENTICATION', false), + + // Password for the simple authentication + 'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Stack traces collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set + | whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting + | long stack traces considerably increases metadata size. + | + */ + + 'stack_traces' => [ + // Enable or disable collecting of stack traces + 'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true), + + // Limit the number of frames to be collected + 'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10), + + // List of vendor names to skip when determining caller, common vendors are automatically added + 'skip_vendors' => [ + // 'phpunit' + ], + + // List of namespaces to skip when determining caller + 'skip_namespaces' => [ + // 'Laravel' + ], + + // List of class names to skip when determining caller + 'skip_classes' => [ + // App\CustomLog::class + ] + + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Serialization + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects + | of serialization. Serialization has a large effect on the cpu time and memory usage. + | + */ + + // Maximum depth of serialized multi-level arrays and objects + 'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10), + + // A list of classes that will never be serialized (e.g. a common service container class) + 'serialization_blackbox' => [ + \Illuminate\Container\Container::class, + \Illuminate\Foundation\Application::class, + \Laravel\Lumen\Application::class + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Register helpers + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to + | access the Clockwork instance. + | + */ + + 'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true), + + /* + |------------------------------------------------------------------------------------------------------------------ + | Send headers for AJAX request + |------------------------------------------------------------------------------------------------------------------ + | + | When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an + | API might require a version number using Accept headers to route the HTTP request to the correct codebase. + | + */ + + 'headers' => [ + // 'Accept' => 'application/vnd.com.whatever.v1+json', + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Server timing + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics + | in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev + | Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false + | will disable the feature. + | + */ + + 'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10) + +]; diff --git a/config/constants.php b/config/constants.php index 861b645ed..906ef3ba2 100644 --- a/config/constants.php +++ b/config/constants.php @@ -6,7 +6,9 @@ return [ 'contact' => 'https://coolify.io/docs/contact', ], 'ssh' => [ - 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1m'), + // Using MUX + 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true), true), + 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1h'), 'connection_timeout' => 10, 'server_interval' => 20, 'command_timeout' => 7200, diff --git a/config/coolify.php b/config/coolify.php index a6d6d8581..f9878fff7 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -7,11 +7,10 @@ return [ 'self_hosted' => env('SELF_HOSTED', true), 'waitlist' => env('WAITLIST', false), 'license_url' => 'https://licenses.coollabs.io', - 'mux_enabled' => env('MUX_ENABLED', true), 'dev_webhook' => env('SERVEO_URL'), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), - 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), + 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), ]; diff --git a/config/sentry.php b/config/sentry.php index 6d59954e0..bae81659a 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.329', + 'release' => '4.0.0-beta.336', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 7cbe1f681..bab78d59c 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('helper_version')->default('1.0.0'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('helper_version'); + }); + } +}; diff --git a/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php b/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php new file mode 100644 index 000000000..ad6e5bd9e --- /dev/null +++ b/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php @@ -0,0 +1,37 @@ +boolean('force_docker_cleanup')->default(true)->change(); + }); + $serverSettings = ServerSetting::all(); + foreach ($serverSettings as $serverSetting) { + if ($serverSetting->force_docker_cleanup === false) { + $serverSetting->force_docker_cleanup = true; + $serverSetting->docker_cleanup_frequency = '*/10 * * * *'; + $serverSetting->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('force_docker_cleanup')->default(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_09_07_185402_change_cleanup_schedule.php b/database/migrations/2024_09_07_185402_change_cleanup_schedule.php new file mode 100644 index 000000000..ea8861f9b --- /dev/null +++ b/database/migrations/2024_09_07_185402_change_cleanup_schedule.php @@ -0,0 +1,37 @@ +string('docker_cleanup_frequency')->default('0 0 * * *')->change(); + }); + + $serverSettings = ServerSetting::all(); + foreach ($serverSettings as $serverSetting) { + if ($serverSetting->force_docker_cleanup && $serverSetting->docker_cleanup_frequency === '*/10 * * * *') { + $serverSetting->docker_cleanup_frequency = '0 0 * * *'; + $serverSetting->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('docker_cleanup_frequency')->default('*/10 * * * *')->change(); + }); + } +}; diff --git a/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php b/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php new file mode 100644 index 000000000..db1322e62 --- /dev/null +++ b/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php @@ -0,0 +1,28 @@ +string('server_timezone')->default('UTC')->change(); + }); + + DB::table('server_settings') + ->whereNull('server_timezone') + ->orWhere('server_timezone', '') + ->update(['server_timezone' => 'UTC']); + } + + public function down() + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('server_timezone')->default('')->change(); + }); + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7eda14d41..718d52d04 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,6 +19,7 @@ services: PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" volumes: - .:/var/www/html/:cached + - /data/coolify/backups/:/var/www/html/storage/app/backups postgres: pull_policy: always ports: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b26cd5746..65a708acb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -48,6 +48,7 @@ services: - PUSHER_APP_SECRET - AUTOUPDATE - SELF_HOSTED + - SSH_MUX_ENABLED - SSH_MUX_PERSIST_TIME - FEEDBACK_DISCORD_WEBHOOK - WAITLIST diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index a1ee1aeea..3f45b62cd 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -45,7 +45,7 @@ services: - PUSHER_APP_SECRET - AUTOUPDATE=true - SELF_HOSTED=true - - MUX_ENABLED=false + - SSH_MUX_ENABLED=false - IS_WINDOWS_DOCKER_DESKTOP=true ports: - "${APP_PORT:-8000}:80" diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 472f46c5d..7aa9d8722 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -8,9 +8,9 @@ ARG DOCKER_COMPOSE_VERSION=2.27.1 # https://github.com/docker/buildx/releases ARG DOCKER_BUILDX_VERSION=0.14.1 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.34.1 +ARG PACK_VERSION=0.35.1 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.24.1 +ARG NIXPACKS_VERSION=1.28.0 USER root WORKDIR /artifacts @@ -34,7 +34,7 @@ RUN if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \ ;fi -COPY --from=minio/mc:RELEASE.2024-03-13T23-51-57Z /usr/bin/mc /usr/bin/mc +COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["/sbin/tini", "--"] diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index f75a0ff1e..63832dc36 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -37,6 +37,9 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" +COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +RUN chmod +x /usr/bin/mc + RUN { \ echo 'upload_max_filesize=256M'; \ echo 'post_max_size=256M'; \ diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index f46124062..d0cebcbca 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -68,3 +68,6 @@ RUN { \ echo 'upload_max_filesize=256M'; \ echo 'post_max_size=256M'; \ } > /etc/php/current_version/cli/conf.d/upload-limits.ini + +COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +RUN chmod +x /usr/bin/mc diff --git a/openapi.yaml b/openapi.yaml index e5bebd5fa..ce0503e1f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2831,7 +2831,7 @@ paths: - name: uuid in: path - description: 'Deployment Uuid' + description: 'Deployment UUID' required: true schema: type: string @@ -2879,7 +2879,7 @@ paths: type: boolean responses: '200': - description: "Get deployment(s) Uuid's" + description: "Get deployment(s) UUID's" content: application/json: schema: @@ -2993,7 +2993,7 @@ paths: tags: - Projects summary: List - description: 'list projects.' + description: 'List projects.' operationId: list-projects responses: '200': @@ -3024,7 +3024,7 @@ paths: application/json: schema: properties: - uuid: + name: type: string description: 'The name of the project.' description: @@ -3054,7 +3054,7 @@ paths: tags: - Projects summary: Get - description: 'Get project by Uuid.' + description: 'Get project by UUID.' operationId: get-project-by-uuid parameters: - @@ -3323,7 +3323,7 @@ paths: - name: uuid in: path - description: 'Private Key Uuid' + description: 'Private Key UUID' required: true schema: type: string @@ -3355,7 +3355,7 @@ paths: - name: uuid in: path - description: 'Private Key Uuid' + description: 'Private Key UUID' required: true schema: type: string @@ -3475,7 +3475,7 @@ paths: - name: uuid in: path - description: "Server's Uuid" + description: "Server's UUID" required: true schema: type: string @@ -3595,7 +3595,7 @@ paths: - name: uuid in: path - description: "Server's Uuid" + description: "Server's UUID" required: true schema: type: string @@ -3627,7 +3627,7 @@ paths: - name: uuid in: path - description: "Server's Uuid" + description: "Server's UUID" required: true schema: type: string @@ -3784,7 +3784,7 @@ paths: type: string responses: '200': - description: 'Get a service by Uuid.' + description: 'Get a service by UUID.' content: application/json: schema: @@ -3814,7 +3814,7 @@ paths: type: string responses: '200': - description: 'Delete a service by Uuid' + description: 'Delete a service by UUID' content: application/json: schema: @@ -3830,6 +3830,256 @@ paths: security: - bearerAuth: [] + '/services/{uuid}/envs': + get: + tags: + - Services + summary: 'List Envs' + description: 'List all envs by service UUID.' + operationId: list-envs-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'All environment variables by service UUID.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EnvironmentVariable' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + post: + tags: + - Services + summary: 'Create Env' + description: 'Create env by service UUID.' + operationId: create-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env created.' + required: true + content: + application/json: + schema: + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: nc0k04gk8g0cgsk440g0koko } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Services + summary: 'Update Env' + description: 'Update env by service UUID.' + operationId: update-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env updated.' + required: true + content: + application/json: + schema: + required: + - key + - value + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/envs/bulk': + patch: + tags: + - Services + summary: 'Update Envs (Bulk)' + description: 'Update multiple envs by service UUID.' + operationId: update-envs-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Bulk envs updated.' + required: true + content: + application/json: + schema: + required: + - data + properties: + data: + type: array + items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_build_time: { type: boolean, description: 'The flag to indicate if the environment variable is used in build time.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } + type: object + responses: + '201': + description: 'Environment variables updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variables updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/envs/{env_uuid}': + delete: + tags: + - Services + summary: 'Delete Env' + description: 'Delete env by UUID.' + operationId: delete-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + - + name: env_uuid + in: path + description: 'UUID of the environment variable.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Environment variable deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] '/services/{uuid}/start': get: tags: @@ -4480,6 +4730,8 @@ components: type: string name: type: string + description: + type: string environments: description: 'The environments of the project.' type: array diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 01fdcbc41..23c2efc6f 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -VERSION="1.3.4" +VERSION="1.4" DOCKER_VERSION="26.0" CDN="https://cdn.coollabs.io/coolify-nightly" @@ -45,6 +45,12 @@ if [ "$OS_TYPE" = 'amzn' ]; then fi LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') +LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') + +if [ -z "$LATEST_HELPER_VERSION" ]; then + LATEST_HELPER_VERSION=latest +fi + DATE=$(date +"%Y%m%d-%H%M%S") if [ $EUID != 0 ]; then @@ -53,9 +59,9 @@ if [ $EUID != 0 ]; then fi case "$OS_TYPE" in -arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn) ;; +arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;; *) - echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." + echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now." exit ;; esac @@ -75,6 +81,7 @@ echo -e "-------------" echo "OS: $OS_TYPE $OS_VERSION" echo "Coolify version: $LATEST_VERSION" +echo "Helper version: $LATEST_HELPER_VERSION" echo -e "-------------" echo "Installing required packages..." @@ -83,6 +90,11 @@ case "$OS_TYPE" in arch) pacman -Sy --noconfirm --needed curl wget git jq >/dev/null || true ;; +alpine) + sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories + apk update >/dev/null + apk add curl wget git jq >/dev/null + ;; ubuntu | debian | raspbian) apt-get update -y >/dev/null apt-get install -y curl wget git jq >/dev/null @@ -165,70 +177,74 @@ if [ -x "$(command -v snap)" ]; then fi if ! [ -x "$(command -v docker)" ]; then - # Almalinux - if [ "$OS_TYPE" == 'almalinux' ]; then - dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - if ! [ -x "$(command -v docker)" ]; then - echo "Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - systemctl start docker - systemctl enable docker - else - set +e - if ! [ -x "$(command -v docker)" ]; then - echo "Docker is not installed. Installing Docker." - # Arch Linux - if [ "$OS_TYPE" = "arch" ]; then - pacman -Sy docker docker-compose --noconfirm - systemctl enable docker.service + case "$OS_TYPE" in + "almalinux") + dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + if ! [ -x "$(command -v docker)" ]; then + echo "Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + exit 1 + fi + systemctl start docker + systemctl enable docker + ;; + "alpine") + apk add docker docker-cli-compose + rc-update add docker default + service docker start + if [ -x "$(command -v docker)" ]; then + echo "Docker installed successfully." + else + echo "Failed to install Docker with apk. Try to install it manually." + echo "Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." + exit + fi + ;; + "arch") + pacman -Sy docker docker-compose --noconfirm + systemctl enable docker.service + if [ -x "$(command -v docker)" ]; then + echo "Docker installed successfully." + else + echo "Failed to install Docker with pacman. Try to install it manually." + echo "Please visit https://wiki.archlinux.org/title/docker for more information." + exit + fi + ;; + "amzn") + dnf install docker -y + DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} + mkdir -p $DOCKER_CONFIG/cli-plugins + curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose + chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose + systemctl start docker + systemctl enable docker + if [ -x "$(command -v docker)" ]; then + echo "Docker installed successfully." + else + echo "Failed to install Docker with dnf. Try to install it manually." + echo "Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." + exit + fi + ;; + *) + # Automated Docker installation + curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh + if [ -x "$(command -v docker)" ]; then + echo "Docker installed successfully." + else + echo "Docker installation failed with Rancher script. Trying with official script." + curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} if [ -x "$(command -v docker)" ]; then echo "Docker installed successfully." else - echo "Failed to install Docker with pacman. Try to install it manually." - echo "Please visit https://wiki.archlinux.org/title/docker for more information." - exit - fi - else - # Amazon Linux 2023 - if [ "$OS_TYPE" = "amzn" ]; then - dnf install docker -y - DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} - mkdir -p $DOCKER_CONFIG/cli-plugins - curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose - chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose - systemctl start docker - systemctl enable docker - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Failed to install Docker with pacman. Try to install it manually." - echo "Please visit https://wiki.archlinux.org/title/docker for more information." - exit - fi - else - # Automated Docker installation - curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Docker installation failed with Rancher script. Trying with official script." - curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Docker installation failed with official script." - echo "Maybe your OS is not supported?" - echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." - exit 1 - fi - fi + echo "Docker installation failed with official script." + echo "Maybe your OS is not supported?" + echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + exit 1 fi fi - fi - set -e - fi + esac fi echo -e "-------------" @@ -260,17 +276,50 @@ if ! jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify fi mv "$TEMP_FILE" /etc/docker/daemon.json +restart_docker_service() { + + # Check if systemctl is available + if command -v systemctl >/dev/null 2>&1; then + echo "Using systemctl to restart Docker..." + systemctl restart docker + + if [ $? -eq 0 ]; then + echo "Docker restarted successfully using systemctl." + else + echo "Failed to restart Docker using systemctl." + return 1 + fi + + # Check if service command is available + elif command -v service >/dev/null 2>&1; then + echo "Using service command to restart Docker..." + service docker restart + + if [ $? -eq 0 ]; then + echo "Docker restarted successfully using service." + else + echo "Failed to restart Docker using service." + return 1 + fi + + # If neither systemctl nor service is available + else + echo "Neither systemctl nor service command is available on this system." + return 1 + fi +} + if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE")) if [ "$DIFF" != "" ]; then echo "Docker configuration updated, restart docker daemon..." - systemctl restart docker + restart_docker_service else echo "Docker configuration is up to date." fi else echo "Docker configuration updated, restart docker daemon..." - systemctl restart docker + restart_docker_service fi echo -e "-------------" @@ -289,28 +338,35 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh # Copy .env.example if .env does not exist -if [ ! -f $ENV_FILE ]; then - cp /data/coolify/source/.env.production $ENV_FILE - # Generate a secure APP_ID and APP_KEY - sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE" - sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE" +if [ -f $ENV_FILE ]; then + echo "File exists: $ENV_FILE" + cat $ENV_FILE + echo "Copying .env to .env-$DATE" + cp $ENV_FILE $ENV_FILE-$DATE +else + echo "File does not exist: $ENV_FILE" + echo "Copying .env.production to .env-$DATE" + cp /data/coolify/source/.env.production $ENV_FILE-$DATE + # Generate a secure APP_ID and APP_KEY + sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" + sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" # Generate a secure Postgres DB username and password # Causes issues: database "random-user" does not exist - # sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE" - sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE" + # sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" # Generate a secure Redis password - sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE" + sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" # Generate secure Pusher credentials - sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE" - sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE" - sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE" + sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" + sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" + sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" fi # Merge .env and .env.production. New values will be added to .env -sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' >/data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env +awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE if [ "$AUTOUPDATE" = "false" ]; then if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then @@ -342,8 +398,8 @@ if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then addSshKey fi -bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" - +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" +rm -f $ENV_FILE-$DATE echo "Waiting for 20 seconds for Coolify to be ready..." sleep 20 diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 775cd3f81..45295a510 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -1,15 +1,17 @@ #!/bin/bash ## Do not modify this file. You will lose the ability to autoupdate! -VERSION="1.0.6" +VERSION="1.1" CDN="https://cdn.coollabs.io/coolify-nightly" +LATEST_IMAGE=${1:-latest} +LATEST_HELPER_VERSION=${2:-latest} curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production # Merge .env and .env.production. New values will be added to .env -sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' >/data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env +awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production > /data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env # Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then @@ -31,7 +33,7 @@ docker network create --attachable coolify 2>/dev/null if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo "docker-compose.custom.yml detected." - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" else - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" fi diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 9ad886308..7bb400bfd 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,13 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.324" + "version": "4.0.0-beta.330" }, "nightly": { - "version": "4.0.0-beta.324" + "version": "4.0.0-beta.331" + }, + "helper": { + "version": "1.0.0" } } -} \ No newline at end of file +} diff --git a/public/svgs/browserless.svg b/public/svgs/browserless.svg new file mode 100644 index 000000000..1d2d09a23 --- /dev/null +++ b/public/svgs/browserless.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/plunk.svg b/public/svgs/plunk.svg new file mode 100644 index 000000000..3f6ed4792 --- /dev/null +++ b/public/svgs/plunk.svg @@ -0,0 +1 @@ + diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 2e5fa41c9..7b7ef8c12 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -28,25 +28,33 @@ -

You don't need to manage your servers anymore. +

+ You don't need to manage your servers anymore. Coolify does - it for you.

-

All configurations are stored on your servers, so - everything works without a connection to Coolify (except integrations and automations).

-

You can get notified on your favourite platforms + it for you. +

+

+ All configurations are stored on your servers, so + everything works without a connection to Coolify (except integrations and automations). +

+

+ You can get notified on your favourite platforms (Discord, - Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.

+ Telegram, Email, etc.) when something goes wrong, or an action is needed from your side. +

- Next + Next @elseif ($currentState === 'select-server-type') - Do you want to deploy your resources to your - or to a ? + Do you want to deploy your resources to your + + or to a + ? Remote Server + @if (!$serverReachable) - Localhost is not reachable with the following public key. -

- Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for - user or skip the boarding process and add a new private key manually to Coolify and to the - server. -
- Check this documentation for further - help. - - Check again - +
+

Server is not reachable

+

Please check the connection details below and correct them if they are + incorrect.

+ +
+ +
+ +

+ Non-root user is experimental: + docs +

+
+
+ +
+

If the connection details are correct, please ensure:

+
    +
  • The correct public key is in your ~/.ssh/authorized_keys + file for the specified user
  • +
  • Or skip the boarding process and manually add a new private key to Coolify and + the server
  • +
+
+ +

+ For more help, check this documentation. +

+ + + + + Check Again + +
@endif

Servers are the main building blocks, as they will host your applications, databases, services, called resources. Any CPU intensive process will use the server's CPU where you are deploying your resources.

-

is the server where Coolify is running on. It is not +

+ is the server where Coolify is running on. It is not recommended to use one server - for everything.

-

is a server reachable through SSH. It can be hosted + for everything. +

+

+ is a server reachable through SSH. It can be hosted at home, or from any cloud - provider.

+ provider. +

@elseif ($currentState === 'private-key') @@ -126,10 +168,7 @@
No - (create - one - for - me) + (create one for me)
@@ -145,20 +184,48 @@
@if (!$serverReachable) - This server is not reachable with the following public key. -

- Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for - user or skip the boarding process and add a new private key manually to Coolify and to the - server. -
- Check this documentation for further - help. - - Check - again - +
+

Server is not reachable

+

Please check the connection details below and correct them if they are + incorrect.

+ +
+ +
+ +

+ Non-root user is experimental: + docs +

+
+
+ +
+

If the connection details are correct, please ensure:

+
    +
  • The correct public key is in your ~/.ssh/authorized_keys + file for the specified user
  • +
  • Or skip the boarding process and manually add a new private key to Coolify and + the server
  • +
+
+ +

+ For more help, check this documentation. +

+ + + + + Check again + +
@endif @@ -180,8 +247,8 @@ label="Name" id="privateKeyName" /> - + @if ($privateKeyType === 'create') ACTION REQUIRED: Copy the 'Public Key' to your @@ -209,27 +276,37 @@
+ label="Name" id="remoteServerName" wire:model="remoteServerName" /> + label="Description" id="remoteServerDescription" + wire:model="remoteServerDescription" />
- - -
- -
Non-root user is experimental: docs. + +
+
+ +
+ +
+ +
Non-root user is + experimental: docs. +
+ id="isCloudflareTunnel" label="Cloudflare Tunnel" wire:model="isCloudflareTunnel" />
Continue @@ -329,5 +406,4 @@
-
diff --git a/resources/views/livewire/project/application/deployment/index.blade.php b/resources/views/livewire/project/application/deployment/index.blade.php index 001499b71..f6fdb64ab 100644 --- a/resources/views/livewire/project/application/deployment/index.blade.php +++ b/resources/views/livewire/project/application/deployment/index.blade.php @@ -31,11 +31,12 @@ @endif @forelse ($deployments as $deployment)
+ 'dark:bg-coolgray-100 p-2 border-l-2 transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200', + 'border-warning border-dashed ' => data_get($deployment, 'status') === 'in_progress' || data_get($deployment, 'status') === 'cancelled-by-user', - 'border-error' => data_get($deployment, 'status') === 'failed', + 'border-error border-dashed ' => + data_get($deployment, 'status') === 'failed', 'border-success' => data_get($deployment, 'status') === 'finished', ]) x-on:click.stop="goto('{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}')"> @@ -106,7 +107,7 @@
@if ($deployment->status !== 'in_progress') - Finished 0s in + Finished 0s ago in 0s @else Running for 0s @@ -157,7 +158,7 @@ } }, measure_since_started() { - return dayjs.utc(created_at).fromNow(); + return dayjs.utc(created_at).fromNow(true); // "true" prevents the "ago" suffix }, })) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index f97914ec2..9d9301d5c 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -9,6 +9,7 @@ fullscreen: false, alwaysScroll: false, intervalId: null, + showTimestamps: true, makeFullscreen() { this.fullscreen = !this.fullscreen; if (this.fullscreen === false) { @@ -53,63 +54,69 @@ class="dark:text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}.
@endif -
+
- - - + class="flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300" + :class="fullscreen ? '' : 'min-h-14 max-h-[40rem] border border-dotted rounded'"> +
+
+ + + + + +
+
-
- @if (decode_remote_command_output($application_deployment_queue)->count() > 0) - @foreach (decode_remote_command_output($application_deployment_queue) as $line) + @forelse ($this->logLines as $line) +
$line['command'] ?? false, + 'flex gap-2 dark:hover:bg-coolgray-500 hover:bg-gray-100', + ])> + {{ $line['timestamp'] }} $line['hidden'], - 'text-red-500 font-bold whitespace-pre-line' => $line['type'] == 'stderr', - ])>[{{ $line['timestamp'] }}] @if ($line['hidden']) -

[COMMAND] {{ $line['command'] }}
[OUTPUT] - @endif @if (str($line['output'])->contains('http://') || str($line['output'])->contains('https://')) - @php - $line['output'] = preg_replace( - '/(https?:\/\/[^\s]+)/', - '$1', - $line['output'], - ); - @endphp {!! $line['output'] !!} - @else - {{ $line['output'] }} - - @endif -
- @endforeach - @else - No logs yet. - @endif + 'text-coollabs dark:text-warning' => $line['hidden'], + 'text-red-500' => $line['stderr'], + 'font-bold' => $line['command'] ?? false, + 'whitespace-pre-wrap', + ])>{!! $line['line'] !!} +
+ @empty + No logs yet. + @endforelse
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index 6d177505c..54c3e2ede 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -4,7 +4,7 @@

Executions

Cleanup Failed Backups
-
+
@forelse($executions as $execution)
@empty -
No executions found.
+
No executions found.
@endforelse