diff --git a/.env.development.example b/.env.development.example
index 920c32d92..d4daed4f7 100644
--- a/.env.development.example
+++ b/.env.development.example
@@ -1,16 +1,34 @@
-APP_NAME=Coolify-localhost
-APP_ID=development
+# Coolify Configuration
APP_ENV=local
+APP_NAME="Coolify Development"
+APP_ID=development
APP_KEY=
-APP_DEBUG=true
APP_URL=http://localhost
APP_PORT=8000
-MUX_ENABLED=false
+APP_DEBUG=true
+SSH_MUX_ENABLED=true
+# PostgreSQL Database Configuration
+DB_DATABASE=coolify
+DB_USERNAME=coolify
+DB_PASSWORD=password
+DB_HOST=host.docker.internal
+DB_PORT=5432
+
+# Ray Configuration
+# Set to true to enable Ray
+RAY_ENABLED=false
+# Set custom ray port
+# RAY_PORT=
+
+# Enable Laravel Telescope for debugging
+TELESCOPE_ENABLED=false
+
+# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
-## For Andras only
-# To purge cache
+# Special Keys for Andras
+# For cache purging
BUNNY_API_KEY=
-# To upload assets
+# For asset uploads
BUNNY_STORAGE_API_KEY=
diff --git a/.env.production b/.env.production
index f15a8b0e9..099ec7c25 100644
--- a/.env.production
+++ b/.env.production
@@ -1,10 +1,16 @@
+# Coolify Configuration
APP_ID=
APP_NAME=Coolify
APP_KEY=
+# PostgreSQL Database Configuration
+DB_USERNAME=coolify
DB_PASSWORD=
+
+# Redis Configuration
REDIS_PASSWORD=
+# Pusher Configuration
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
new file mode 100644
index 000000000..42df4785e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
@@ -0,0 +1,65 @@
+name: 🐞 Bug Report
+description: "File a new bug report."
+title: "[Bug]: "
+labels: ["🐛 Bug", "🔍 Triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ > [!IMPORTANT]
+ > **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
+
+ # 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
+ - If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
+
+ - type: textarea
+ attributes:
+ label: Error Message and Logs
+ description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Steps to Reproduce
+ description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
+ value: |
+ 1.
+ 2.
+ 3.
+ 4.
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Example Repository URL
+ description: If applicable, provide a URL to a repository demonstrating the issue.
+
+ - type: input
+ attributes:
+ label: Coolify Version
+ description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
+ placeholder: "v4.0.0-beta.335"
+ validations:
+ required: true
+
+ - type: dropdown
+ attributes:
+ label: Are you using Coolify Cloud?
+ options:
+ - "No (self-hosted)"
+ - "Yes (Coolify Cloud)"
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Operating System and Version (self-hosted)
+ description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
+ placeholder: "Ubuntu 22.04"
+
+ - type: textarea
+ attributes:
+ label: Additional Information
+ description: Any other relevant details about the issue.
diff --git a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
new file mode 100644
index 000000000..ef26125e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
@@ -0,0 +1,31 @@
+name: 💎 Enhancement Bounty
+description: "Propose a new feature, service, or improvement with an attached bounty."
+title: "[Enhancement]: "
+labels: ["✨ Enhancement", "🔍 Triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ > [!IMPORTANT]
+ > **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
+
+ # 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
+ - [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
+
+ - type: dropdown
+ attributes:
+ label: Request Type
+ description: Select the type of request you are making.
+ options:
+ - New Feature
+ - New Service
+ - Improvement
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Description
+ description: Provide a detailed description of the feature, improvement, or service you are proposing.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
deleted file mode 100644
index f3d52b1b4..000000000
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Bug report
-description: "Create a new bug report."
-title: "[Bug]: "
-body:
- - type: markdown
- attributes:
- value: >-
- # 💎 Bounty program (with
- [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
-
-
- If you would like to prioritize the issue resolution, you can add bounty
- to this issue.
-
-
- Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
- get started.
- - type: textarea
- attributes:
- label: Description
- description: A clear and concise description of the problem
- - type: textarea
- attributes:
- label: Minimal Reproduction (if possible, example repository)
- description: Please provide a step by step guide to reproduce the issue.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Exception or Error
- description: Please provide error logs if possible.
- - type: input
- attributes:
- label: Version
- description: Coolify's version (see top of your screen).
- validations:
- required: true
- - type: checkboxes
- attributes:
- label: Cloud?
- description: "Are you using the cloud version of Coolify?"
- options:
- - label: 'Yes'
- required: false
- - label: 'No'
- required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4f12f436c..92c48e2d6 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,18 @@
blank_issues_enabled: false
+
contact_links:
- - name: 🤔 Community Support (Chat)
+ - name: 🤔 Questions and Community Support
url: https://coollabs.io/discord
- about: Reach out to us on Discord.
- - name: 🙋♂️ Feature Requests
- url: https://github.com/coollabsio/coolify/discussions/categories/new-features
- about: All feature requests will be discussed here.
+ about: If you have any questions, reach out to us on Discord inside the "#support" channel.
+
+ - name: 💡 Feature Request
+ url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
+ about: Suggest a new feature for Coolify.
+
+ - name: ⚙️ Service Request
+ url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
+ about: Request a new service integration for Coolify.
+
+ - name: 🔧 Improvements
+ url: https://github.com/coollabsio/coolify/discussions/categories/improvements
+ about: Suggest improvements to existing features for Coolify.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 3ded74ce3..5afe00a30 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1 +1,13 @@
-> Always use `next` branch as destination branch for PRs, not `main`
+## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
+- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
+- [ ] I have listed all changes in the `Changes` section.
+- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
+- [ ] I have tested my changes.
+- [ ] I have considered backwards compatibility.
+- [ ] I have removed this checklist and any unused sections.
+
+## Changes
+-
+
+## Issues
+- fix #
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 7e8132ec6..a9e8a5dd0 100644
--- a/.github/workflows/coolify-helper.yml
+++ b/.github/workflows/coolify-helper.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 }}:latest
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ 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 }}:latest-aarch64
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-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 }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
+
diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml
new file mode 100644
index 000000000..33e048627
--- /dev/null
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -0,0 +1,103 @@
+name: Coolify Realtime Development (v4)
+
+on:
+ push:
+ branches: [ "next" ]
+ paths:
+ - .github/workflows/coolify-realtime.yml
+ - docker/coolify-realtime/Dockerfile
+ - docker/coolify-realtime/terminal-server.js
+ - docker/coolify-realtime/package.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: "coollabsio/coolify-realtime"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ labels: |
+ coolify.managed=true
+ aarch64:
+ runs-on: [ self-hosted, arm64 ]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ labels: |
+ coolify.managed=true
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [ amd64, aarch64 ]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Create & publish manifest
+ run: |
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
new file mode 100644
index 000000000..30910ae0b
--- /dev/null
+++ b/.github/workflows/coolify-realtime.yml
@@ -0,0 +1,103 @@
+name: Coolify Realtime (v4)
+
+on:
+ push:
+ branches: [ "main" ]
+ paths:
+ - .github/workflows/coolify-realtime.yml
+ - docker/coolify-realtime/Dockerfile
+ - docker/coolify-realtime/terminal-server.js
+ - docker/coolify-realtime/package.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: "coollabsio/coolify-realtime"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ labels: |
+ coolify.managed=true
+ aarch64:
+ runs-on: [ self-hosted, arm64 ]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ labels: |
+ coolify.managed=true
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [ amd64, aarch64 ]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Create & publish manifest
+ run: |
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
deleted file mode 100644
index 0edaa4f1c..000000000
--- a/.github/workflows/docker-image.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Docker Image CI
-
-on:
- # push:
- # branches: [ "main" ]
- # pull_request:
- # branches: [ "*" ]
- push:
- branches: ["this-does-not-exist"]
- pull_request:
- branches: ["this-does-not-exist"]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Cache Docker layers
- uses: actions/cache@v2
- with:
- path: |
- /usr/local/share/ca-certificates
- /var/cache/apt/archives
- /var/lib/apt/lists
- ~/.cache
- key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
- restore-keys: |
- ${{ runner.os }}-docker-
- - name: Build the Docker image
- run: |
- cp .env.example .env
- docker run --rm -u "$(id -u):$(id -g)" \
- -v "$(pwd):/app" \
- -w /app composer:2 \
- composer install --ignore-platform-reqs
- ./vendor/bin/spin build
- - name: Start the stack
- run: |
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec coolify php artisan key:generate
- ./vendor/bin/spin exec coolify php artisan migrate:fresh --seed
- - name: Test (missing E2E tests)
- run: |
- ./vendor/bin/spin exec coolify php artisan test
diff --git a/.github/workflows/fix-php-code-style-issues b/.github/workflows/fix-php-code-style-issues
deleted file mode 100644
index aebce91bc..000000000
--- a/.github/workflows/fix-php-code-style-issues
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Fix PHP code style issues
-
-on: [push]
-
-permissions:
- contents: write
-
-jobs:
- php-code-styling:
- runs-on: ubuntu-latest
- timeout-minutes: 5
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- ref: ${{ github.head_ref }}
-
- - name: Fix PHP code style issues
- uses: aglipanci/laravel-pint-action@2.4
-
- - name: Commit changes
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- commit_message: Fix styling
diff --git a/.github/workflows/lock-closed-issues-discussions-and-prs.yml b/.github/workflows/lock-closed-issues-discussions-and-prs.yml
new file mode 100644
index 000000000..d00853964
--- /dev/null
+++ b/.github/workflows/lock-closed-issues-discussions-and-prs.yml
@@ -0,0 +1,17 @@
+name: Lock closed Issues, Discussions, and PRs
+
+on:
+ schedule:
+ - cron: '0 1 * * *'
+
+jobs:
+ lock-threads:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Lock threads after 30 days of inactivity
+ uses: dessant/lock-threads@v5
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ issue-inactive-days: '30'
+ pr-inactive-days: '30'
+ discussion-inactive-days: '30'
diff --git a/.github/workflows/manage-stale-issues-and-prs.yml b/.github/workflows/manage-stale-issues-and-prs.yml
new file mode 100644
index 000000000..2afc996cb
--- /dev/null
+++ b/.github/workflows/manage-stale-issues-and-prs.yml
@@ -0,0 +1,28 @@
+name: Manage Stale Issues and PRs
+
+on:
+ schedule:
+ - cron: '0 2 * * *'
+
+jobs:
+ manage-stale:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Manage stale issues and PRs
+ uses: actions/stale@v9
+ id: stale
+ with:
+ stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
+ stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.'
+ close-issue-message: 'This issue has been automatically closed due to inactivity.'
+ close-pr-message: 'This pull request has been automatically closed due to inactivity.'
+ days-before-stale: 14
+ days-before-close: 7
+ stale-issue-label: '⏱︎ Stale'
+ stale-pr-label: '⏱︎ Stale'
+ only-labels: '💤 Waiting for feedback'
+ remove-stale-when-updated: true
+ operations-per-run: 100
+ labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback'
+ close-issue-reason: 'not_planned'
+ exempt-all-milestones: false
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
deleted file mode 100644
index cf2fae8f3..000000000
--- a/.github/workflows/pr-build.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-name: PR Build (v4)
-
-on:
- pull_request:
- types:
- - opened
- branches-ignore: ["main", "v3"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: "coollabsio/coolify"
-
-jobs:
- amd64:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/amd64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- aarch64:
- runs-on: [self-hosted, arm64]
- permissions:
- contents: read
- packages: write
- steps:
- - uses: actions/checkout@v4
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/aarch64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64
- merge-manifest:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- needs: [amd64, aarch64]
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
- run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- - uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
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/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/remove-labels-and-assignees-on-close.yml
new file mode 100644
index 000000000..ea097e328
--- /dev/null
+++ b/.github/workflows/remove-labels-and-assignees-on-close.yml
@@ -0,0 +1,78 @@
+name: Remove Labels and Assignees on Issue Close
+
+on:
+ issues:
+ types: [closed]
+ pull_request:
+ types: [closed]
+ pull_request_target:
+ types: [closed]
+
+jobs:
+ remove-labels-and-assignees:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Remove labels and assignees
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { owner, repo } = context.repo;
+
+ async function processIssue(issueNumber) {
+ try {
+ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
+ owner,
+ repo,
+ issue_number: issueNumber
+ });
+
+ const labelsToKeep = currentLabels
+ .filter(label => label.name === '⏱︎ Stale')
+ .map(label => label.name);
+
+ await github.rest.issues.setLabels({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ labels: labelsToKeep
+ });
+
+ const { data: issue } = await github.rest.issues.get({
+ owner,
+ repo,
+ issue_number: issueNumber
+ });
+
+ if (issue.assignees && issue.assignees.length > 0) {
+ await github.rest.issues.removeAssignees({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ assignees: issue.assignees.map(assignee => assignee.login)
+ });
+ }
+ } catch (error) {
+ if (error.status !== 404) {
+ console.error(`Error processing issue ${issueNumber}:`, error);
+ }
+ }
+ }
+
+ if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
+ const issue = context.payload.issue || context.payload.pull_request;
+ await processIssue(issue.number);
+ }
+
+ if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
+ const pr = context.payload.pull_request;
+ if (pr.body) {
+ const issueReferences = pr.body.match(/#(\d+)/g);
+ if (issueReferences) {
+ for (const reference of issueReferences) {
+ const issueNumber = parseInt(reference.substring(1));
+ await processIssue(issueNumber);
+ }
+ }
+ }
+ }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..80ec0614e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,243 @@
+# 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
+
+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. [Create a Pull Request](#7-create-a-pull-request)
+8. [Development Notes](#development-notes)
+9. [Resetting Development Environment](#resetting-development-environment)
+10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
+
+## 1. Setup Development Environment
+
+Follow the steps below for your operating system:
+
+
+Windows
+
+1. Install `docker-ce`, Docker Desktop (or similar):
+ - Docker CE (recommended):
+ - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
+ - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
+ - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
+ - Install Docker Desktop (easier):
+ - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
+ - Ensure WSL2 backend is enabled in Docker Desktop settings
+
+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?ref=coolify)
+
+
+
+
+MacOS
+
+1. Install Orbstack, Docker Desktop (or similar):
+ - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
+ - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
+ - Docker Desktop:
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
+
+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?ref=coolify)
+
+
+
+
+Linux
+
+1. Install Docker Engine, Docker Desktop (or similar):
+ - Docker Engine (recommended, as there is no VM overhead):
+ - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
+ - Docker Desktop:
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
+
+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?ref=coolify)
+
+
+
+## 2. Verify Installation (Optional)
+
+After installing Docker (or Orbstack) and Spin, verify the installation:
+
+1. Open a terminal or command prompt
+2. Run the following commands:
+ ```bash
+ docker --version
+ spin --version
+ ```
+ You should see version information for both Docker and Spin.
+
+## 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 (choose one):
+
+ | Editor | Platform | Download Link |
+ |--------|----------|---------------|
+ | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
+ | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
+ | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
+
+3. Clone the Coolify Repository from your fork to your local machine
+ - Use `git clone` in the command line, or
+ - Use GitHub Desktop (recommended):
+ - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
+ - Open GitHub Desktop and login with your GitHub account
+ - Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
+
+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.
+
+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.
+
+## 6. Start Development
+
+1. Access your Coolify instance:
+ - URL: `http://localhost:8000`
+ - Login: `test@example.com`
+ - Password: `password`
+
+2. Additional development tools:
+ | 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. Create a Pull Request
+
+1. After making changes or adding a new service:
+ - Commit your changes to your forked repository.
+ - Push the changes to your GitHub account.
+
+2. Creating the Pull Request (PR):
+ - Navigate to the main Coolify repository on GitHub.
+ - Click the "Pull requests" tab.
+ - Click the green "New pull request" button.
+ - Choose your fork and branch as the compare branch.
+ - Click "Create pull request".
+
+3. Filling out the PR details:
+ - Give your PR a descriptive title.
+ - Use the Pull Request Template provided and fill in the details.
+
+> [!IMPORTANT]
+> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
+
+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.
+
+## Development Notes
+
+When working on Coolify, keep the following in mind:
+
+1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
+ ```bash
+ docker exec -it coolify php artisan migrate
+ ```
+
+2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
+ ```bash
+ 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 environment-specific issues.
+
+> [!IMPORTANT]
+> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
+
+## Resetting Development Environment
+
+If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
+
+1. Stop all running containers `ctrl + c`.
+
+2. Remove all Coolify containers:
+ ```bash
+ docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
+ ```
+
+3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
+ ```bash
+ docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
+ ```
+
+4. Remove unused images:
+ ```bash
+ docker image prune -a
+ ```
+
+5. Start Coolify again:
+ ```bash
+ spin up
+ ```
+
+6. Run database migrations and seeders:
+ ```bash
+ docker exec -it coolify php artisan migrate:fresh --seed
+ ```
+
+After completing these steps, you'll have a fresh development setup.
+
+> [!IMPORTANT]
+> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
+
+## 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/CONTRIBUTION.md b/CONTRIBUTION.md
deleted file mode 100644
index 02a21573c..000000000
--- a/CONTRIBUTION.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Contributing
-
-> "First, thanks for considering to contribute 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 `#contribution` channel.
-
-## Code Contribution
-
-### 1) Setup your development environment
-
-- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
-- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
-
-### 2) Set your environment variables
-
-- Copy [.env.development.example](./.env.development.example) to .env.
-
-## 3) Start & setup Coolify
-
-- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- - If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
-
-### 4) Start development
-You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
-
-Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
-
-Mails are caught by Mailpit: `localhost:8025`
-
-## New Service Contribution
-Check out the docs [here](https://coolify.io/docs/knowledge-base/add-a-service).
-
diff --git a/README.md b/README.md
index ce6289d48..14a741088 100644
--- a/README.md
+++ b/README.md
@@ -35,20 +35,32 @@ Thank you so much!
Special thanks to our biggest sponsors!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+### Special Sponsors
+
+
+
+* [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+)
@@ -71,8 +83,11 @@ Special thanks to our biggest sponsors!
+
+
+
## Organizations
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 000000000..d9f05f17d
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,130 @@
+# 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.
+
+## Table of Contents
+- [Release Process](#release-process)
+- [Version Types](#version-types)
+ - [Stable](#stable)
+ - [Nightly](#nightly)
+ - [Beta](#beta)
+- [Version Availability](#version-availability)
+ - [Self-Hosted](#self-hosted)
+ - [Cloud](#cloud)
+- [Manually Update to Specific Versions](#manually-update-to-specific-versions)
+
+## Release Process
+
+1. **Development on `next` or Feature Branches**
+ - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
+
+2. **Merging to `main`**
+ - Once ready, changes are merged from the `next` branch into the `main` branch.
+
+3. **Building the Release**
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
+
+4. **Creating a GitHub Release**
+ - A new GitHub release is manually created with details of the changes made in the version.
+
+5. **Updating the CDN**
+ - To make a new version publicly available, the version information on the CDN needs to be updated: [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 take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
+
+## Version Types
+
+
+ Stable (coming soon)
+
+- **Stable**
+ - The production version suitable for stable, production environments (generally recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
+ - **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+
+ Nightly
+
+- **Nightly**
+ - The latest development version, suitable for testing the latest changes and experimenting with new features.
+ - **Update Frequency:** Daily or bi-weekly updates.
+ - **Release Size:** Smaller, more frequent releases.
+ - **Versioning Scheme:** TO BE DETERMINED
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
+ ```
+
+
+
+
+ Beta
+
+- **Beta**
+ - Test releases for the upcoming stable version.
+ - **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
+ - **Update Frequency:** Available if we think beta testing is necessary.
+ - **Release Size:** Same size as stable release as it will become the next stabe release after some time.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+> [!WARNING]
+> Do not use nightly/beta builds in production as there is no guarantee of stability.
+
+## Version Availability
+
+When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types.
+
+### Self-Hosted
+
+- **Update Frequency:** More frequent updates, especially on the nightly release channel.
+- **Update Availability:** New versions are available once the CDN has been updated.
+- **Update Methods:**
+ 1. **Manual Update in Instance Settings:**
+ - Go to `Settings > Update Check Frequency` and click the `Check Manually` button.
+ - If an update is available, an upgrade button will appear on the sidebar.
+ 2. **Automatic Update:**
+ - If enabled, the instance will update automatically at the time set in the settings.
+ 3. **Re-run Installation Script:**
+ - Run the installation script again to upgrade to the latest version available on the CDN:
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+> [!IMPORTANT]
+> If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release.
+
+### Cloud
+
+- **Update Frequency:** Less frequent as it's a managed service.
+- **Update Availability:** New versions are available once Andras has updated the cloud version manually.
+- **Update Method:**
+ - Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it.
+
+> [!IMPORTANT]
+> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
+
+## 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/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 7155f9a0a..61005845b 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -2,6 +2,7 @@
namespace App\Actions\Application;
+use App\Actions\Server\CleanupDocker;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,44 +10,35 @@ class StopApplication
{
use AsAction;
- public function handle(Application $application, bool $previewDeployments = false)
+ public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
- if ($application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
-
- return;
- }
-
- $servers = collect([]);
- $servers->push($application->destination->server);
- $application->additional_servers->map(function ($server) use ($servers) {
- $servers->push($server);
- });
- foreach ($servers as $server) {
+ try {
+ $server = $application->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- if ($previewDeployments) {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
- } else {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
- }
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerName = data_get($container, 'Names');
- if ($containerName) {
- instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
- }
- }
+ ray('Stopping application: '.$application->name);
+
+ if ($server->isSwarm()) {
+ instant_remote_process(["docker stack rm {$application->uuid}"], $server);
+
+ return;
}
+
+ $containersToStop = $application->getContainersToStop($previewDeployments);
+ $application->stopContainers($containersToStop, $server);
+
if ($application->build_pack === 'dockercompose') {
- // remove network
- $uuid = $application->uuid;
- instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
- instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ $application->delete_connected_networks($application->uuid);
}
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ } catch (\Exception $e) {
+ ray($e->getMessage());
+
+ return $e->getMessage();
}
}
}
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index 63e3afe2f..c691f52c0 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
+use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
@@ -137,7 +138,7 @@ class RunRemoteProcess
$command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail();
- return generateSshCommand($server, $command);
+ return SshMultiplexingHelper::generateSshCommand($server, $command);
}
protected function handleOutput(string $type, string $output)
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index e97c55930..6d0063749 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -79,14 +79,7 @@ class StartClickhouse
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +95,11 @@ class StartClickhouse
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -162,6 +160,8 @@ class StartClickhouse
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
}
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 862fc54fc..3ee46a2e1 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -46,9 +46,6 @@ class StartDragonfly
'networks' => [
$this->database->destination->network,
],
- 'ulimits' => [
- 'memlock' => '-1',
- ],
'labels' => [
'coolify.managed' => 'true',
],
@@ -79,14 +76,7 @@ class StartDragonfly
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +92,11 @@ class StartDragonfly
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 85cb89c1c..a11452a68 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -78,14 +78,7 @@ class StartKeydb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -110,6 +103,10 @@ class StartKeydb
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -166,6 +163,8 @@ class StartKeydb
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 33948192b..a5630f734 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -73,14 +73,7 @@ class StartMariadb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,6 +97,11 @@ class StartMariadb
'read_only' => true,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -171,6 +169,8 @@ class StartMariadb
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 911054f5e..5bff194d5 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -81,14 +81,7 @@ class StartMongodb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -121,6 +114,10 @@ class StartMongodb
'read_only' => true,
];
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -185,6 +182,8 @@ class StartMongodb
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index b55d9dead..cc4203580 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -73,14 +73,7 @@ class StartMysql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,6 +97,11 @@ class StartMysql
'read_only' => true,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -171,6 +169,8 @@ class StartMysql
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 909f4c893..2a8e5476c 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -80,14 +80,7 @@ class StartPostgresql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -126,6 +119,10 @@ class StartPostgresql
'config_file=/etc/postgresql/postgresql.conf',
];
}
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -193,6 +190,8 @@ class StartPostgresql
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index f10afef5e..eeddab924 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -82,14 +82,7 @@ class StartRedis
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -114,6 +107,11 @@ class StartRedis
];
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -170,6 +168,8 @@ class StartRedis
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index d562ec56f..e4cea7cee 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
+use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
- public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
+ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
+ $this->stopContainer($database, $database->uuid, 300);
+ if (! $isDeleteOperation) {
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ }
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
+
+ return 'Database stopped successfully';
+ }
+
+ private function stopContainer($database, string $containerName, int $timeout = 300): void
+ {
+ $server = $database->destination->server;
+
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName, $server);
+ break;
+ }
+ usleep(100000);
+ }
+
+ $this->removeContainer($containerName, $server);
+ }
+
+ private function forceStopContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ private function deleteConnectedNetworks($uuid, $server)
+ {
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index fdaa88ebf..ed563eaae 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -543,7 +543,7 @@ class GetContainersStatus
}
}
}
- $exitedServices = $exitedServices->unique('id');
+ $exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
@@ -651,8 +651,9 @@ class GetContainersStatus
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
- // Check if proxy is running
- $this->server->proxyType();
+ if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
+ return;
+ }
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index f8882d12a..481757162 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
*/
public function create(array $input): User
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403);
}
@@ -48,7 +47,7 @@ class CreateNewUser implements CreatesNewUsers
$team = $user->teams()->first();
// Disable registration after first user is created
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$settings->is_registration_enabled = false;
$settings->save();
} else {
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
index dcb4058c0..55af1a8c0 100644
--- a/app/Actions/License/CheckResaleLicense.php
+++ b/app/Actions/License/CheckResaleLicense.php
@@ -2,7 +2,6 @@
namespace App\Actions\License;
-use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,7 +12,7 @@ class CheckResaleLicense
public function handle()
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php
index f4fe650c5..bdeafd061 100644
--- a/app/Actions/Proxy/CheckConfiguration.php
+++ b/app/Actions/Proxy/CheckConfiguration.php
@@ -22,7 +22,7 @@ class CheckConfiguration
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
- $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
+ $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
}
if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration');
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 735b972af..03a0beddf 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -2,14 +2,17 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
+use Symfony\Component\Yaml\Yaml;
class CheckProxy
{
use AsAction;
- public function handle(Server $server, $fromUI = false)
+ // It should return if the proxy should be started (true) or not (false)
+ public function handle(Server $server, $fromUI = false): bool
{
if (! $server->isFunctional()) {
return false;
@@ -26,7 +29,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
if (! $uptime) {
throw new \Exception($error);
}
@@ -62,22 +65,42 @@ class CheckProxy
$ip = 'host.docker.internal';
}
- $connection80 = @fsockopen($ip, '80');
- $connection443 = @fsockopen($ip, '443');
- $port80 = is_resource($connection80) && fclose($connection80);
- $port443 = is_resource($connection443) && fclose($connection443);
- if ($port80) {
- if ($fromUI) {
- throw new \Exception("Port 80 is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord ");
+ $portsToCheck = ['80', '443'];
+
+ try {
+ if ($server->proxyType() !== ProxyTypes::NONE->value) {
+ $proxyCompose = CheckConfiguration::run($server);
+ if (isset($proxyCompose)) {
+ $yaml = Yaml::parse($proxyCompose);
+ $portsToCheck = [];
+ if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
+ $ports = data_get($yaml, 'services.traefik.ports');
+ } elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
+ $ports = data_get($yaml, 'services.caddy.ports');
+ }
+ if (isset($ports)) {
+ foreach ($ports as $port) {
+ $portsToCheck[] = str($port)->before(':')->value();
+ }
+ }
+ }
} else {
- return false;
+ $portsToCheck = [];
}
+ } catch (\Exception $e) {
+ ray($e->getMessage());
}
- if ($port443) {
- if ($fromUI) {
- throw new \Exception("Port 443 is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord ");
- } else {
- return false;
+ if (count($portsToCheck) === 0) {
+ return false;
+ }
+ foreach ($portsToCheck as $port) {
+ $connection = @fsockopen($ip, $port);
+ if (is_resource($connection) && fclose($connection)) {
+ if ($fromUI) {
+ throw new \Exception("Port $port is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord ");
+ } else {
+ return false;
+ }
}
}
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 991c94b11..f20c10123 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -26,7 +26,7 @@ class StartProxy
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
- $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
+ $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save();
if ($server->isSwarm()) {
$commands = $commands->merge([
@@ -35,7 +35,7 @@ class StartProxy
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
@@ -46,11 +46,14 @@ class StartProxy
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
- "echo 'Stopping existing coolify-proxy.'",
- 'docker compose down -v --remove-orphans > /dev/null 2>&1',
+ 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
+ " echo 'Stopping and removing existing coolify-proxy.'",
+ ' docker rm -f coolify-proxy || true',
+ " echo 'Successfully stopped and removed existing coolify-proxy.'",
+ 'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 0009e001d..dc6ac12bf 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -9,17 +9,31 @@ class CleanupDocker
{
use AsAction;
- public function handle(Server $server, bool $force = true)
+ public function handle(Server $server)
{
- // cleanup docker images, containers, and builder caches
- if ($force) {
- instant_remote_process(['docker image prune -af'], $server, false);
- instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
- instant_remote_process(['docker builder prune -af'], $server, false);
- } else {
- instant_remote_process(['docker image prune -f'], $server, false);
- instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
- instant_remote_process(['docker builder prune -f'], $server, false);
+ $settings = instanceSettings();
+ $helperImageVersion = data_get($settings, 'helper_version');
+ $helperImage = config('coolify.helper_image');
+ $helperImageWithVersion = "$helperImage:$helperImageVersion";
+
+ $commands = [
+ 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
+ 'docker image prune -af --filter "label!=coolify.managed=true"',
+ 'docker builder prune -af',
+ "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
+ ];
+
+ $serverSettings = $server->settings;
+ if ($serverSettings->delete_unused_volumes) {
+ $commands[] = 'docker volume prune -af';
+ }
+
+ if ($serverSettings->delete_unused_networks) {
+ $commands[] = 'docker network prune -f';
+ }
+
+ foreach ($commands as $command) {
+ instant_remote_process([$command], $server, false);
}
}
}
diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php
index 3946afe95..0d36e8863 100644
--- a/app/Actions/Server/ConfigureCloudflared.php
+++ b/app/Actions/Server/ConfigureCloudflared.php
@@ -2,6 +2,7 @@
namespace App\Actions\Server;
+use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
+ $server->settings->is_cloudflare_tunnel = false;
+ $server->settings->save();
throw $e;
} finally {
+ CloudflareTunnelConfigured::dispatch($server->team_id);
+
$commands = collect([
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
+
}
}
}
diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php
index 034d89fe7..9b6741211 100644
--- a/app/Actions/Server/InstallLogDrain.php
+++ b/app/Actions/Server/InstallLogDrain.php
@@ -47,7 +47,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
- Set server_name {$server->name}
+ Set coolify.server_name {$server->name}
+ Rename COOLIFY_APP_NAME coolify.app_name
+ Rename COOLIFY_PROJECT_NAME coolify.project_name
+ Rename COOLIFY_SERVER_IP coolify.server_ip
+ Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name nrlogs
Match *
@@ -98,7 +102,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
- Set server_name {$server->name}
+ Set coolify.server_name {$server->name}
+ Rename COOLIFY_APP_NAME coolify.app_name
+ Rename COOLIFY_PROJECT_NAME coolify.project_name
+ Rename COOLIFY_SERVER_IP coolify.server_ip
+ Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name http
Match *
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 8910d6e97..30664df26 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -2,10 +2,8 @@
namespace App\Actions\Server;
-use App\Models\InstanceSettings;
+use App\Jobs\PullHelperImageJob;
use App\Models\Server;
-use Illuminate\Support\Facades\File;
-use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -21,17 +19,12 @@ class UpdateCoolify
public function handle($manual_update = false)
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
}
- CleanupDocker::dispatch($this->server, false)->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));
- }
+ CleanupDocker::dispatch($this->server)->onQueue('high');
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if (! $manual_update) {
@@ -62,10 +55,18 @@ class UpdateCoolify
return;
}
+
+ $all_servers = Server::all();
+ $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+
+ 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/DeleteService.php b/app/Actions/Service/DeleteService.php
index 194cf4db9..f28e5490e 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -2,6 +2,7 @@
namespace App\Actions\Service;
+use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,11 +10,11 @@ class DeleteService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{
try {
$server = data_get($service, 'server');
- if ($server->isFunctional()) {
+ if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
- $commands[] = "docker rm -f $service->uuid";
- instant_remote_process($commands, $server, false);
+ // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
+ if (! empty($commands)) {
+ foreach ($commands as $command) {
+ $result = instant_remote_process([$command], $server, false);
+ if ($result !== 0) {
+ ray("Failed to execute: $command");
+ }
+ }
+ }
}
+
+ if ($deleteConnectedNetworks) {
+ $service->delete_connected_networks($service->uuid);
+ }
+
+ instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
+ if ($deleteConfigurations) {
+ $service->delete_configurations();
+ }
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
@@ -50,6 +67,11 @@ class DeleteService
$task->delete();
}
$service->tags()->detach();
+ $service->forceDelete();
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
}
}
}
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 4b6a25dcc..06d2e0efb 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';
@@ -29,7 +31,7 @@ class StartService
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
- $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
+ $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 3dd91b4e2..5c7bbc2aa 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -2,6 +2,7 @@
namespace App\Actions\Service;
+use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,35 +10,27 @@ class StopService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
$server = $service->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- ray('Stopping service: '.$service->name);
- $applications = $service->applications()->get();
- foreach ($applications as $application) {
- instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- $application->update(['status' => 'exited']);
+
+ $containersToStop = $service->getContainersToStop();
+ $service->stopContainers($containersToStop, $server);
+
+ if (! $isDeleteOperation) {
+ $service->delete_connected_networks($service->uuid);
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
}
- $dbs = $service->databases()->get();
- foreach ($dbs as $db) {
- instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- $db->update(['status' => 'exited']);
- }
- instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
- instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
-
}
}
diff --git a/app/Console/Commands/CheckApplicationDeploymentQueue.php b/app/Console/Commands/CheckApplicationDeploymentQueue.php
new file mode 100644
index 000000000..e89d26f2c
--- /dev/null
+++ b/app/Console/Commands/CheckApplicationDeploymentQueue.php
@@ -0,0 +1,50 @@
+option('seconds');
+ $deployments = ApplicationDeploymentQueue::whereIn('status', [
+ ApplicationDeploymentStatus::IN_PROGRESS,
+ ApplicationDeploymentStatus::QUEUED,
+ ])->where('created_at', '<=', now()->subSeconds($seconds))->get();
+ if ($deployments->isEmpty()) {
+ $this->info('No deployments found in the last '.$seconds.' seconds.');
+
+ return;
+ }
+
+ $this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
+
+ foreach ($deployments as $deployment) {
+ if ($this->option('force')) {
+ $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
+ $this->cancelDeployment($deployment);
+ } else {
+ $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
+ if ($this->confirm('Do you want to cancel this deployment?', true)) {
+ $this->cancelDeployment($deployment);
+ }
+ }
+ }
+ }
+
+ private function cancelDeployment(ApplicationDeploymentQueue $deployment)
+ {
+ $deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
+ if ($deployment->server?->isFunctional()) {
+ remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
+ }
+ }
+}
diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php
index f068e3eb2..3aae28ae6 100644
--- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php
+++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php
@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command
{
- protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
+ protected $signature = 'cleanup:deployment-queue {--team-id=}';
- protected $description = 'CleanupApplicationDeploymentQueue';
+ protected $description = 'Cleanup application deployment queue.';
public function handle()
{
diff --git a/app/Console/Commands/CleanupQueue.php b/app/Console/Commands/CleanupQueue.php
deleted file mode 100644
index fd2b637ac..000000000
--- a/app/Console/Commands/CleanupQueue.php
+++ /dev/null
@@ -1,24 +0,0 @@
-keys('*:laravel*');
- foreach ($keys as $key) {
- $keyWithoutPrefix = str_replace($prefix, '', $key);
- Redis::connection()->del($keyWithoutPrefix);
- }
- }
-}
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
new file mode 100644
index 000000000..ed0740d34
--- /dev/null
+++ b/app/Console/Commands/CleanupRedis.php
@@ -0,0 +1,31 @@
+keys('*:laravel*');
+ collect($keys)->each(function ($key) use ($prefix) {
+ $keyWithoutPrefix = str_replace($prefix, '', $key);
+ Redis::connection()->del($keyWithoutPrefix);
+ });
+
+ $queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
+ collect($queueOverlaps)->each(function ($key) {
+ Redis::connection()->del($key);
+ });
+
+ }
+}
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index fbbf2c820..66c25ec27 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -2,8 +2,13 @@
namespace App\Console\Commands;
+use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
+use App\Models\ApplicationDeploymentQueue;
+use App\Models\ApplicationPreview;
+use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
+use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -33,6 +38,27 @@ class CleanupStuckedResources extends Command
private function cleanup_stucked_resources()
{
+ try {
+ $servers = Server::all()->filter(function ($server) {
+ return $server->isFunctional();
+ });
+ foreach ($servers as $server) {
+ CleanupHelperContainersJob::dispatch($server);
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
+ }
+ try {
+ $applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
+ foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
+ if (is_null($applicationDeploymentQueue->application)) {
+ echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
+ $applicationDeploymentQueue->delete();
+ }
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
+ }
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
@@ -42,6 +68,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
+ try {
+ $applicationsPreviews = ApplicationPreview::get();
+ foreach ($applicationsPreviews as $applicationPreview) {
+ if (! data_get($applicationPreview, 'application')) {
+ echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
+ $applicationPreview->delete();
+ }
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stuck application: {$e->getMessage()}\n";
+ }
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
@@ -153,6 +190,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/Commands/Dev.php b/app/Console/Commands/Dev.php
index 964b8e46e..20a2667c3 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -48,6 +48,13 @@ class Dev extends Command
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
+
+ // Generate STORAGE link if not exists
+ if (! file_exists(public_path('storage'))) {
+ echo "Generating STORAGE link.\n";
+ Artisan::call('storage:link');
+ }
+
// Seed database if it's empty
$settings = InstanceSettings::find(0);
if (! $settings) {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 30d761a10..ad7bff86d 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -5,10 +5,8 @@ namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
-use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
@@ -18,7 +16,7 @@ use Illuminate\Support\Facades\Http;
class Init extends Command
{
- protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
+ protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +24,63 @@ class Init extends Command
public function handle()
{
+ if (isCloud() && ! $this->option('force-cloud')) {
+ echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
+
+ return;
+ }
+
$this->servers = Server::all();
- $this->alive();
- get_public_ips();
+ if (isCloud()) {
+
+ } else {
+ $this->send_alive_signal();
+ get_public_ips();
+ }
+
+ // Backward compatibility
+ $this->disable_metrics();
+ $this->replace_slash_in_environment_name();
+ $this->restore_coolify_db_backup();
+ //
+ $this->update_traefik_labels();
+ if (! isCloud() || $this->option('force-cloud')) {
+ $this->cleanup_unused_network_from_coolify_proxy();
+ }
+ if (isCloud()) {
+ $this->cleanup_unnecessary_dynamic_proxy_configuration();
+ } else {
+ $this->cleanup_in_progress_application_deployments();
+ }
+ $this->call('cleanup:redis');
+ $this->call('cleanup:stucked-resources');
+
+ if (isCloud()) {
+ $response = Http::retry(3, 1000)->get(config('constants.services.official'));
+ if ($response->successful()) {
+ $services = $response->json();
+ File::put(base_path('templates/service-templates.json'), json_encode($services));
+ }
+ } else {
+ try {
+ $localhost = $this->servers->where('id', 0)->first();
+ $localhost->setupDynamicProxyConfiguration();
+ } catch (\Throwable $e) {
+ echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
+ }
+ $settings = instanceSettings();
+ if (! is_null(env('AUTOUPDATE', null))) {
+ if (env('AUTOUPDATE') == true) {
+ $settings->update(['is_auto_update_enabled' => true]);
+ } else {
+ $settings->update(['is_auto_update_enabled' => false]);
+ }
+ }
+ }
+ }
+
+ private function disable_metrics()
+ {
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +91,6 @@ class Init extends Command
}
}
}
-
- $full_cleanup = $this->option('full-cleanup');
- $cleanup_deployments = $this->option('cleanup-deployments');
- $cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
- $this->replace_slash_in_environment_name();
- if ($cleanup_deployments) {
- echo "Running cleanup deployments.\n";
- $this->cleanup_in_progress_application_deployments();
-
- return;
- }
- if ($cleanup_proxy_networks) {
- echo "Running cleanup proxy networks.\n";
- $this->cleanup_unused_network_from_coolify_proxy();
-
- return;
- }
- if ($full_cleanup) {
- // Required for falsely deleted coolify db
- $this->restore_coolify_db_backup();
- $this->update_traefik_labels();
- $this->cleanup_unused_network_from_coolify_proxy();
- $this->cleanup_unnecessary_dynamic_proxy_configuration();
- $this->cleanup_in_progress_application_deployments();
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:queue');
- $this->call('cleanup:stucked-resources');
- if (! isCloud()) {
- try {
- $localhost = $this->servers->where('id', 0)->first();
- $localhost->setupDynamicProxyConfiguration();
- } catch (\Throwable $e) {
- echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
- }
- }
-
- $settings = InstanceSettings::get();
- if (! is_null(env('AUTOUPDATE', null))) {
- if (env('AUTOUPDATE') == true) {
- $settings->update(['is_auto_update_enabled' => true]);
- } else {
- $settings->update(['is_auto_update_enabled' => false]);
- }
- }
- if (isCloud()) {
- $response = Http::retry(3, 1000)->get(config('constants.services.official'));
- if ($response->successful()) {
- $services = $response->json();
- File::put(base_path('templates/service-templates.json'), json_encode($services));
- }
- }
-
- return;
- }
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
@@ -108,25 +104,23 @@ class Init extends Command
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
- if (isCloud()) {
- foreach ($this->servers as $server) {
- try {
- if (! $server->isFunctional()) {
- continue;
- }
- if ($server->id === 0) {
- continue;
- }
- $file = $server->proxyPath().'/dynamic/coolify.yaml';
-
- return instant_remote_process([
- "rm -f $file",
- ], $server, false);
- } catch (\Throwable $e) {
- echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
+ foreach ($this->servers as $server) {
+ try {
+ if (! $server->isFunctional()) {
+ continue;
}
+ if ($server->id === 0) {
+ continue;
+ }
+ $file = $server->proxyPath().'/dynamic/coolify.yaml';
+ return instant_remote_process([
+ "rm -f $file",
+ ], $server, false);
+ } catch (\Throwable $e) {
+ echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
+
}
}
@@ -172,43 +166,36 @@ class Init extends Command
private function restore_coolify_db_backup()
{
- try {
- $database = StandalonePostgresql::withTrashed()->find(0);
- if ($database && $database->trashed()) {
- echo "Restoring coolify db backup\n";
- $database->restore();
- $scheduledBackup = ScheduledDatabaseBackup::find(0);
- if (! $scheduledBackup) {
- ScheduledDatabaseBackup::create([
- 'id' => 0,
- 'enabled' => true,
- 'save_s3' => false,
- 'frequency' => '0 0 * * *',
- 'database_id' => $database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
- 'team_id' => 0,
- ]);
+ if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ try {
+ $database = StandalonePostgresql::withTrashed()->find(0);
+ if ($database && $database->trashed()) {
+ echo "Restoring coolify db backup\n";
+ $database->restore();
+ $scheduledBackup = ScheduledDatabaseBackup::find(0);
+ if (! $scheduledBackup) {
+ ScheduledDatabaseBackup::create([
+ 'id' => 0,
+ 'enabled' => true,
+ 'save_s3' => false,
+ 'frequency' => '0 0 * * *',
+ 'database_id' => $database->id,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'team_id' => 0,
+ ]);
+ }
}
- }
- } catch (\Throwable $e) {
- echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
- }
- }
-
- private function cleanup_stucked_helper_containers()
- {
- foreach ($this->servers as $server) {
- if ($server->isFunctional()) {
- CleanupHelperContainersJob::dispatch($server);
+ } catch (\Throwable $e) {
+ echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
}
- private function alive()
+ private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
@@ -222,23 +209,7 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n";
}
}
- // private function cleanup_ssh()
- // {
- // TODO: it will cleanup id.root@host.docker.internal
- // try {
- // $files = Storage::allFiles('ssh/keys');
- // foreach ($files as $file) {
- // Storage::delete($file);
- // }
- // $files = Storage::allFiles('ssh/mux');
- // foreach ($files as $file) {
- // Storage::delete($file);
- // }
- // } catch (\Throwable $e) {
- // echo "Error in cleaning ssh: {$e->getMessage()}\n";
- // }
- // }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -260,11 +231,13 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
- $environments = Environment::all();
- foreach ($environments as $environment) {
- if (str_contains($environment->name, '/')) {
- $environment->name = str_replace('/', '-', $environment->name);
- $environment->save();
+ if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ $environments = Environment::all();
+ foreach ($environments as $environment) {
+ if (str_contains($environment->name, '/')) {
+ $environment->name = str_replace('/', '-', $environment->name);
+ $environment->save();
+ }
}
}
}
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
new file mode 100644
index 000000000..e8d73ef47
--- /dev/null
+++ b/app/Console/Commands/OpenApi.php
@@ -0,0 +1,26 @@
+errorOutput();
+ $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
+ $error = preg_replace('/^\h*\v+/m', '', $error);
+ echo $error;
+ echo $process->output();
+
+ }
+}
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index de64afefa..da04cefce 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -78,7 +78,7 @@ class ServicesGenerate extends Command
if ($logo->count() > 0) {
$logo = str($logo[0])->after('# logo:')->trim()->value();
} else {
- $logo = 'svgs/unknown.svg';
+ $logo = 'svgs/coolify.png';
}
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
if ($minversion->count() > 0) {
diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php
index 7135cfc9c..228467f88 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -16,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
- protected $signature = 'sync:bunny {--templates} {--release}';
+ protected $signature = 'sync:bunny {--templates} {--release} {--nightly}';
/**
* The console command description.
@@ -33,6 +33,7 @@ class SyncBunny extends Command
$that = $this;
$only_template = $this->option('templates');
$only_version = $this->option('release');
+ $nightly = $this->option('nightly');
$bunny_cdn = 'https://cdn.coollabs.io';
$bunny_cdn_path = 'coolify';
$bunny_cdn_storage_name = 'coolcdn';
@@ -45,9 +46,15 @@ class SyncBunny extends Command
$upgrade_script = 'upgrade.sh';
$production_env = '.env.production';
$service_template = 'service-templates.json';
-
$versions = 'versions.json';
+ $compose_file_location = "$parent_dir/$compose_file";
+ $compose_file_prod_location = "$parent_dir/$compose_file_prod";
+ $install_script_location = "$parent_dir/scripts/install.sh";
+ $upgrade_script_location = "$parent_dir/scripts/upgrade.sh";
+ $production_env_location = "$parent_dir/.env.production";
+ $versions_location = "$parent_dir/$versions";
+
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
@@ -73,8 +80,26 @@ class SyncBunny extends Command
]);
});
try {
+ if ($nightly) {
+ $bunny_cdn_path = 'coolify-nightly';
+
+ $compose_file_location = "$parent_dir/other/nightly/$compose_file";
+ $compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod";
+ $production_env_location = "$parent_dir/other/nightly/$production_env";
+ $upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script";
+ $install_script_location = "$parent_dir/other/nightly/$install_script";
+ $versions_location = "$parent_dir/other/nightly/$versions";
+ }
if (! $only_template && ! $only_version) {
- $this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ if ($nightly) {
+ $this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ } else {
+ $this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ }
+ $confirmed = confirm('Are you sure you want to sync?');
+ if (! $confirmed) {
+ return;
+ }
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
@@ -90,8 +115,12 @@ class SyncBunny extends Command
return;
} elseif ($only_version) {
- $this->info('About to sync versions.json to BunnyCDN.');
- $file = file_get_contents("$parent_dir/$versions");
+ if ($nightly) {
+ $this->info('About to sync NIGHLTY versions.json to BunnyCDN.');
+ } else {
+ $this->info('About to sync PRODUCTION versions.json to BunnyCDN.');
+ }
+ $file = file_get_contents($versions_location);
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
@@ -100,7 +129,7 @@ class SyncBunny extends Command
return;
}
Http::pool(fn (Pool $pool) => [
- $pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
+ $pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
@@ -109,11 +138,11 @@ class SyncBunny extends Command
}
Http::pool(fn (Pool $pool) => [
- $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
- $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
- $pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
- $pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
- $pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
+ $pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
+ $pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
+ $pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
+ $pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
+ $pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index e8f213b16..1430fcdd1 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -4,16 +4,16 @@ 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;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
+use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
@@ -28,24 +28,28 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
+
+ $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
-
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
+
+ $schedule->command('telescope:prune')->daily();
+
+ $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily();
- $schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
+ $schedule->command('cleanup:unreachable-servers')->daily()->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);
@@ -62,26 +66,42 @@ class Kernel extends ConsoleKernel
private function pull_images($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
- $schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
+ $schedule->job(function () use ($server) {
+ $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
+ $sentinel_found = json_decode($sentinel_found, true);
+ $status = data_get($sentinel_found, '0.State.Status', 'exited');
+ if ($status !== 'running') {
+ PullSentinelImageJob::dispatch($server);
+ }
+ })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
- $schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
}
+ $schedule->job(new PullHelperImageJob)
+ ->cron($settings->update_check_frequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
}
private function schedule_updates($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$updateCheckFrequency = $settings->update_check_frequency;
- $schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
+ $schedule->job(new CheckForUpdatesJob)
+ ->cron($updateCheckFrequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
if ($settings->is_auto_update_enabled) {
$autoUpdateFrequency = $settings->auto_update_frequency;
- $schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
+ $schedule->job(new UpdateCoolifyJob)
+ ->cron($autoUpdateFrequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
}
}
@@ -96,7 +116,13 @@ class Kernel extends ConsoleKernel
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
- $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
+ // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
+ $serverTimezone = $server->settings->server_timezone;
+ if ($server->settings->force_docker_cleanup) {
+ $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
+ } else {
+ $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ }
}
}
@@ -117,12 +143,19 @@ class Kernel extends ConsoleKernel
continue;
}
+ $server = $scheduled_backup->server();
+
+ if (! $server) {
+ continue;
+ }
+ $serverTimezone = $server->settings->server_timezone;
+
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
- ))->cron($scheduled_backup->frequency)->onOneServer();
+ ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
}
}
@@ -155,12 +188,19 @@ class Kernel extends ConsoleKernel
continue;
}
}
+
+ $server = $scheduled_task->server();
+ if (! $server) {
+ continue;
+ }
+ $serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
+
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
task: $scheduled_task
- ))->cron($scheduled_task->frequency)->onOneServer();
+ ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
}
}
diff --git a/app/Enums/ContainerStatusTypes.php b/app/Enums/ContainerStatusTypes.php
new file mode 100644
index 000000000..ffcb6d5b5
--- /dev/null
+++ b/app/Enums/ContainerStatusTypes.php
@@ -0,0 +1,14 @@
+user()->currentTeam()->id ?? null;
+ }
+ if (is_null($teamId)) {
+ throw new \Exception('Team id is null');
+ }
+ $this->teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 6b69350fe..63fbfc862 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) {
return;
}
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
if ($this->settings->do_not_track) {
return;
}
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
new file mode 100644
index 000000000..1a2146799
--- /dev/null
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -0,0 +1,186 @@
+private_key_id);
+ $sshKeyLocation = $privateKey->getKeyLocation();
+ $muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
+
+ return [
+ 'sshKeyLocation' => $sshKeyLocation,
+ 'muxFilename' => $muxFilename,
+ ];
+ }
+
+ public static function ensureMultiplexedConnection(Server $server)
+ {
+ if (! self::isMultiplexingEnabled()) {
+ return;
+ }
+
+ $sshConfig = self::serverSshConfiguration($server);
+ $muxSocket = $sshConfig['muxFilename'];
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+
+ self::validateSshKey($sshKeyLocation);
+
+ $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+ $checkCommand .= "{$server->user}@{$server->ip}";
+ $process = Process::run($checkCommand);
+
+ if ($process->exitCode() !== 0) {
+ self::establishNewMultiplexedConnection($server);
+ }
+ }
+
+ public static function establishNewMultiplexedConnection(Server $server)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $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="cloudflared access ssh --hostname %h" ';
+ }
+
+ $establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
+ $establishCommand .= "{$server->user}@{$server->ip}";
+
+ $establishProcess = Process::run($establishCommand);
+
+ if ($establishProcess->exitCode() !== 0) {
+ throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
+ }
+ }
+
+ public static function removeMuxFile(Server $server)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+ $closeCommand .= "{$server->user}@{$server->ip}";
+ Process::run($closeCommand);
+ }
+
+ public static function generateScpCommand(Server $server, string $source, string $dest)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $timeout = config('constants.ssh.command_timeout');
+ $muxPersistTime = config('constants.ssh.mux_persist_time');
+
+ $scp_command = "timeout $timeout scp ";
+ if ($server->isIpv6()) {
+ $scp_command .= '-6 ';
+ }
+ if (self::isMultiplexingEnabled()) {
+ $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
+ self::ensureMultiplexedConnection($server);
+ }
+
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+
+ $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
+ $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
+
+ return $scp_command;
+ }
+
+ public static function generateSshCommand(Server $server, string $command)
+ {
+ if ($server->settings->force_disabled) {
+ throw new \RuntimeException('Server is disabled.');
+ }
+
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $timeout = config('constants.ssh.command_timeout');
+ $muxPersistTime = config('constants.ssh.mux_persist_time');
+
+ $ssh_command = "timeout $timeout ssh ";
+
+ if (self::isMultiplexingEnabled()) {
+ $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
+ self::ensureMultiplexedConnection($server);
+ }
+
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
+ }
+
+ $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
+
+ $delimiter = Hash::make($command);
+ $delimiter = base64_encode($delimiter);
+ $command = str_replace($delimiter, '', $command);
+
+ $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
+ .$command.PHP_EOL
+ .$delimiter;
+
+ return $ssh_command;
+ }
+
+ private static function isMultiplexingEnabled(): bool
+ {
+ return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
+ }
+
+ private static function validateSshKey(string $sshKeyLocation): void
+ {
+ $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
+ $keyCheckProcess = Process::run($checkKeyCommand);
+
+ if ($keyCheckProcess->exitCode() !== 0) {
+ throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
+ }
+ }
+
+ private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
+ {
+ $options = "-i {$sshKeyLocation} "
+ .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
+ .'-o PasswordAuthentication=no '
+ ."-o ConnectTimeout=$connectionTimeout "
+ ."-o ServerAliveInterval=$serverInterval "
+ .'-o RequestTTY=no '
+ .'-o LogLevel=ERROR ';
+
+ // Bruh
+ if ($isScp) {
+ $options .= "-P {$server->port} ";
+ } else {
+ $options .= "-p {$server->port} ";
+ }
+
+ return $options;
+ }
+}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index cd4d724b4..eef179048 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
summary: 'List',
description: 'List all applications.',
path: '/applications',
+ operationId: 'list-applications',
security: [
['bearerAuth' => []],
],
@@ -101,6 +102,7 @@ class ApplicationsController extends Controller
summary: 'Create (Public)',
description: 'Create new application based on a public git repository.',
path: '/applications/public',
+ operationId: 'create-public-application',
security: [
['bearerAuth' => []],
],
@@ -175,6 +177,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -201,7 +204,8 @@ class ApplicationsController extends Controller
#[OA\Post(
summary: 'Create (Private - GH App)',
description: 'Create new application based on a private repository through a Github App.',
- path: '/applications/private-gh-app',
+ path: '/applications/private-github-app',
+ operationId: 'create-private-github-app-application',
security: [
['bearerAuth' => []],
],
@@ -276,6 +280,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -303,6 +308,7 @@ class ApplicationsController extends Controller
summary: 'Create (Private - Deploy Key)',
description: 'Create new application based on a private repository through a Deploy Key.',
path: '/applications/private-deploy-key',
+ operationId: 'create-private-deploy-key-application',
security: [
['bearerAuth' => []],
],
@@ -377,6 +383,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -404,6 +411,7 @@ class ApplicationsController extends Controller
summary: 'Create (Dockerfile)',
description: 'Create new application based on a simple Dockerfile.',
path: '/applications/dockerfile',
+ operationId: 'create-dockerfile-application',
security: [
['bearerAuth' => []],
],
@@ -463,6 +471,7 @@ class ApplicationsController extends Controller
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -490,6 +499,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Image)',
description: 'Create new application based on a prebuilt docker image',
path: '/applications/dockerimage',
+ operationId: 'create-dockerimage-application',
security: [
['bearerAuth' => []],
],
@@ -546,6 +556,7 @@ class ApplicationsController extends Controller
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -573,6 +584,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Compose)',
description: 'Create new application based on a docker-compose file.',
path: '/applications/dockercompose',
+ operationId: 'create-dockercompose-application',
security: [
['bearerAuth' => []],
],
@@ -595,6 +607,7 @@ class ApplicationsController extends Controller
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -620,7 +633,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths'];
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -658,6 +671,7 @@ class ApplicationsController extends Controller
$fqdn = $request->domains;
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
+ $useBuildServer = $request->use_build_server;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -730,6 +744,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -826,6 +844,10 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -918,6 +940,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -997,6 +1023,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1055,6 +1085,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1171,6 +1205,7 @@ class ApplicationsController extends Controller
summary: 'Get',
description: 'Get application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'get-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1235,6 +1270,7 @@ class ApplicationsController extends Controller
summary: 'Delete',
description: 'Delete application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'delete-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1250,16 +1286,10 @@ class ApplicationsController extends Controller
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1307,10 +1337,14 @@ class ApplicationsController extends Controller
'message' => 'Application not found',
], 404);
}
+
DeleteResourceJob::dispatch(
resource: $application,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Application deletion request queued.',
@@ -1321,6 +1355,7 @@ class ApplicationsController extends Controller
summary: 'Update',
description: 'Update application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'update-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1394,6 +1429,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -1450,7 +1486,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect'];
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
@@ -1526,6 +1562,17 @@ class ApplicationsController extends Controller
}
$request->offsetUnset('docker_compose_domains');
}
+ $instantDeploy = $request->instant_deploy;
+
+ $use_build_server = $request->use_build_server;
+
+ if (isset($use_build_server)) {
+ $application->settings->is_build_server_enabled = $use_build_server;
+ $application->settings->save();
+ }
+
+ removeUnnecessaryFieldsFromRequest($request);
+
$data = $request->all();
data_set($data, 'fqdn', $domains);
if ($dockerComposeDomainsJson->count() > 0) {
@@ -1534,6 +1581,16 @@ class ApplicationsController extends Controller
$application->fill($data);
$application->save();
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ is_api: true,
+ );
+ }
+
return response()->json([
'uuid' => $application->uuid,
]);
@@ -1543,6 +1600,7 @@ class ApplicationsController extends Controller
summary: 'List Envs',
description: 'List all envs by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'list-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1625,6 +1683,7 @@ class ApplicationsController extends Controller
summary: 'Update Env',
description: 'Update env by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'update-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1807,6 +1866,7 @@ class ApplicationsController extends Controller
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by application UUID.',
path: '/applications/{uuid}/envs/bulk',
+ operationId: 'update-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1998,6 +2058,7 @@ class ApplicationsController extends Controller
summary: 'Create Env',
description: 'Create env by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'create-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2157,6 +2218,7 @@ class ApplicationsController extends Controller
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/applications/{uuid}/envs/{env_uuid}',
+ operationId: 'delete-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2242,6 +2304,7 @@ class ApplicationsController extends Controller
summary: 'Start',
description: 'Start application. `Post` request is also accepted.',
path: '/applications/{uuid}/start',
+ operationId: 'start-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2345,6 +2408,7 @@ class ApplicationsController extends Controller
summary: 'Stop',
description: 'Stop application. `Post` request is also accepted.',
path: '/applications/{uuid}/stop',
+ operationId: 'stop-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2417,6 +2481,7 @@ class ApplicationsController extends Controller
summary: 'Restart',
description: 'Restart application. `Post` request is also accepted.',
path: '/applications/{uuid}/restart',
+ operationId: 'restart-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2497,6 +2562,131 @@ class ApplicationsController extends Controller
}
+ #[OA\Post(
+ summary: 'Execute Command',
+ description: "Execute a command on the application's current container.",
+ path: '/applications/{uuid}/execute',
+ operationId: 'execute-command-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the application.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Command to execute.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: "Execute a command on the application's current container.",
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Command executed.'],
+ 'response' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ 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 execute_command_by_uuid(Request $request)
+ {
+ // TODO: Need to review this from security perspective, to not allow arbitrary command execution
+ $allowedFields = ['command'];
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+ if (! $application) {
+ return response()->json(['message' => 'Application not found.'], 404);
+ }
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'command' => 'string|required',
+ ]);
+
+ $extraFields = array_diff(array_keys($request->all()), $allowedFields);
+ if ($validator->fails() || ! empty($extraFields)) {
+ $errors = $validator->errors();
+ if (! empty($extraFields)) {
+ foreach ($extraFields as $field) {
+ $errors->add($field, 'This field is not allowed.');
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+
+ $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
+ $status = getContainerStatus($application->destination->server, $container['Names']);
+
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Application is not running.',
+ ], 400);
+ }
+
+ $commands = collect([
+ executeInDocker($container['Names'], $request->command),
+ ]);
+
+ $res = instant_remote_process(command: $commands, server: $application->destination->server);
+
+ return response()->json([
+ 'message' => 'Command executed.',
+ 'response' => $res,
+ ]);
+ }
+
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 0f1ee00d8..65873f818 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -46,6 +46,7 @@ class DatabasesController extends Controller
summary: 'List',
description: 'List all databases.',
path: '/databases',
+ operationId: 'list-databases',
security: [
['bearerAuth' => []],
],
@@ -91,6 +92,7 @@ class DatabasesController extends Controller
summary: 'Get',
description: 'Get database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'get-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -151,6 +153,7 @@ class DatabasesController extends Controller
summary: 'Update',
description: 'Update database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'update-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,6 +513,7 @@ class DatabasesController extends Controller
summary: 'Create (PostgreSQL)',
description: 'Create a new PostgreSQL database.',
path: '/databases/postgresql',
+ operationId: 'create-database-postgresql',
security: [
['bearerAuth' => []],
],
@@ -575,6 +579,7 @@ class DatabasesController extends Controller
summary: 'Create (Clickhouse)',
description: 'Create a new Clickhouse database.',
path: '/databases/clickhouse',
+ operationId: 'create-database-clickhouse',
security: [
['bearerAuth' => []],
],
@@ -636,6 +641,7 @@ class DatabasesController extends Controller
summary: 'Create (DragonFly)',
description: 'Create a new DragonFly database.',
path: '/databases/dragonfly',
+ operationId: 'create-database-dragonfly',
security: [
['bearerAuth' => []],
],
@@ -696,6 +702,7 @@ class DatabasesController extends Controller
summary: 'Create (Redis)',
description: 'Create a new Redis database.',
path: '/databases/redis',
+ operationId: 'create-database-redis',
security: [
['bearerAuth' => []],
],
@@ -757,6 +764,7 @@ class DatabasesController extends Controller
summary: 'Create (KeyDB)',
description: 'Create a new KeyDB database.',
path: '/databases/keydb',
+ operationId: 'create-database-keydb',
security: [
['bearerAuth' => []],
],
@@ -818,6 +826,7 @@ class DatabasesController extends Controller
summary: 'Create (MariaDB)',
description: 'Create a new MariaDB database.',
path: '/databases/mariadb',
+ operationId: 'create-database-mariadb',
security: [
['bearerAuth' => []],
],
@@ -882,6 +891,7 @@ class DatabasesController extends Controller
summary: 'Create (MySQL)',
description: 'Create a new MySQL database.',
path: '/databases/mysql',
+ operationId: 'create-database-mysql',
security: [
['bearerAuth' => []],
],
@@ -945,6 +955,7 @@ class DatabasesController extends Controller
summary: 'Create (MongoDB)',
description: 'Create a new MongoDB database.',
path: '/databases/mongodb',
+ operationId: 'create-database-mongodb',
security: [
['bearerAuth' => []],
],
@@ -1514,6 +1525,7 @@ class DatabasesController extends Controller
summary: 'Delete',
description: 'Delete database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'delete-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1529,16 +1541,10 @@ class DatabasesController extends Controller
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1583,10 +1589,14 @@ class DatabasesController extends Controller
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
+
DeleteResourceJob::dispatch(
resource: $database,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Database deletion request queued.',
@@ -1597,6 +1607,7 @@ class DatabasesController extends Controller
summary: 'Start',
description: 'Start database. `Post` request is also accepted.',
path: '/databases/{uuid}/start',
+ operationId: 'start-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1672,6 +1683,7 @@ class DatabasesController extends Controller
summary: 'Stop',
description: 'Stop database. `Post` request is also accepted.',
path: '/databases/{uuid}/stop',
+ operationId: 'stop-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1747,6 +1759,7 @@ class DatabasesController extends Controller
summary: 'Restart',
description: 'Restart database. `Post` request is also accepted.',
path: '/databases/{uuid}/restart',
+ operationId: 'restart-database-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 437162058..d1c8f5ea6 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -32,6 +32,7 @@ class DeployController extends Controller
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
+ operationId: 'list-deployments',
security: [
['bearerAuth' => []],
],
@@ -79,12 +80,13 @@ class DeployController extends Controller
summary: 'Get',
description: 'Get deployment by UUID.',
path: '/deployments/{uuid}',
+ operationId: 'get-deployment-by-uuid',
security: [
['bearerAuth' => []],
],
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(
@@ -134,6 +136,7 @@ class DeployController extends Controller
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
path: '/deploy',
+ operationId: 'deploy-by-tag-or-uuid',
security: [
['bearerAuth' => []],
],
@@ -147,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/EnvironmentVariablesController.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php
deleted file mode 100644
index d127d0525..000000000
--- a/app/Http/Controllers/Api/EnvironmentVariablesController.php
+++ /dev/null
@@ -1,35 +0,0 @@
-env_uuid)->first();
- if (! $env) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
- if (! $found_app) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $env->delete();
-
- return response()->json([
- 'message' => 'Environment variable deleted.',
- ]);
- }
-}
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index 1e48ffdbe..2414b7a42 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -13,6 +13,7 @@ class OtherController extends Controller
summary: 'Version',
description: 'Get Coolify version.',
path: '/version',
+ operationId: 'version',
security: [
['bearerAuth' => []],
],
@@ -43,6 +44,7 @@ class OtherController extends Controller
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
+ operationId: 'enable-api',
security: [
['bearerAuth' => []],
],
@@ -84,7 +86,7 @@ class OtherController extends Controller
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200);
@@ -94,6 +96,7 @@ class OtherController extends Controller
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
+ operationId: 'disable-api',
security: [
['bearerAuth' => []],
],
@@ -135,7 +138,7 @@ class OtherController extends Controller
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200);
@@ -158,6 +161,7 @@ class OtherController extends Controller
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
path: '/healthcheck',
+ operationId: 'healthcheck',
responses: [
new OA\Response(
response: 200,
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index 6aec31e9b..f1958de2c 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -11,8 +11,9 @@ class ProjectController extends Controller
{
#[OA\Get(
summary: 'List',
- description: 'list projects.',
+ description: 'List projects.',
path: '/projects',
+ operationId: 'list-projects',
security: [
['bearerAuth' => []],
],
@@ -46,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),
);
@@ -54,8 +55,9 @@ 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: [
['bearerAuth' => []],
],
@@ -102,6 +104,7 @@ class ProjectController extends Controller
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
+ operationId: 'get-environment-by-name',
security: [
['bearerAuth' => []],
],
@@ -136,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);
@@ -155,6 +161,7 @@ class ProjectController extends Controller
summary: 'Create',
description: 'Create Project.',
path: '/projects',
+ operationId: 'create-project',
security: [
['bearerAuth' => []],
],
@@ -167,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.'],
],
),
@@ -250,6 +257,7 @@ class ProjectController extends Controller
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
+ operationId: 'update-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -333,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();
@@ -355,6 +363,7 @@ class ProjectController extends Controller
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
+ operationId: 'delete-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -408,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/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php
index ae076bb71..1fd5792e0 100644
--- a/app/Http/Controllers/Api/ResourcesController.php
+++ b/app/Http/Controllers/Api/ResourcesController.php
@@ -13,6 +13,7 @@ class ResourcesController extends Controller
summary: 'List',
description: 'Get all resources.',
path: '/resources',
+ operationId: 'list-resources',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index 67128234e..bb474aed3 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -26,6 +26,7 @@ class SecurityController extends Controller
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
+ operationId: 'list-private-keys',
security: [
['bearerAuth' => []],
],
@@ -68,12 +69,13 @@ class SecurityController extends Controller
summary: 'Get',
description: 'Get key by UUID.',
path: '/security/keys/{uuid}',
+ operationId: 'get-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
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(
@@ -124,6 +126,7 @@ class SecurityController extends Controller
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
+ operationId: 'create-private-key',
security: [
['bearerAuth' => []],
],
@@ -217,6 +220,7 @@ class SecurityController extends Controller
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
+ operationId: 'update-private-key',
security: [
['bearerAuth' => []],
],
@@ -313,12 +317,13 @@ class SecurityController extends Controller
summary: 'Delete',
description: 'Delete a private key.',
path: '/security/keys/{uuid}',
+ operationId: 'delete-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
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 9044c4a35..a49515579 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -46,6 +46,7 @@ class ServersController extends Controller
summary: 'List',
description: 'List all servers.',
path: '/servers',
+ operationId: 'list-servers',
security: [
['bearerAuth' => []],
],
@@ -100,12 +101,13 @@ class ServersController extends Controller
summary: 'Get',
description: 'Get server by UUID.',
path: '/servers/{uuid}',
+ operationId: 'get-server-by-uuid',
security: [
['bearerAuth' => []],
],
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(
@@ -177,12 +179,13 @@ class ServersController extends Controller
summary: 'Resources',
description: 'Get resources by server.',
path: '/servers/{uuid}/resources',
+ operationId: 'get-resources-by-server-uuid',
security: [
['bearerAuth' => []],
],
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(
@@ -254,12 +257,13 @@ class ServersController extends Controller
summary: 'Domains',
description: 'Get domains by server.',
path: '/servers/{uuid}/domains',
+ operationId: 'get-domains-by-server-uuid',
security: [
['bearerAuth' => []],
],
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(
@@ -304,7 +308,7 @@ class ServersController extends Controller
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
@@ -401,6 +405,7 @@ class ServersController extends Controller
summary: 'Create',
description: 'Create Server.',
path: '/servers',
+ operationId: 'create-server',
security: [
['bearerAuth' => []],
],
@@ -545,6 +550,7 @@ class ServersController extends Controller
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
+ operationId: 'update-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -655,6 +661,7 @@ class ServersController extends Controller
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
+ operationId: 'delete-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -727,6 +734,7 @@ class ServersController extends Controller
summary: 'Validate',
description: 'Validate server by UUID.',
path: '/servers/{uuid}/validate',
+ operationId: 'validate-server-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 3a5b59f55..89418517b 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -38,6 +38,7 @@ class ServicesController extends Controller
summary: 'List',
description: 'List all services.',
path: '/services',
+ operationId: 'list-services',
security: [
['bearerAuth' => []],
],
@@ -88,6 +89,7 @@ class ServicesController extends Controller
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
+ operationId: 'create-service',
security: [
['bearerAuth' => []],
],
@@ -365,6 +367,7 @@ class ServicesController extends Controller
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
+ operationId: 'get-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -375,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',
@@ -422,17 +425,22 @@ class ServicesController extends Controller
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
+ operationId: 'delete-service-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
response: 200,
- description: 'Delete a service by Uuid',
+ description: 'Delete a service by UUID',
content: [
new OA\MediaType(
mediaType: 'application/json',
@@ -472,17 +480,540 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- DeleteResourceJob::dispatch($service);
+
+ DeleteResourceJob::dispatch(
+ resource: $service,
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Service deletion request queued.',
]);
}
+ #[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.',
path: '/services/{uuid}/start',
+ operationId: 'start-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,9 +1041,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -558,6 +1091,7 @@ class ServicesController extends Controller
summary: 'Stop',
description: 'Stop service. `Post` request is also accepted.',
path: '/services/{uuid}/stop',
+ operationId: 'stop-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -585,9 +1119,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -633,6 +1169,7 @@ class ServicesController extends Controller
summary: 'Restart',
description: 'Restart service. `Post` request is also accepted.',
path: '/services/{uuid}/restart',
+ operationId: 'restart-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -660,9 +1197,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index 1a481e5ec..3f951c6f7 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -32,6 +32,7 @@ class TeamController extends Controller
summary: 'List',
description: 'Get all teams.',
path: '/teams',
+ operationId: 'list-teams',
security: [
['bearerAuth' => []],
],
@@ -79,6 +80,7 @@ class TeamController extends Controller
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
+ operationId: 'get-team-by-id',
security: [
['bearerAuth' => []],
],
@@ -129,6 +131,7 @@ class TeamController extends Controller
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
+ operationId: 'get-members-by-team-id',
security: [
['bearerAuth' => []],
],
@@ -189,6 +192,7 @@ class TeamController extends Controller
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
+ operationId: 'get-current-team',
security: [
['bearerAuth' => []],
],
@@ -225,6 +229,7 @@ class TeamController extends Controller
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
+ operationId: 'get-current-team-members',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php
index 9569e8cfa..630d01045 100644
--- a/app/Http/Controllers/OauthController.php
+++ b/app/Http/Controllers/OauthController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -22,7 +21,7 @@ class OauthController extends Controller
$oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first();
if (! $user) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403, 'Registration is disabled');
}
diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php
index 648720ba4..471e6d602 100644
--- a/app/Http/Middleware/ApiAllowed.php
+++ b/app/Http/Middleware/ApiAllowed.php
@@ -14,7 +14,7 @@ class ApiAllowed
if (isCloud()) {
return $next($request);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 473cbc679..9ae383a9f 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -26,6 +26,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
@@ -109,10 +110,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $is_debug_enabled;
- private $build_args;
+ private Collection|string $build_args;
private $env_args;
+ private $environment_variables;
+
private $env_nixpacks_args;
private $docker_compose;
@@ -157,7 +160,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
- private bool $preserveRepository = true;
+ private bool $preserveRepository = false;
public $tries = 1;
@@ -166,6 +169,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
+ $this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -198,11 +202,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
- $this->container_name = $this->application->settings->custom_internal_name;
+ if ($this->pull_request_id === 0) {
+ $this->container_name = $this->application->settings->custom_internal_name;
+ } else {
+ $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
+ }
}
- ray('New container name: ', $this->container_name);
+ ray('New container name: ', $this->container_name)->green();
- savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -276,6 +283,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->original_server = $this->server;
} else {
$this->build_server = $buildServers->random();
+ $this->application_deployment_queue->build_server_id = $this->build_server->id;
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
$this->original_server = $this->server;
$this->use_build_server = true;
@@ -414,15 +422,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
+ if ($this->preserveRepository) {
+ foreach ($this->application->fileStorages as $fileStorage) {
+ $path = $fileStorage->fs_path;
+ $saveName = 'file_stat_'.$fileStorage->id;
+ $realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value();
+ // check if the file is a directory or a file inside the repository
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName]
+ );
+ if ($this->saved_outputs->has($saveName)) {
+ $fileStat = $this->saved_outputs->get($saveName);
+ if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) {
+ $fileStorage->is_directory = true;
+ $fileStorage->content = null;
+ $fileStorage->save();
+ $fileStorage->deleteStorageOnServer();
+ $fileStorage->saveStorageOnServer();
+ } elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) {
+ $fileStorage->is_directory = false;
+ $fileStorage->is_based_on_git = true;
+ $fileStorage->save();
+ $fileStorage->deleteStorageOnServer();
+ $fileStorage->saveStorageOnServer();
+ }
+ }
+ }
+ }
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->parseRawCompose();
+ $this->application->oldRawParser();
$yaml = $composeFile = $this->application->docker_compose_raw;
$this->save_environment_variables();
} else {
- $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id'));
+ $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
@@ -439,11 +474,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return;
}
- $yaml = Yaml::dump($composeFile->toArray(), 10);
+ $yaml = Yaml::dump(convertToArray($composeFile), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
+ 'hidden' => true,
]);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -473,13 +509,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// TODO
} else {
$this->execute_remote_command([
- "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true,
+ "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true",
+ 'hidden' => true,
+ 'ignore_errors' => true,
], [
- "docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true,
+ "docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
+ 'hidden' => true,
+ 'ignore_errors' => true,
]);
}
// Start compose file
+ $server_workdir = $this->application->workdir();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
@@ -488,7 +529,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$this->write_deployment_configurations();
- $server_workdir = $this->application->workdir();
$this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
@@ -508,15 +548,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$command = "{$this->coolify_variables} docker compose";
- if ($this->env_filename) {
- $command .= " --env-file {$this->workdir}/{$this->env_filename}";
- }
- $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ if ($this->preserveRepository) {
+ if ($this->env_filename) {
+ $command .= " --env-file {$server_workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
+ $this->write_deployment_configurations();
- $this->write_deployment_configurations();
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
- );
+ $this->execute_remote_command(
+ ['command' => $command, 'hidden' => true],
+ );
+ } else {
+ if ($this->env_filename) {
+ $command .= " --env-file {$this->workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
+ );
+ $this->write_deployment_configurations();
+ }
}
}
@@ -610,15 +661,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
[
"mkdir -p $this->configuration_dir",
],
- // removing this now as we are using docker cp
- // [
- // "rm -rf $this->configuration_dir/{*,.*}",
- // ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
);
}
+ foreach ($this->application->fileStorages as $fileStorage) {
+ if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
+ $fileStorage->saveStorageOnServer();
+ }
+ }
if ($this->use_build_server) {
$this->server = $this->build_server;
}
@@ -698,7 +750,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"),
+ 'hidden' => true,
],
);
if ($this->application->docker_registry_image_tag) {
@@ -706,10 +759,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
+ 'ignore_errors' => true,
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
+ 'ignore_errors' => true,
+ 'hidden' => true,
],
);
}
@@ -806,14 +863,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
+ "docker images -q {$this->production_image_name} 2>/dev/null",
+ 'hidden' => true,
+ 'save' => 'local_image_found',
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
- "docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true,
+ "docker pull {$this->production_image_name} 2>/dev/null",
+ 'ignore_errors' => true,
+ 'hidden' => true,
]);
$this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
+ "docker images -q {$this->production_image_name} 2>/dev/null",
+ 'hidden' => true,
+ 'save' => 'local_image_found',
]);
}
}
@@ -846,17 +909,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
+ $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
+ $envs->push("COOLIFY_DOMAIN_FQDN={$url}");
}
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $envs->push("COOLIFY_BRANCH={$local_branch}");
- }
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
+ if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
+ }
}
+
+ add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
+
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -891,18 +961,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
+ if ((int) $this->application->compose_parsing_version >= 3) {
+ $envs->push("COOLIFY_URL={$this->application->fqdn}");
+ } else {
+ $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
+ }
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
- $envs->push("COOLIFY_URL={$url}");
+ if ((int) $this->application->compose_parsing_version >= 3) {
+ $envs->push("COOLIFY_FQDN={$url}");
+ } else {
+ $envs->push("COOLIFY_URL={$url}");
+ }
}
- if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $envs->push("COOLIFY_BRANCH={$local_branch}");
- }
- if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
- $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
+ if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
+ if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
+ }
+ if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
+ }
}
+
+ add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
+
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -979,17 +1062,58 @@ 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';
@@ -1209,7 +1333,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
+ $settings = instanceSettings();
$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);
@@ -1329,10 +1455,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
- executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true,
'save' => 'git_commit_sha',
- ],
+ ]
);
} else {
$this->execute_remote_command(
@@ -1361,7 +1487,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->execute_remote_command(
[
- $importCommands, 'hidden' => true,
+ $importCommands,
+ 'hidden' => true,
]
);
$this->create_workdir();
@@ -1445,6 +1572,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') {
@@ -1571,7 +1701,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_from_repo',
+ 'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
@@ -1674,14 +1807,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
- $docker_compose['services'][$this->container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
@@ -1708,13 +1834,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
}
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
+ if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
+ $docker_compose['services'][$this->container_name]['volumes'] = [];
+ }
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages);
}
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
+ if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
+ $docker_compose['services'][$this->container_name]['volumes'] = [];
+ }
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
- })->toArray();
+ })->toArray());
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
@@ -1837,13 +1970,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker pull {$image}"),
+ 'hidden' => true,
]
);
}
private function build_image()
{
+ // Add Coolify related variables to the build args
+ $this->environment_variables->filter(function ($key, $value) {
+ return str($key)->startsWith('COOLIFY_');
+ })->each(function ($key, $value) {
+ $this->build_args->push("--build-arg '{$key}'");
+ });
+
+ $this->build_args = $this->build_args->implode(' ');
+
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
@@ -1887,12 +2030,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
}
@@ -1900,10 +2045,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -1917,10 +2068,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
}
@@ -1957,10 +2114,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
} else {
@@ -1974,10 +2137,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
} else {
@@ -1986,22 +2155,30 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -2015,10 +2192,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
}
@@ -2027,24 +2210,43 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
- /**
- * @param int $timeout in seconds
- */
- private function graceful_shutdown_container(string $containerName, int $timeout = 30)
+ private function graceful_shutdown_container(string $containerName, int $timeout = 300)
{
try {
- $this->execute_remote_command(
- ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
- ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
- );
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ break;
+ }
+ usleep(100000);
+ }
+
+ $isRunning = $this->execute_remote_command(
+ ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
+ ) === 'true';
+
+ if ($isRunning) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
} catch (\Exception $error) {
- // report error if needed
+ $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
+ $this->remove_container($containerName);
+ }
+
+ private function remove_container(string $containerName)
+ {
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
-
}
private function stop_running_container(bool $force = false)
@@ -2114,15 +2316,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
-
- $this->build_args = $this->build_args->implode(' ');
- ray($this->build_args);
}
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile',
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
@@ -2140,7 +2341,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
- $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -2168,7 +2368,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
- 'command' => $exec, 'hidden' => true,
+ 'command' => $exec,
+ 'hidden' => true,
],
);
@@ -2195,7 +2396,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
try {
$this->execute_remote_command(
[
- 'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output',
+ 'command' => $exec,
+ 'hidden' => true,
+ 'save' => 'post-deployment-command-output',
],
);
} catch (Exception $e) {
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..f2348118a 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -2,13 +2,13 @@
namespace App\Jobs;
-use App\Models\InstanceSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
@@ -21,16 +21,18 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
if (isDev() || isCloud()) {
return;
}
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
+
$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/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index 7b064a464..b8ca8b7ed 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
{
try {
ray('Cleaning up helper containers on '.$this->server->name);
- $containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
- $containers = format_docker_command_output_to_json($containers);
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerId = data_get($container, 'ID');
+ $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
+ $containerIds = collect(json_decode($containers))->pluck('ID');
+ if ($containerIds->count() > 0) {
+ foreach ($containerIds as $containerId) {
ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}
diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php
new file mode 100644
index 000000000..6d49bee4b
--- /dev/null
+++ b/app/Jobs/CleanupStaleMultiplexedConnections.php
@@ -0,0 +1,82 @@
+cleanupStaleConnections();
+ $this->cleanupNonExistentServerConnections();
+ }
+
+ private function cleanupStaleConnections()
+ {
+ $muxFiles = Storage::disk('ssh-mux')->files();
+
+ foreach ($muxFiles as $muxFile) {
+ $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
+ $server = Server::where('uuid', $serverUuid)->first();
+
+ if (! $server) {
+ $this->removeMultiplexFile($muxFile);
+
+ continue;
+ }
+
+ $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
+ $checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
+ $checkProcess = Process::run($checkCommand);
+
+ if ($checkProcess->exitCode() !== 0) {
+ $this->removeMultiplexFile($muxFile);
+ } else {
+ $muxContent = Storage::disk('ssh-mux')->get($muxFile);
+ $establishedAt = Carbon::parse(substr($muxContent, 37));
+ $expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
+
+ if (Carbon::now()->isAfter($expirationTime)) {
+ $this->removeMultiplexFile($muxFile);
+ }
+ }
+ }
+ }
+
+ private function cleanupNonExistentServerConnections()
+ {
+ $muxFiles = Storage::disk('ssh-mux')->files();
+ $existingServerUuids = Server::pluck('uuid')->toArray();
+
+ foreach ($muxFiles as $muxFile) {
+ $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
+ if (! in_array($serverUuid, $existingServerUuids)) {
+ $this->removeMultiplexFile($muxFile);
+ }
+ }
+ }
+
+ private function extractServerUuidFromMuxFile($muxFile)
+ {
+ return substr($muxFile, 4);
+ }
+
+ private function removeMultiplexFile($muxFile)
+ {
+ $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
+ $closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
+ Process::run($closeCommand);
+ Storage::disk('ssh-mux')->delete($muxFile);
+ }
+}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index e919855d5..22ae06ebd 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->uuid;
- }
-
public function handle()
{
GetContainersStatus::run($this->server);
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 79b00e9cd..769739d5e 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -2,7 +2,6 @@
namespace App\Jobs;
-use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
@@ -22,7 +21,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
@@ -56,49 +54,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $backup_output = null;
+ public ?string $postgres_password = null;
+
public ?S3Storage $s3 = null;
public function __construct($backup)
{
$this->backup = $backup;
- $this->team = Team::find($backup->team_id);
- if (is_null($this->team)) {
- return;
- }
- if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
- $this->database = data_get($this->backup, 'database');
- $this->server = $this->database->service->server;
- $this->s3 = $this->backup->s3;
- } else {
- $this->database = data_get($this->backup, 'database');
- $this->server = $this->database->destination->server;
- $this->s3 = $this->backup->s3;
- }
- }
-
- public function middleware(): array
- {
- return [new WithoutOverlapping($this->backup->id)];
- }
-
- public function uniqueId(): int
- {
- return $this->backup->id;
}
public function handle(): void
{
try {
- BackupCreated::dispatch($this->team->id);
-
- // Check if team is exists
- if (is_null($this->team)) {
- $this->backup->update(['status' => 'failed']);
- StopDatabase::run($this->database);
- $this->database->delete();
+ $this->team = Team::find($this->backup->team_id);
+ if (! $this->team) {
+ $this->backup->delete();
return;
}
+ if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
+ $this->database = data_get($this->backup, 'database');
+ $this->server = $this->database->service->server;
+ $this->s3 = $this->backup->s3;
+ } else {
+ $this->database = data_get($this->backup, 'database');
+ $this->server = $this->database->destination->server;
+ $this->s3 = $this->backup->s3;
+ }
+ if (is_null($this->server)) {
+ throw new \Exception('Server not found?!');
+ }
+ if (is_null($this->database)) {
+ throw new \Exception('Database not found?!');
+ }
+
+ BackupCreated::dispatch($this->team->id);
+
$status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
@@ -134,6 +125,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$databasesToBackup = $this->database->postgres_user;
}
+ $this->postgres_password = $envs->filter(function ($env) {
+ return str($env)->startsWith('POSTGRES_PASSWORD=');
+ })->first();
+ if ($this->postgres_password) {
+ $this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value();
+ }
+
} elseif (str($databaseType)->contains('mysql')) {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -238,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
-
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db';
@@ -251,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try {
if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -279,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -288,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -326,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
- BackupCreated::dispatch($this->team->id);
+ if ($this->team) {
+ BackupCreated::dispatch($this->team->id);
+ }
}
}
@@ -336,7 +344,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
@@ -351,13 +359,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
$commands[] = 'mkdir -p '.$this->backup_dir;
if ($collectionsToExclude->count() === 0) {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
}
} else {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
@@ -381,7 +389,18 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
+ $backupCommand = 'docker exec';
+ if ($this->postgres_password) {
+ $backupCommand .= " -e PGPASSWORD=$this->postgres_password";
+ }
+ if ($this->backup->dump_all) {
+ $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
+ } else {
+ $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
+ }
+
+ $commands[] = $backupCommand;
+ ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -399,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
- ray($commands);
+ if ($this->backup->dump_all) {
+ $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
+ }
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -418,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
+ if ($this->backup->dump_all) {
+ $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
+ }
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
@@ -452,7 +478,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
- $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
+ $deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
@@ -477,12 +503,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} 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";
+
+ $this->ensureHelperImageAvailable();
+
+ $fullImageName = $this->getFullImageName();
+
+ if (isDev()) {
+ if ($this->database->name === 'coolify-db') {
+ $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ } else {
+ $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ }
+ } else {
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
+ }
+ if ($this->s3->isHetzner()) {
+ $endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
+ $commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
+ } else {
+ $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
+ }
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
+
$this->add_to_backup_output('Uploaded to S3.');
- 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;
@@ -491,4 +537,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
instant_remote_process([$command], $this->server);
}
}
+
+ private function ensureHelperImageAvailable(): void
+ {
+ $fullImageName = $this->getFullImageName();
+
+ $imageExists = $this->checkImageExists($fullImageName);
+
+ if (! $imageExists) {
+ $this->pullHelperImage($fullImageName);
+ }
+ }
+
+ private function checkImageExists(string $fullImageName): bool
+ {
+ $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
+
+ return trim($result) === 'exists';
+ }
+
+ private function pullHelperImage(string $fullImageName): void
+ {
+ try {
+ instant_remote_process(["docker pull {$fullImageName}"], $this->server);
+ } catch (\Exception $e) {
+ $errorMessage = 'Failed to pull helper image: '.$e->getMessage();
+ $this->add_to_backup_output($errorMessage);
+ throw new \RuntimeException($errorMessage);
+ }
+ }
+
+ private function getFullImageName(): string
+ {
+ $settings = instanceSettings();
+ $helperImage = config('coolify.helper_image');
+ $latestVersion = $settings->helper_version;
+
+ return "{$helperImage}:{$latestVersion}";
+ }
}
diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php
deleted file mode 100644
index d3b0e99cf..000000000
--- a/app/Jobs/DatabaseBackupStatusJob.php
+++ /dev/null
@@ -1,62 +0,0 @@
-scheduledDatabaseBackups()->get();
- // if ($scheduled_backups->isEmpty()) {
- // continue;
- // }
- // foreach ($scheduled_backups as $scheduled_backup) {
- // $last_days_backups = $scheduled_backup->get_last_days_backup_status();
- // if ($last_days_backups->isEmpty()) {
- // continue;
- // }
- // $failed = $last_days_backups->where('status', 'failed');
- // }
- // }
-
- // $scheduled_backups = ScheduledDatabaseBackup::all();
- // $databases = collect();
- // $teams = collect();
- // foreach ($scheduled_backups as $scheduled_backup) {
- // $last_days_backups = $scheduled_backup->get_last_days_backup_status();
- // if ($last_days_backups->isEmpty()) {
- // continue;
- // }
- // $failed = $last_days_backups->where('status', 'failed');
- // $database = $scheduled_backup->database;
- // $team = $database->team();
- // $teams->put($team->id, $team);
- // $databases->put("{$team->id}:{$database->name}", [
- // 'failed_count' => $failed->count(),
- // ]);
- // }
- // foreach ($databases as $name => $database) {
- // [$team_id, $name] = explode(':', $name);
- // $team = $teams->get($team_id);
- // $team?->notify(new DailyBackup($databases));
- // }
- }
-}
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index dbf44dd5d..2442d5b06 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
+use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
- public bool $deleteConfigurations = false,
- public bool $deleteVolumes = false) {}
+ public bool $deleteConfigurations = true,
+ public bool $deleteVolumes = true,
+ public bool $dockerCleanup = true,
+ public bool $deleteConnectedNetworks = true
+ ) {}
public function handle()
{
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
- StopDatabase::run($this->resource);
+ StopDatabase::run($this->resource, true);
break;
case 'service':
- StopService::run($this->resource);
- DeleteService::run($this->resource);
+ StopService::run($this->resource, true);
+ DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
+
+ $isDatabase = $this->resource instanceof StandalonePostgresql
+ || $this->resource instanceof StandaloneRedis
+ || $this->resource instanceof StandaloneMongodb
+ || $this->resource instanceof StandaloneMysql
+ || $this->resource instanceof StandaloneMariadb
+ || $this->resource instanceof StandaloneKeydb
+ || $this->resource instanceof StandaloneDragonfly
+ || $this->resource instanceof StandaloneClickhouse;
+ $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
+ if (($this->dockerCleanup || $isDatabase) && $server) {
+ CleanupDocker::dispatch($server, true);
+ }
+
+ if ($this->deleteConnectedNetworks && ! $isDatabase) {
+ $this->resource?->delete_connected_networks($this->resource->uuid);
+ }
} catch (\Throwable $e) {
- ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
+ if ($this->dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
Artisan::queue('cleanup:stucked-resources');
}
}
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 5010263ae..900bae99c 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -17,11 +17,13 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $timeout = 300;
+ public $timeout = 600;
- public int|string|null $usageBefore = null;
+ public $tries = 1;
- public function __construct(public Server $server) {}
+ public ?string $usageBefore = null;
+
+ public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
{
@@ -29,9 +31,10 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
if (! $this->server->isFunctional()) {
return;
}
- if ($this->server->settings->is_force_cleanup_enabled) {
- Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
- CleanupDocker::run(server: $this->server, force: true);
+
+ if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
+ Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name);
+ CleanupDocker::run(server: $this->server);
return;
}
@@ -39,12 +42,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
$this->usageBefore = $this->server->getDiskUsage();
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
- CleanupDocker::run(server: $this->server, force: true);
+ CleanupDocker::run(server: $this->server);
return;
}
- if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
- CleanupDocker::run(server: $this->server, force: false);
+ if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
+ CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
@@ -56,7 +59,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
- ray($e->getMessage());
+ CleanupDocker::run(server: $this->server);
+ Log::error('DockerCleanupJob failed: '.$e->getMessage());
throw $e;
}
}
diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php
index 3188d35d6..9c0a2b55b 100644
--- a/app/Jobs/GithubAppPermissionJob.php
+++ b/app/Jobs/GithubAppPermissionJob.php
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public GithubApp $github_app) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->github_app->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->github_app->uuid;
- }
-
public function handle()
{
try {
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..4b208fc31 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -8,8 +8,8 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
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
{
@@ -17,25 +17,25 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): string
- {
- return $this->server->uuid;
- }
-
- public function __construct(public Server $server) {}
+ public function __construct() {}
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();
+ $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/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php
index f8c769382..32f84e6d5 100644
--- a/app/Jobs/PullSentinelImageJob.php
+++ b/app/Jobs/PullSentinelImageJob.php
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): string
- {
- return $this->server->uuid;
- }
-
public function __construct(public Server $server) {}
public function handle(): void
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 819e28f89..6850ae98a 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue
@@ -36,6 +35,8 @@ class ScheduledTaskJob implements ShouldQueue
public array $containers = [];
+ public string $server_timezone;
+
public function __construct($task)
{
$this->task = $task;
@@ -47,20 +48,27 @@ class ScheduledTaskJob implements ShouldQueue
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
+ $this->server_timezone = $this->getServerTimezone();
}
- public function middleware(): array
+ private function getServerTimezone(): string
{
- return [new WithoutOverlapping($this->task->id)];
- }
+ if ($this->resource instanceof Application) {
+ $timezone = $this->resource->destination->server->settings->server_timezone;
- public function uniqueId(): int
- {
- return $this->task->id;
+ return $timezone;
+ } elseif ($this->resource instanceof Service) {
+ $timezone = $this->resource->server->settings->server_timezone;
+
+ return $timezone;
+ }
+
+ return 'UTC';
}
public function handle(): void
{
+
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -121,6 +129,7 @@ class ScheduledTaskJob implements ShouldQueue
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
+ } finally {
}
}
}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 1db15cfd4..39d4aa0c0 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
@@ -24,7 +23,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $tries = 3;
+ public $tries = 1;
+
+ public $timeout = 60;
public $containers;
@@ -43,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
- // public function middleware(): array
- // {
- // return [(new WithoutOverlapping($this->server->uuid))];
- // }
-
- // public function uniqueId(): int
- // {
- // return $this->server->uuid;
- // }
-
public function handle()
{
try {
@@ -78,8 +69,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
return 'No containers found.';
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
- $this->checkLogDrainContainer();
- $this->checkSentinel();
+ if ($this->server->isLogDrainEnabled()) {
+ $this->checkLogDrainContainer();
+ }
}
} catch (\Throwable $e) {
@@ -90,24 +82,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
- private function checkSentinel()
- {
- if ($this->server->isSentinelEnabled()) {
- $sentinelContainerFound = $this->containers->filter(function ($value, $key) {
- return data_get($value, 'Name') === '/coolify-sentinel';
- })->first();
- if ($sentinelContainerFound) {
- $status = data_get($sentinelContainerFound, 'State.Status');
- if ($status !== 'running') {
- PullSentinelImageJob::dispatch($this);
- }
- }
- }
- }
-
private function serverStatus()
{
- ['uptime' => $uptime] = $this->server->validateConnection();
+ ['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 24292025b..1f09d5a3b 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Team $team) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->team->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->team->uuid;
- }
-
public function handle()
{
try {
diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php
index ac9182eca..fcc33c859 100644
--- a/app/Jobs/ServerStatusJob.php
+++ b/app/Jobs/ServerStatusJob.php
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->uuid;
- }
-
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
new file mode 100644
index 000000000..376cb8532
--- /dev/null
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -0,0 +1,59 @@
+server->isFunctional()) {
+ ray('Server is not ready.');
+
+ return 'Server is not ready.';
+ }
+ $team = $this->server->team;
+ $percentage = $this->server->storageCheck();
+ if ($percentage > 1) {
+ ray('Server storage is at '.$percentage.'%');
+ }
+
+ } catch (\Throwable $e) {
+ ray($e->getMessage());
+
+ return handleError($e);
+ }
+
+ }
+}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
index 4c65a711f..2cc705e4a 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -3,7 +3,6 @@
namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -23,7 +22,7 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
{
try {
CheckForUpdatesJob::dispatchSync();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.');
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 147a1ad6f..52d4674ee 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
@@ -139,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
- $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
@@ -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;
@@ -172,10 +175,19 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
- $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
+ $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
@@ -219,27 +231,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function savePrivateKey()
{
$this->validate([
- 'privateKeyName' => 'required',
- 'privateKey' => 'required',
+ 'privateKeyName' => 'required|string|max:255',
+ 'privateKeyDescription' => 'nullable|string|max:255',
+ 'privateKey' => 'required|string',
]);
- $this->createdPrivateKey = PrivateKey::create([
- 'name' => $this->privateKeyName,
- 'description' => $this->privateKeyDescription,
- 'private_key' => $this->privateKey,
- 'team_id' => currentTeam()->id,
- ]);
- $this->createdPrivateKey->save();
- $this->currentState = 'create-server';
+
+ try {
+ $privateKey = PrivateKey::createAndStore([
+ 'name' => $this->privateKeyName,
+ 'description' => $this->privateKeyDescription,
+ 'private_key' => $this->privateKey,
+ 'team_id' => currentTeam()->id,
+ ]);
+
+ $this->createdPrivateKey = $privateKey;
+ $this->currentState = 'create-server';
+ } catch (\Exception $e) {
+ $this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
+ }
}
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 +289,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 +297,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 +319,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
+ $this->createdServer->settings()->update([
+ 'is_usable' => false,
+ ]);
+
return handleError(error: $e, livewire: $this);
}
}
@@ -349,6 +376,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/CommandCenter/Index.php b/app/Livewire/CommandCenter/Index.php
deleted file mode 100644
index 0a05e811f..000000000
--- a/app/Livewire/CommandCenter/Index.php
+++ /dev/null
@@ -1,21 +0,0 @@
-servers = Server::isReachable()->get();
- }
-
- public function render()
- {
- return view('livewire.command-center.index');
- }
-}
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index 1abd28c3c..d18a7689e 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -30,8 +30,7 @@ class Dashboard extends Component
public function cleanup_queue()
{
- $this->dispatch('success', 'Cleanup started.');
- Artisan::queue('cleanup:application-deployment-queue', [
+ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id,
]);
}
@@ -50,15 +49,6 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray();
}
- // public function getIptables()
- // {
- // $servers = Server::ownedByCurrentTeam()->get();
- // foreach ($servers as $server) {
- // checkRequiredCommands($server);
- // $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
- // ray($iptables);
- // }
- // }
public function render()
{
return view('livewire.dashboard');
diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php
index 7125f2120..87ae83931 100644
--- a/app/Livewire/Destination/Form.php
+++ b/app/Livewire/Destination/Form.php
@@ -38,7 +38,7 @@ class Form extends Component
}
$this->destination->delete();
- return redirect()->route('dashboard');
+ return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php
index d6dc0d521..934e81661 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -47,7 +47,7 @@ class Help extends Component
]
);
$mail->subject("[HELP]: {$this->subject}");
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
$url = 'https://app.coolify.io/api/feedback';
@@ -61,6 +61,7 @@ class Help extends Component
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
}
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
+ $this->reset('description', 'subject');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index ec196c154..988add7c8 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -2,13 +2,28 @@
namespace App\Livewire;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
- public function delete()
+ public $team;
+
+ public function mount()
{
+ $this->team = currentTeam()->name;
+ }
+
+ public function delete($password)
+ {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
$currentTeam = currentTeam();
$currentTeam->delete();
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 2960ed226..53673292e 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -172,7 +172,7 @@ class Email extends Component
public function copyFromInstanceSettings()
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->smtp_enabled) {
$team = currentTeam();
$team->update([
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index cde3322db..a3a688f7c 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -66,9 +66,9 @@ class Advanced extends Component
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->parseRawCompose();
+ $this->application->oldRawParser();
} else {
- $this->application->parseCompose();
+ $this->application->parse();
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
@@ -96,6 +96,12 @@ class Advanced extends Component
} else {
$this->application->settings->custom_internal_name = null;
}
+ if (is_null($this->application->settings->custom_internal_name)) {
+ $this->application->settings->save();
+ $this->dispatch('success', 'Custom name saved.');
+
+ return;
+ }
$customInternalName = $this->application->settings->custom_internal_name;
$server = $this->application->destination->server;
$allApplications = $server->applications();
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index 84a24255c..3de895f8c 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -69,6 +69,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/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index b3e39d23d..5fccce792 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -55,9 +55,14 @@ class DeploymentNavbar extends Component
public function cancel()
{
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
+ $build_server_id = $this->application_deployment_queue->build_server_id;
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
try {
- $server = Server::find($server_id);
+ if ($this->application->settings->is_build_server_enabled) {
+ $server = Server::find($build_server_id);
+ } else {
+ $server = Server::find($server_id);
+ }
if ($this->application_deployment_queue->logs) {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 77593bf0a..d2700f444 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
-use App\Models\LocalFileVolume;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -30,6 +29,8 @@ class General extends Component
public ?string $ports_exposes = null;
+ public bool $is_preserve_repository_enabled = false;
+
public bool $is_container_label_escape_enabled = true;
public $customLabels;
@@ -130,7 +131,7 @@ class General extends Component
public function mount()
{
try {
- $this->parsedServices = $this->application->parseCompose();
+ $this->parsedServices = $this->application->parse();
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
@@ -145,6 +146,7 @@ class General extends Component
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
+ $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
@@ -168,9 +170,21 @@ class General extends Component
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
+
+ // If port_exposes changed, reset default labels
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false);
}
+ if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
+ if ($this->application->settings->is_preserve_repository_enabled === false) {
+ $this->application->fileStorages->each(function ($storage) {
+ $storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
+ $storage->save();
+ });
+ }
+
+ }
+
}
public function loadComposeFile($isInit = false)
@@ -179,39 +193,18 @@ class General extends Component
if ($isInit && $this->application->docker_compose_raw) {
return;
}
+
+ // Must reload the application to get the latest database changes
+ // Why? Not sure, but it works.
+ // $this->application->refresh();
+
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
if (is_null($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
return;
}
- $compose = $this->application->parseCompose();
- $services = data_get($compose, 'services');
- if ($services) {
- $volumes = collect($services)->map(function ($service) {
- return data_get($service, 'volumes');
- })->flatten()->filter(function ($volume) {
- return str($volume)->startsWith('/data/coolify');
- })->unique()->values();
- foreach ($volumes as $volume) {
- $source = str($volume)->before(':');
- $target = str($volume)->after(':')->beforeLast(':');
-
- LocalFileVolume::updateOrCreate(
- [
- 'mount_path' => $target,
- 'resource_id' => $this->application->id,
- 'resource_type' => get_class($this->application),
- ],
- [
- 'fs_path' => $source,
- 'mount_path' => $target,
- 'resource_id' => $this->application->id,
- 'resource_type' => get_class($this->application),
- ]
- );
- }
- }
+ $this->application->parse();
$this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index c02949e17..1082b48cd 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
+ public bool $docker_cleanup = true;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop()
{
- StopApplication::run($this->application);
+ StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
+
+ public function render()
+ {
+ return view('livewire.project.application.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 30bc0a9d1..b1ba035dc 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -79,8 +81,15 @@ class Previews extends Component
return;
}
- $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
+ if ($this->application->build_pack === 'dockercompose') {
+ $preview->generate_preview_fqdn_compose();
+ $this->application->refresh();
+ $this->dispatch('success', 'Domain generated.');
+ return;
+ }
+
+ $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
$url = Url::fromString($fqdn);
$template = $this->application->preview_url_template;
$host = $url->getHost();
@@ -177,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
- $this->dispatch('reloadWindow');
+
+ GetContainersStatus::run($server);
+ $this->application->refresh();
+ $this->dispatch('containerStatusUpdated');
+ $this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -196,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
+
+ ApplicationPreview::where('application_id', $this->application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->first()
+ ->delete();
+
$this->application->refresh();
$this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.');
@@ -213,4 +230,49 @@ class Previews extends Component
return handleError($e, $this);
}
}
+
+ private function stopContainers(array $containers, $server, int $timeout)
+ {
+ $processes = [];
+ foreach ($containers as $container) {
+ $containerName = str_replace('/', '', $container['Names']);
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
+ }
+
+ private function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(["docker kill $containerName"], $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 59f2f9a39..7e2e4a12b 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@@ -12,6 +14,12 @@ class BackupEdit extends Component
public $s3s;
+ public bool $delete_associated_backups_locally = false;
+
+ public bool $delete_associated_backups_s3 = false;
+
+ public bool $delete_associated_backups_sftp = false;
+
public ?string $status = null;
public array $parameters;
@@ -23,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
+ 'backup.dump_all' => 'required|boolean',
];
protected $validationAttributes = [
@@ -32,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
+ 'backup.dump_all' => 'Backup All Databases',
];
protected $messages = [
@@ -46,10 +56,24 @@ class BackupEdit extends Component
}
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
+ if ($this->delete_associated_backups_locally) {
+ $this->deleteAssociatedBackupsLocally();
+ }
+ if ($this->delete_associated_backups_s3) {
+ $this->deleteAssociatedBackupsS3();
+ }
+
$this->backup->delete();
+
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
@@ -104,4 +128,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage());
}
}
+
+ public function deleteAssociatedBackupsLocally()
+ {
+ $executions = $this->backup->executions;
+ $backupFolder = null;
+
+ foreach ($executions as $execution) {
+ if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ $server = $this->backup->database->service->destination->server;
+ } else {
+ $server = $this->backup->database->destination->server;
+ }
+
+ if (! $backupFolder) {
+ $backupFolder = dirname($execution->filename);
+ }
+
+ delete_backup_locally($execution->filename, $server);
+ $execution->delete();
+ }
+
+ if ($backupFolder) {
+ $this->deleteEmptyBackupFolder($backupFolder, $server);
+ }
+ }
+
+ public function deleteAssociatedBackupsS3()
+ {
+ //Add function to delete backups from S3
+ }
+
+ public function deleteAssociatedBackupsSftp()
+ {
+ //Add function to delete backups from SFTP
+ }
+
+ private function deleteEmptyBackupFolder($folderPath, $server)
+ {
+ $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$folderPath'"], $server);
+
+ $parentFolder = dirname($folderPath);
+ $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkParentEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$parentFolder'"], $server);
+ }
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-edit', [
+ 'checkboxes' => [
+ ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index de1bac36f..c8c33a022 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -3,19 +3,28 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
+ public $database;
+
public $executions = [];
public $setDeletableBackup;
+ public $delete_backup_s3 = true;
+
+ public $delete_backup_sftp = true;
+
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -32,19 +41,36 @@ class BackupExecutions extends Component
}
}
- public function deleteBackup($exeuctionId)
+ #[On('deleteBackup')]
+ public function deleteBackup($executionId, $password)
{
- $execution = $this->backup->executions()->where('id', $exeuctionId)->first();
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
+ $execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
+
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
+
+ if ($this->delete_backup_s3) {
+ // Add logic to delete from S3
+ }
+
+ if ($this->delete_backup_sftp) {
+ // Add logic to delete from SFTP
+ }
+
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
@@ -58,7 +84,66 @@ class BackupExecutions extends Component
public function refreshBackupExecutions(): void
{
if ($this->backup) {
- $this->executions = $this->backup->executions()->get()->sortBy('created_at');
+ $this->executions = $this->backup->executions()->get();
}
}
+
+ public function mount(ScheduledDatabaseBackup $backup)
+ {
+ $this->backup = $backup;
+ $this->database = $backup->database;
+ $this->refreshBackupExecutions();
+ }
+
+ public function server()
+ {
+ if ($this->database) {
+ $server = null;
+
+ if ($this->database instanceof \App\Models\ServiceDatabase) {
+ $server = $this->database->service->destination->server;
+ } elseif ($this->database->destination && $this->database->destination->server) {
+ $server = $this->database->destination->server;
+ }
+ if ($server) {
+ return $server;
+ }
+ }
+
+ return null;
+ }
+
+ public function getServerTimezone()
+ {
+ $server = $this->server();
+ if (! $server) {
+ return 'UTC';
+ }
+ $serverTimezone = $server->settings->server_timezone;
+
+ return $serverTimezone;
+ }
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->getServerTimezone();
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } catch (\Exception $e) {
+ $dateObj->setTimezone(new \DateTimeZone('UTC'));
+ }
+
+ return $dateObj->format('Y-m-d H:i:s T');
+ }
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-executions', [
+ 'checkboxes' => [
+ ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
+ ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index ffdbe95c3..7a6446815 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index f81f4a2f0..394ba6c9a 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -30,6 +30,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 6435f6781..49884ff9a 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters;
+ public $docker_cleanup = true;
+
public function getListeners()
{
$userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop()
{
- StopDatabase::run($this->database);
+ StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function render()
+ {
+ return view('livewire.project.database.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 2b78c9f10..f976e1edd 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 858d7b383..12d4882f3 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 5a5ef8a62..ac40e7dfa 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -33,6 +33,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -46,6 +47,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 58d8e03a8..7d5270ddf 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index eabbbd679..c12fa49f3 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -49,6 +49,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -65,6 +66,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index a7ce0161a..72fd95de8 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index beb5a9c39..8021e25d3 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -26,7 +26,7 @@ class ScheduledBackups extends Component
public function mount(): void
{
if ($this->selectedBackupId) {
- $this->setSelectedBackup($this->selectedBackupId);
+ $this->setSelectedBackup($this->selectedBackupId, true);
}
$this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
@@ -37,10 +37,13 @@ class ScheduledBackups extends Component
$this->s3s = currentTeam()->s3s;
}
- public function setSelectedBackup($backupId)
+ public function setSelectedBackup($backupId, $force = false)
{
+ if ($this->selectedBackupId === $backupId && ! $force) {
+ return;
+ }
$this->selectedBackupId = $backupId;
- $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
+ $this->selectedBackup = $this->database->scheduledBackups->find($backupId);
if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null;
}
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 22478916f..e01741770 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false;
+ public string $environmentName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->environmentName = Environment::findOrFail($this->environment_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 499b86e3e..360fad10a 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false;
+ public string $projectName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->projectName = Project::findOrFail($this->project_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 633ce5bda..199a20cf6 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
+use App\Models\StandaloneDocker;
+use App\Models\SwarmDocker;
use Illuminate\Support\Str;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@@ -58,12 +60,26 @@ class DockerCompose extends Component
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
+
+ $destination_uuid = $this->query['destination'];
+ $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
+ if (! $destination) {
+ $destination = SwarmDocker::where('uuid', $destination_uuid)->first();
+ }
+ if (! $destination) {
+ throw new \Exception('Destination not found. What?!');
+ }
+ $destination_class = $destination->getMorphClass();
+
$service = Service::create([
'name' => 'service'.Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination_class,
]);
+
$variables = parseEnvFormatToArray($this->envFile);
foreach ($variables as $key => $variable) {
EnvironmentVariable::create([
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index b29fe8cab..b5c5cb1db 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -99,6 +99,16 @@ class PublicGitRepository extends Component
}
}
+ public function updatedDockerComposeLocation()
+ {
+ if ($this->docker_compose_location) {
+ $this->docker_compose_location = rtrim($this->docker_compose_location, '/');
+ if (! str($this->docker_compose_location)->startsWith('/')) {
+ $this->docker_compose_location = '/'.$this->docker_compose_location;
+ }
+ }
+ }
+
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index b25290f71..f757fc460 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -45,6 +45,8 @@ class Select extends Component
public ?string $selectedEnvironment = null;
+ public string $postgresql_type = 'postgres:16-alpine';
+
public ?string $existingPostgresqlUrl = null;
public ?string $search = null;
@@ -88,6 +90,121 @@ class Select extends Component
// }
// }
+ public function loadServices2()
+ {
+ $services = get_service_templates(true);
+ $services = collect($services)->map(function ($service, $key) {
+ return [
+ 'name' => str($key)->headline(),
+ 'logo' => asset(data_get($service, 'logo', 'svgs/unknown.svg')),
+ ] + (array) $service;
+ })->all();
+ $gitBasedApplications = [
+ [
+ 'id' => 'public',
+ 'name' => 'Public Repository',
+ 'description' => 'You can deploy any kind of public repositories from the supported git providers.',
+ 'logo' => asset('svgs/git.svg'),
+ ],
+ [
+ 'id' => 'private-gh-app',
+ 'name' => 'Private Repository (with GitHub App)',
+ 'description' => 'You can deploy public & private repositories through your GitHub Apps.',
+ 'logo' => asset('svgs/github.svg'),
+ ],
+ [
+ 'id' => 'private-deploy-key',
+ 'name' => 'Private Repository (with Deploy Key)',
+ 'description' => 'You can deploy private repositories with a deploy key.',
+ 'logo' => asset('svgs/git.svg'),
+ ],
+ ];
+ $dockerBasedApplications = [
+ [
+ 'id' => 'dockerfile',
+ 'name' => 'Dockerfile',
+ 'description' => 'You can deploy a simple Dockerfile, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ [
+ 'id' => 'docker-compose-empty',
+ 'name' => 'Docker Compose Empty',
+ 'description' => 'You can deploy complex application easily with Docker Compose, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ [
+ 'id' => 'docker-image',
+ 'name' => 'Docker Image',
+ 'description' => 'You can deploy an existing Docker Image from any Registry, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ ];
+ $databases = [
+ [
+ 'id' => 'postgresql',
+ 'name' => 'PostgreSQL',
+ 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
+ 'logo' => '
+',
+ ],
+ [
+ 'id' => 'mysql',
+ 'name' => 'MySQL',
+ 'description' => 'MySQL is an open-source relational database known for its simplicity and performance.',
+ 'logo' => '
+
+
+
+ ',
+
+ ],
+ [
+ 'id' => 'mariadb',
+ 'name' => 'MariaDB',
+ 'description' => 'MariaDB is an open-source relational database known for its simplicity and performance.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'redis',
+ 'name' => 'Redis',
+ 'description' => 'Redis is an open-source in-memory data structure store known for its simplicity and performance.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'keydb',
+ 'name' => 'KeyDB',
+ 'description' => 'KeyDB is an open-source in-memory data structure store known for its simplicity and performance.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'dragonfly',
+ 'name' => 'Dragonfly',
+ 'description' => 'Dragonfly is an open-source in-memory data structure store known for its simplicity and performance.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'mongodb',
+ 'name' => 'MongoDB',
+ 'description' => 'MongoDB is an open-source document-oriented database known for its simplicity and performance.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'clickhouse',
+ 'name' => 'ClickHouse',
+ 'description' => 'ClickHouse is an open-source column-oriented database known for its simplicity and performance.',
+ 'logo' => '
',
+ ],
+
+ ];
+
+ return [
+ 'services' => $services,
+ 'gitBasedApplications' => $gitBasedApplications,
+ 'dockerBasedApplications' => $dockerBasedApplications,
+ 'databases' => $databases,
+ ];
+ }
+
public function updatedSearch()
{
$this->loadServices();
@@ -139,6 +256,7 @@ class Select extends Component
public function setType(string $type)
{
+ $type = str($type)->lower()->slug()->value();
if ($this->loading) {
return;
}
@@ -202,6 +320,8 @@ class Select extends Component
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
if ($docker) {
$this->setDestination($docker->uuid);
+
+ return $this->whatToDoNext();
}
}
$this->current_step = 'destinations';
@@ -211,15 +331,38 @@ class Select extends Component
{
$this->destination_uuid = $destination_uuid;
+ return $this->whatToDoNext();
+ }
+
+ public function setPostgresqlType(string $type)
+ {
+ $this->postgresql_type = $type;
+
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
+ 'database_image' => $this->postgresql_type,
]);
}
+ public function whatToDoNext()
+ {
+ if ($this->type === 'postgresql') {
+ $this->current_step = 'select-postgresql-type';
+ } else {
+ return redirect()->route('project.resource.create', [
+ 'project_uuid' => $this->parameters['project_uuid'],
+ 'environment_name' => $this->parameters['environment_name'],
+ 'type' => $this->type,
+ 'destination' => $this->destination_uuid,
+ 'server_id' => $this->server_id,
+ ]);
+ }
+ }
+
public function loadServers()
{
$this->servers = Server::isUsable()->get();
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 341dd93d8..5c6a37d6d 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -18,6 +18,7 @@ class Create extends Component
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
+ $database_image = request()->query('database_image');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
@@ -33,7 +34,11 @@ class Create extends Component
if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === 'postgresql') {
- $database = create_standalone_postgresql($environment->id, $destination_uuid);
+ $database = create_standalone_postgresql(
+ environmentId: $environment->id,
+ destinationUuid: $destination_uuid,
+ databaseImage: $database_image
+ );
} elseif ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} elseif ($type->value() === 'mongodb') {
@@ -86,18 +91,16 @@ class Create extends Component
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
- $generatedValue = $value;
- if ($value->contains('SERVICE_')) {
- $command = $value->after('SERVICE_')->beforeLast('_');
- $generatedValue = generateEnvValue($command->value(), $service);
+ if ($value) {
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $value,
+ 'service_id' => $service->id,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
}
- EnvironmentVariable::create([
- 'key' => $key,
- 'value' => $generatedValue,
- 'service_id' => $service->id,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
+
});
}
$service->parse(isNew: true);
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index c82012aaa..a2e48fee7 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -25,6 +25,7 @@ class Configuration extends Component
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
+ 'refresh' => '$refresh',
];
}
@@ -51,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id);
if ($application) {
$application->restart();
- $this->dispatch('success', 'Application restarted successfully.');
+ $this->dispatch('success', 'Service application restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -64,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id);
if ($database) {
$database->restart();
- $this->dispatch('success', 'Database restarted successfully.');
+ $this->dispatch('success', 'Service database restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -75,6 +76,12 @@ class Configuration extends Component
{
try {
GetContainersStatus::run($this->service->server);
+ $this->service->applications->each(function ($application) {
+ $application->refresh();
+ });
+ $this->service->databases->each(function ($database) {
+ $database->refresh();
+ });
$this->dispatch('$refresh');
} catch (\Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index f67b95a8a..dc043e65a 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -11,7 +11,11 @@ class EditCompose extends Component
public $serviceId;
- protected $listeners = ['refreshEnvs', 'envsUpdated'];
+ protected $listeners = [
+ 'refreshEnvs',
+ 'envsUpdated',
+ 'refresh' => 'envsUpdated',
+ ];
protected $rules = [
'service.docker_compose_raw' => 'required',
@@ -39,6 +43,7 @@ class EditCompose extends Component
{
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
+ $this->dispatch('refreshStorages');
}
public function instantSave()
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 2d9c95daa..215019112 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
@@ -33,6 +35,7 @@ class FileStorage extends Component
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
+ 'fileStorage.is_based_on_git' => 'required|boolean',
];
public function mount()
@@ -45,6 +48,7 @@ class FileStorage extends Component
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
+ $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -53,6 +57,7 @@ class FileStorage extends Component
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = true;
$this->fileStorage->content = null;
+ $this->fileStorage->is_based_on_git = false;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
@@ -68,6 +73,9 @@ class FileStorage extends Component
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = false;
$this->fileStorage->content = null;
+ if (data_get($this->resource, 'settings.is_preserve_repository_enabled')) {
+ $this->fileStorage->is_based_on_git = true;
+ }
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
@@ -77,8 +85,14 @@ class FileStorage extends Component
}
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
$message = 'File deleted.';
if ($this->fileStorage->is_directory) {
@@ -123,6 +137,13 @@ class FileStorage extends Component
public function render()
{
- return view('livewire.project.service.file-storage');
+ return view('livewire.project.service.file-storage', [
+ 'directoryDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
+ ],
+ 'fileDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
+ ],
+ ]);
}
}
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index 674182df5..70b3b5db6 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -20,6 +20,10 @@ class Navbar extends Component
public $isDeploymentProgress = false;
+ public $docker_cleanup = true;
+
+ public $title = 'Configuration';
+
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
@@ -40,7 +44,7 @@ class Navbar extends Component
public function serviceStarted()
{
- $this->dispatch('success', 'Service status changed.');
+ // $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@@ -60,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.');
}
- public function render()
- {
- return view('livewire.project.service.navbar');
- }
-
public function checkDeployments()
{
try {
@@ -95,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id);
}
- public function stop(bool $forceCleanup = false)
+ public function stop()
{
- StopService::run($this->service);
- if ($forceCleanup) {
- $this->dispatch('success', 'Containers cleaned up.');
- } else {
- $this->dispatch('success', 'Service stopped.');
- }
+ StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
}
@@ -114,11 +108,35 @@ class Navbar extends Component
return;
}
- PullImage::run($this->service);
- StopService::run($this->service);
+ StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function pullAndRestartEvent()
+ {
+ $this->checkDeployments();
+ if ($this->isDeploymentProgress) {
+ $this->dispatch('error', 'There is a deployment in progress.');
+
+ return;
+ }
+ PullImage::run($this->service);
+ StopService::run(service: $this->service, dockerCleanup: false);
+ $this->service->parse();
+ $this->dispatch('imagePulled');
+ $activity = StartService::run($this->service);
+ $this->dispatch('activityMonitor', $activity->id);
+ }
+
+ public function render()
+ {
+ return view('livewire.project.service.navbar', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index e7d00c3dd..56b506043 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters;
+ public $docker_cleanup = true;
+
+ public $delete_volumes = true;
+
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
- public function render()
- {
- return view('livewire.project.service.service-application-view');
- }
-
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose');
}
}
+
+ public function render()
+ {
+ return view('livewire.project.service.service-application-view', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 3f62202c8..7f2416e3e 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
- $isPassword = data_get($field, 'isPassword');
+ $isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,13 +47,21 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
- $this->fields = $this->fields->sortBy('name');
+ $this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
+ return $group->sortBy(function ($field) {
+ return data_get($field, 'isPassword') ? 1 : 0;
+ })->mapWithKeys(function ($field) {
+ return [$field['key'] => $field];
+ });
+ })->flatMap(function ($group) {
+ return $group;
+ });
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
- $this->submit();
+ $this->submit(notify: false);
}
public function instantSave()
@@ -62,7 +70,7 @@ class StackForm extends Component
$this->dispatch('success', 'Service settings saved.');
}
- public function submit()
+ public function submit($notify = true)
{
try {
$this->validate();
@@ -76,7 +84,7 @@ class StackForm extends Component
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->dispatch('refreshEnvs');
- $this->dispatch('success', 'Service saved.');
+ $notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 5f0178be4..c05260899 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
+use App\Models\Service;
+use App\Models\ServiceApplication;
+use App\Models\ServiceDatabase;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{
public $resource;
+ public $resourceName;
+
public $projectUuid;
public $environmentName;
@@ -18,22 +25,95 @@ class Danger extends Component
public bool $delete_volumes = true;
+ public bool $docker_cleanup = true;
+
+ public bool $delete_connected_networks = true;
+
public ?string $modalId = null;
+ public string $resourceDomain = '';
+
public function mount()
{
- $this->modalId = new Cuid2;
$parameters = get_route_parameters();
+ $this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
+
+ if ($this->resource === null) {
+ if (isset($parameters['service_uuid'])) {
+ $this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
+ } elseif (isset($parameters['stack_service_uuid'])) {
+ $this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
+ ?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
+ }
+ }
+
+ if ($this->resource === null) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ if (! method_exists($this->resource, 'type')) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ switch ($this->resource->type()) {
+ case 'application':
+ $this->resourceName = $this->resource->name ?? 'Application';
+ break;
+ case 'standalone-postgresql':
+ case 'standalone-redis':
+ case 'standalone-mongodb':
+ case 'standalone-mysql':
+ case 'standalone-mariadb':
+ case 'standalone-keydb':
+ case 'standalone-dragonfly':
+ case 'standalone-clickhouse':
+ $this->resourceName = $this->resource->name ?? 'Database';
+ break;
+ case 'service':
+ $this->resourceName = $this->resource->name ?? 'Service';
+ break;
+ case 'service-application':
+ $this->resourceName = $this->resource->name ?? 'Service Application';
+ break;
+ case 'service-database':
+ $this->resourceName = $this->resource->name ?? 'Service Database';
+ break;
+ default:
+ $this->resourceName = 'Unknown Resource';
+ }
}
- public function delete()
+ public function delete($password)
{
+ if (isProduction()) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+ }
+
+ if (! $this->resource) {
+ $this->addError('resource', 'Resource not found.');
+
+ return;
+ }
+
try {
- // $this->authorize('delete', $this->resource);
$this->resource->delete();
- DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
+ DeleteResourceJob::dispatch(
+ $this->resource,
+ $this->delete_configurations,
+ $this->delete_volumes,
+ $this->docker_cleanup,
+ $this->delete_connected_networks
+ );
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
@@ -43,4 +123,19 @@ class Danger extends Component
return handleError($e, $this);
}
}
+
+ public function render()
+ {
+ return view('livewire.project.shared.danger', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
+ ['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index a2c018beb..7fb5c45db 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -115,8 +117,14 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
- public function removeServer(int $network_id, int $server_id)
+ public function removeServer(int $network_id, int $server_id, $password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index a859c90b0..0dbf0f957 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -48,14 +48,6 @@ class Add extends Component
public function submit()
{
$this->validate();
- // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
- // $type = str($this->value)->after('{{')->before('.')->value;
- // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
- // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
-
- // return;
- // }
- // }
$this->dispatch('saveKey', [
'key' => $this->key,
'value' => $this->value,
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 9e6760293..5a711259b 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -23,8 +23,9 @@ class All extends Component
public string $view = 'normal';
protected $listeners = [
- 'refreshEnvs',
'saveKey' => 'submit',
+ 'refreshEnvs',
+ 'environmentVariableDeleted' => 'refreshEnvs',
];
protected $rules = [
@@ -40,220 +41,228 @@ class All extends Component
$this->showPreview = true;
}
$this->modalId = new Cuid2;
- $this->sortMe();
- $this->getDevView();
- }
-
- public function sortMe()
- {
- if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
- if ($this->resource->settings->is_env_sorting_enabled) {
- $this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
- $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
- } else {
- $this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
- $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
- }
- }
- $this->getDevView();
+ $this->sortEnvironmentVariables();
}
public function instantSave()
{
- if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
- $this->resource->settings->save();
- $this->dispatch('success', 'Environment variable settings updated.');
- $this->sortMe();
+ $this->resource->settings->save();
+ $this->sortEnvironmentVariables();
+ $this->dispatch('success', 'Environment variable settings updated.');
+ }
+
+ public function sortEnvironmentVariables()
+ {
+ if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
+ if ($this->resource->environment_variables) {
+ $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
+ }
+
+ if ($this->resource->environment_variables_preview) {
+ $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values();
+ }
}
+
+ $this->getDevView();
}
public function getDevView()
{
- $this->variables = $this->resource->environment_variables->map(function ($item) {
+ $this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables);
+ if ($this->showPreview) {
+ $this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview);
+ }
+ }
+
+ private function formatEnvironmentVariables($variables)
+ {
+ return $variables->map(function ($item) {
if ($item->is_shown_once) {
- return "$item->key=(locked secret)";
+ return "$item->key=(Locked Secret, delete and add again to change)";
}
if ($item->is_multiline) {
- return "$item->key=(multiline, edit in normal view)";
+ return "$item->key=(Multiline environment variable, edit in normal view)";
}
return "$item->key=$item->value";
- })->join('
-');
- if ($this->showPreview) {
- $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
- if ($item->is_shown_once) {
- return "$item->key=(locked secret)";
- }
- if ($item->is_multiline) {
- return "$item->key=(multiline, edit in normal view)";
- }
-
- return "$item->key=$item->value";
- })->join('
-');
- }
+ })->join("\n");
}
public function switch()
{
- if ($this->view === 'normal') {
- $this->view = 'dev';
- } else {
- $this->view = 'normal';
- }
- $this->sortMe();
+ $this->view = $this->view === 'normal' ? 'dev' : 'normal';
+ $this->sortEnvironmentVariables();
}
- public function saveVariables($isPreview)
+ public function submit($data = null)
{
- if ($isPreview) {
- $variables = parseEnvFormatToArray($this->variablesPreview);
- $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
- } else {
- $variables = parseEnvFormatToArray($this->variables);
- $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
- }
- foreach ($variables as $key => $variable) {
- if ($isPreview) {
- $found = $this->resource->environment_variables_preview()->where('key', $key)->first();
+ try {
+ if ($data === null) {
+ $this->handleBulkSubmit();
} else {
- $found = $this->resource->environment_variables()->where('key', $key)->first();
+ $this->handleSingleSubmit($data);
}
- if ($found) {
- if ($found->is_shown_once || $found->is_multiline) {
- continue;
+
+ $this->updateOrder();
+ $this->sortEnvironmentVariables();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ } finally {
+ $this->refreshEnvs();
+ }
+ }
+
+ private function updateOrder()
+ {
+ $variables = parseEnvFormatToArray($this->variables);
+ $order = 1;
+ foreach ($variables as $key => $value) {
+ $env = $this->resource->environment_variables()->where('key', $key)->first();
+ if ($env) {
+ $env->order = $order;
+ $env->save();
+ }
+ $order++;
+ }
+
+ if ($this->showPreview) {
+ $previewVariables = parseEnvFormatToArray($this->variablesPreview);
+ $order = 1;
+ foreach ($previewVariables as $key => $value) {
+ $env = $this->resource->environment_variables_preview()->where('key', $key)->first();
+ if ($env) {
+ $env->order = $order;
+ $env->save();
}
- $found->value = $variable;
- // if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
- // $type = str($found->value)->after('{{')->before('.')->value;
- // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
- // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
+ $order++;
+ }
+ }
+ }
- // return;
- // }
- // }
- $found->save();
+ private function handleBulkSubmit()
+ {
+ $variables = parseEnvFormatToArray($this->variables);
+ $this->deleteRemovedVariables(false, $variables);
+ $this->updateOrCreateVariables(false, $variables);
- continue;
+ if ($this->showPreview) {
+ $previewVariables = parseEnvFormatToArray($this->variablesPreview);
+ $this->deleteRemovedVariables(true, $previewVariables);
+ $this->updateOrCreateVariables(true, $previewVariables);
+ }
+
+ $this->dispatch('success', 'Environment variables updated.');
+ }
+
+ private function handleSingleSubmit($data)
+ {
+ $found = $this->resource->environment_variables()->where('key', $data['key'])->first();
+ if ($found) {
+ $this->dispatch('error', 'Environment variable already exists.');
+
+ return;
+ }
+
+ $maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
+ $environment = $this->createEnvironmentVariable($data);
+ $environment->order = $maxOrder + 1;
+ $environment->save();
+ }
+
+ private function createEnvironmentVariable($data)
+ {
+ $environment = new EnvironmentVariable;
+ $environment->key = $data['key'];
+ $environment->value = $data['value'];
+ $environment->is_build_time = $data['is_build_time'] ?? false;
+ $environment->is_multiline = $data['is_multiline'] ?? false;
+ $environment->is_literal = $data['is_literal'] ?? false;
+ $environment->is_preview = $data['is_preview'] ?? false;
+
+ $resourceType = $this->resource->type();
+ $resourceIdField = $this->getResourceIdField($resourceType);
+
+ if ($resourceIdField) {
+ $environment->$resourceIdField = $this->resource->id;
+ }
+
+ return $environment;
+ }
+
+ private function getResourceIdField($resourceType)
+ {
+ $resourceTypes = [
+ 'application' => 'application_id',
+ 'standalone-postgresql' => 'standalone_postgresql_id',
+ 'standalone-redis' => 'standalone_redis_id',
+ 'standalone-mongodb' => 'standalone_mongodb_id',
+ 'standalone-mysql' => 'standalone_mysql_id',
+ 'standalone-mariadb' => 'standalone_mariadb_id',
+ 'standalone-keydb' => 'standalone_keydb_id',
+ 'standalone-dragonfly' => 'standalone_dragonfly_id',
+ 'standalone-clickhouse' => 'standalone_clickhouse_id',
+ 'service' => 'service_id',
+ ];
+
+ return $resourceTypes[$resourceType] ?? null;
+ }
+
+ private function deleteRemovedVariables($isPreview, $variables)
+ {
+ $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+ $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
+ }
+
+ private function updateOrCreateVariables($isPreview, $variables)
+ {
+ foreach ($variables as $key => $value) {
+ $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+ $found = $this->resource->$method()->where('key', $key)->first();
+
+ if ($found) {
+ if (! $found->is_shown_once && ! $found->is_multiline) {
+ $found->value = $value;
+ $found->save();
+ }
} else {
$environment = new EnvironmentVariable;
$environment->key = $key;
- $environment->value = $variable;
- // if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
- // $type = str($environment->value)->after('{{')->before('.')->value;
- // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
- // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
-
- // return;
- // }
- // }
+ $environment->value = $value;
$environment->is_build_time = false;
$environment->is_multiline = false;
- $environment->is_preview = $isPreview ? true : false;
- switch ($this->resource->type()) {
- case 'application':
- $environment->application_id = $this->resource->id;
- break;
- case 'standalone-postgresql':
- $environment->standalone_postgresql_id = $this->resource->id;
- break;
- case 'standalone-redis':
- $environment->standalone_redis_id = $this->resource->id;
- break;
- case 'standalone-mongodb':
- $environment->standalone_mongodb_id = $this->resource->id;
- break;
- case 'standalone-mysql':
- $environment->standalone_mysql_id = $this->resource->id;
- break;
- case 'standalone-mariadb':
- $environment->standalone_mariadb_id = $this->resource->id;
- break;
- case 'standalone-keydb':
- $environment->standalone_keydb_id = $this->resource->id;
- break;
- case 'standalone-dragonfly':
- $environment->standalone_dragonfly_id = $this->resource->id;
- break;
- case 'standalone-clickhouse':
- $environment->standalone_clickhouse_id = $this->resource->id;
- break;
- case 'service':
- $environment->service_id = $this->resource->id;
- break;
- }
+ $environment->is_preview = $isPreview;
+
+ $this->setEnvironmentResourceId($environment);
$environment->save();
}
}
- if ($isPreview) {
- $this->dispatch('success', 'Preview environment variables updated.');
- } else {
- $this->dispatch('success', 'Environment variables updated.');
+ }
+
+ private function setEnvironmentResourceId($environment)
+ {
+ $resourceTypes = [
+ 'application' => 'application_id',
+ 'standalone-postgresql' => 'standalone_postgresql_id',
+ 'standalone-redis' => 'standalone_redis_id',
+ 'standalone-mongodb' => 'standalone_mongodb_id',
+ 'standalone-mysql' => 'standalone_mysql_id',
+ 'standalone-mariadb' => 'standalone_mariadb_id',
+ 'standalone-keydb' => 'standalone_keydb_id',
+ 'standalone-dragonfly' => 'standalone_dragonfly_id',
+ 'standalone-clickhouse' => 'standalone_clickhouse_id',
+ 'service' => 'service_id',
+ ];
+
+ $resourceType = $this->resource->type();
+ if (isset($resourceTypes[$resourceType])) {
+ $environment->{$resourceTypes[$resourceType]} = $this->resource->id;
}
- $this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
+ $this->sortEnvironmentVariables();
$this->getDevView();
}
-
- public function submit($data)
- {
- try {
- $found = $this->resource->environment_variables()->where('key', $data['key'])->first();
- if ($found) {
- $this->dispatch('error', 'Environment variable already exists.');
-
- return;
- }
- $environment = new EnvironmentVariable;
- $environment->key = $data['key'];
- $environment->value = $data['value'];
- $environment->is_build_time = $data['is_build_time'];
- $environment->is_multiline = $data['is_multiline'];
- $environment->is_literal = $data['is_literal'];
- $environment->is_preview = $data['is_preview'];
-
- switch ($this->resource->type()) {
- case 'application':
- $environment->application_id = $this->resource->id;
- break;
- case 'standalone-postgresql':
- $environment->standalone_postgresql_id = $this->resource->id;
- break;
- case 'standalone-redis':
- $environment->standalone_redis_id = $this->resource->id;
- break;
- case 'standalone-mongodb':
- $environment->standalone_mongodb_id = $this->resource->id;
- break;
- case 'standalone-mysql':
- $environment->standalone_mysql_id = $this->resource->id;
- break;
- case 'standalone-mariadb':
- $environment->standalone_mariadb_id = $this->resource->id;
- break;
- case 'standalone-keydb':
- $environment->standalone_keydb_id = $this->resource->id;
- break;
- case 'standalone-dragonfly':
- $environment->standalone_dragonfly_id = $this->resource->id;
- break;
- case 'standalone-clickhouse':
- $environment->standalone_clickhouse_id = $this->resource->id;
- break;
- case 'service':
- $environment->service_id = $this->resource->id;
- break;
- }
- $environment->save();
- $this->refreshEnvs();
- $this->dispatch('success', 'Environment variable added.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index e63871602..463ceecad 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -24,7 +24,8 @@ class Show extends Component
public string $type;
protected $listeners = [
- 'refresh' => 'refresh',
+ 'refreshEnvs' => 'refresh',
+ 'refresh',
'compose_loaded' => '$refresh',
];
@@ -129,7 +130,8 @@ class Show extends Component
{
try {
$this->env->delete();
- $this->dispatch('refreshEnvs');
+ $this->dispatch('environmentVariableDeleted');
+ $this->dispatch('success', 'Environment variable deleted successfully.');
} catch (\Exception $e) {
return handleError($e);
}
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 343915d9c..90419caed 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -2,18 +2,18 @@
namespace App\Livewire\Project\Shared;
-use App\Actions\Server\RunCommand;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Collection;
+use Livewire\Attributes\On;
use Livewire\Component;
class ExecuteContainerCommand extends Component
{
- public string $command;
+ public $selected_container = 'default';
- public string $container;
+ public $container;
public Collection $containers;
@@ -23,8 +23,6 @@ class ExecuteContainerCommand extends Component
public string $type;
- public string $workDir = '';
-
public Server $server;
public Collection $servers;
@@ -33,11 +31,13 @@ class ExecuteContainerCommand extends Component
'server' => 'required',
'container' => 'required',
'command' => 'required',
- 'workDir' => 'nullable',
];
public function mount()
{
+ if (! auth()->user()->isAdmin()) {
+ abort(403);
+ }
$this->parameters = get_route_parameters();
$this->containers = collect();
$this->servers = collect();
@@ -62,24 +62,13 @@ class ExecuteContainerCommand extends Component
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
- $this->container = $this->resource->uuid;
- $this->containers->push($this->container);
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
- $this->resource->applications()->get()->each(function ($application) {
- $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
- });
- $this->resource->databases()->get()->each(function ($database) {
- $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
- });
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
}
- if ($this->containers->count() > 0) {
- $this->container = $this->containers->first();
- }
}
public function loadContainers()
@@ -96,50 +85,78 @@ class ExecuteContainerCommand extends Component
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
}
foreach ($containers as $container) {
- $payload = [
- 'server' => $server,
- 'container' => $container,
- ];
- $this->containers = $this->containers->push($payload);
+ // if container state is running
+ if (data_get($container, 'State') === 'running') {
+ $payload = [
+ 'server' => $server,
+ 'container' => $container,
+ ];
+ $this->containers = $this->containers->push($payload);
+ }
}
+ } elseif (data_get($this->parameters, 'database_uuid')) {
+ if ($this->resource->isRunning()) {
+ $this->containers = $this->containers->push([
+ 'server' => $server,
+ 'container' => [
+ 'Names' => $this->resource->uuid,
+ ],
+ ]);
+ }
+ } elseif (data_get($this->parameters, 'service_uuid')) {
+ $this->resource->applications()->get()->each(function ($application) {
+ if ($application->isRunning()) {
+ $this->containers->push([
+ 'server' => $this->resource->server,
+ 'container' => [
+ 'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
+ ],
+ ]);
+ }
+ });
+ $this->resource->databases()->get()->each(function ($database) {
+ if ($database->isRunning()) {
+ $this->containers->push([
+ 'server' => $this->resource->server,
+ 'container' => [
+ 'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
+ ],
+ ]);
+ }
+ });
}
+
}
if ($this->containers->count() > 0) {
- if (data_get($this->parameters, 'application_uuid')) {
- $this->container = data_get($this->containers->first(), 'container.Names');
- } elseif (data_get($this->parameters, 'database_uuid')) {
- $this->container = $this->containers->first();
- } elseif (data_get($this->parameters, 'service_uuid')) {
- $this->container = $this->containers->first();
- }
+ $this->container = $this->containers->first();
}
}
- public function runCommand()
+ #[On('connectToContainer')]
+ public function connectToContainer()
{
+ if ($this->selected_container === 'default') {
+ $this->dispatch('error', 'Please select a container.');
+
+ return;
+ }
try {
- if (data_get($this->parameters, 'application_uuid')) {
- $container = $this->containers->where('container.Names', $this->container)->first();
- $container_name = data_get($container, 'container.Names');
- if (is_null($container)) {
- throw new \RuntimeException('Container not found.');
- }
- $server = data_get($container, 'server');
- } else {
- $container_name = $this->container;
- $server = $this->servers->first();
+ $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
+ if (is_null($container)) {
+ throw new \RuntimeException('Container not found.');
}
+ $server = data_get($this->container, 'server');
+
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
- $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
- if (! empty($this->workDir)) {
- $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
- } else {
- $exec = "docker exec {$container_name} {$cmd}";
- }
- $activity = RunCommand::run(server: $server, command: $exec);
- $this->dispatch('activityMonitor', $activity->id);
+ $this->dispatch(
+ 'send-terminal-command',
+ isset($container),
+ data_get($container, 'container.Names'),
+ data_get($container, 'server.uuid')
+ );
+
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index edcaf0f34..0e140b8c1 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -97,7 +98,7 @@ class GetLogs extends Component
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
return;
}
- if (! $this->numberOfLines) {
+ if ($this->numberOfLines <= 0) {
$this->numberOfLines = 1000;
}
if ($this->container) {
@@ -108,14 +109,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
} else {
if ($this->server->isSwarm()) {
@@ -124,14 +125,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
}
if ($refresh) {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php
index 4d8c87dbf..b383e294a 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/All.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/All.php
@@ -26,7 +26,7 @@ class All extends Component
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
} elseif ($this->resource->type() == 'application') {
if ($this->resource->build_pack === 'dockercompose') {
- $parsed = $this->resource->parseCompose();
+ $parsed = $this->resource->parse();
$containers = collect(data_get($parsed, 'services'))->keys();
$this->containerNames = $containers;
} else {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
index 7a2e14e89..017cc9fd7 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
@@ -10,6 +10,8 @@ class Executions extends Component
public $selectedKey;
+ public $task;
+
public function getListeners()
{
return [
@@ -26,4 +28,47 @@ class Executions extends Component
}
$this->selectedKey = $key;
}
+
+ public function server()
+ {
+ if (! $this->task) {
+ return null;
+ }
+
+ if ($this->task->application) {
+ if ($this->task->application->destination && $this->task->application->destination->server) {
+ return $this->task->application->destination->server;
+ }
+ } elseif ($this->task->service) {
+ if ($this->task->service->destination && $this->task->service->destination->server) {
+ return $this->task->service->destination->server;
+ }
+ }
+
+ return null;
+ }
+
+ public function getServerTimezone()
+ {
+ $server = $this->server();
+ if (! $server) {
+ return 'UTC';
+ }
+ $serverTimezone = $server->settings->server_timezone;
+
+ return $serverTimezone;
+ }
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->getServerTimezone();
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } catch (\Exception $e) {
+ $dateObj->setTimezone(new \DateTimeZone('UTC'));
+ }
+
+ return $dateObj->format('Y-m-d H:i:s T');
+ }
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 8be4ff643..37f50dd32 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -20,6 +20,8 @@ class Show extends Component
public string $type;
+ public string $scheduledTaskName;
+
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
+ $this->scheduledTaskName = $this->task->name;
}
public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete();
if ($this->type == 'application') {
- return redirect()->route('project.application.configuration', $this->parameters);
+ return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else {
- return redirect()->route('project.service.configuration', $this->parameters);
+ return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
}
} catch (\Exception $e) {
return handleError($e);
diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php
index 08f51ce08..e4b5c9b89 100644
--- a/app/Livewire/Project/Shared/Storages/Show.php
+++ b/app/Livewire/Project/Shared/Storages/Show.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Show extends Component
@@ -36,8 +38,14 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully');
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
$this->storage->delete();
$this->dispatch('refreshStorages');
}
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
new file mode 100644
index 000000000..916db650f
--- /dev/null
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -0,0 +1,58 @@
+user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
+ ];
+ }
+
+ public function closeTerminal()
+ {
+ $this->dispatch('reloadWindow');
+ }
+
+ #[On('send-terminal-command')]
+ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
+ {
+
+ $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
+
+ if ($isContainer) {
+ $status = getContainerStatus($server, $identifier);
+ if ($status !== 'running') {
+ return;
+ }
+ $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+ } else {
+ $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
+ }
+
+ // ssh command is sent back to frontend then to websocket
+ // this is done because the websocket connection is not available here
+ // a better solution would be to remove websocket on NodeJS and work with something like
+ // 1. Laravel Pusher/Echo connection (not possible without a sdk)
+ // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
+ // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
+ // 4. Follow-up discussions here:
+ // - https://github.com/coollabsio/coolify/issues/2298
+ // - https://github.com/coollabsio/coolify/discussions/3362
+ $this->dispatch('send-back-command', $command);
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.terminal');
+ }
+}
diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php
deleted file mode 100644
index c2d3adeea..000000000
--- a/app/Livewire/RunCommand.php
+++ /dev/null
@@ -1,43 +0,0 @@
- 'required',
- 'command' => 'required',
- ];
-
- protected $validationAttributes = [
- 'server' => 'server',
- 'command' => 'command',
- ];
-
- public function mount($servers)
- {
- $this->servers = $servers;
- $this->server = $servers[0]->uuid;
- }
-
- public function runCommand()
- {
- $this->validate();
- try {
- $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
- $this->dispatch('activityMonitor', $activity->id);
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-}
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index ff8679d21..fe68a8ba5 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Security;
+use App\Models\InstanceSettings;
use Livewire\Component;
class ApiTokens extends Component
@@ -14,8 +15,12 @@ class ApiTokens extends Component
public bool $readOnly = true;
+ public bool $rootAccess = false;
+
public array $permissions = ['read-only'];
+ public $isApiEnabled;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -23,6 +28,7 @@ class ApiTokens extends Component
public function mount()
{
+ $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
@@ -31,12 +37,11 @@ class ApiTokens extends Component
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
+ $this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
- if (count($this->permissions) == 0) {
- $this->permissions = ['*'];
- }
+ $this->makeSureOneIsSelected();
}
public function updatedReadOnly()
@@ -44,11 +49,30 @@ class ApiTokens extends Component
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
+ $this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
- if (count($this->permissions) == 0) {
+ $this->makeSureOneIsSelected();
+ }
+
+ public function updatedRootAccess()
+ {
+ if ($this->rootAccess) {
$this->permissions = ['*'];
+ $this->readOnly = false;
+ $this->viewSensitiveData = false;
+ } else {
+ $this->readOnly = true;
+ $this->permissions = ['read-only'];
+ }
+ }
+
+ public function makeSureOneIsSelected()
+ {
+ if (count($this->permissions) == 0) {
+ $this->permissions = ['read-only'];
+ $this->readOnly = true;
}
}
@@ -58,12 +82,6 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
- // if ($this->viewSensitiveData) {
- // $this->permissions[] = 'view:sensitive';
- // }
- // if ($this->readOnly) {
- // $this->permissions[] = 'read-only';
- // }
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php
index 32a67bbea..319cec192 100644
--- a/app/Livewire/Security/PrivateKey/Create.php
+++ b/app/Livewire/Security/PrivateKey/Create.php
@@ -3,17 +3,13 @@
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
-use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
-use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component
{
- use WithRateLimiting;
+ public string $name = '';
- public string $name;
-
- public string $value;
+ public string $value = '';
public ?string $from = null;
@@ -26,72 +22,69 @@ class Create extends Component
'value' => 'required|string',
];
- protected $validationAttributes = [
- 'name' => 'name',
- 'value' => 'private Key',
- ];
-
public function generateNewRSAKey()
{
- try {
- $this->rateLimit(10);
- $this->name = generate_random_name();
- $this->description = 'Created by Coolify';
- ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
+ $this->generateNewKey('rsa');
}
public function generateNewEDKey()
{
- try {
- $this->rateLimit(10);
- $this->name = generate_random_name();
- $this->description = 'Created by Coolify';
- ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
+ $this->generateNewKey('ed25519');
}
- public function updated($updateProperty)
+ private function generateNewKey($type)
{
- if ($updateProperty === 'value') {
- try {
- $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
- } catch (\Throwable $e) {
- if ($this->$updateProperty === '') {
- $this->publicKey = '';
- } else {
- $this->publicKey = 'Invalid private key';
- }
- }
+ $keyData = PrivateKey::generateNewKeyPair($type);
+ $this->setKeyData($keyData);
+ }
+
+ public function updated($property)
+ {
+ if ($property === 'value') {
+ $this->validatePrivateKey();
}
- $this->validateOnly($updateProperty);
}
public function createPrivateKey()
{
$this->validate();
+
try {
- $this->value = trim($this->value);
- if (! str_ends_with($this->value, "\n")) {
- $this->value .= "\n";
- }
- $private_key = PrivateKey::create([
+ $privateKey = PrivateKey::createAndStore([
'name' => $this->name,
'description' => $this->description,
- 'private_key' => $this->value,
+ 'private_key' => trim($this->value)."\n",
'team_id' => currentTeam()->id,
]);
- if ($this->from === 'server') {
- return redirect()->route('dashboard');
- }
- return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
+ return $this->redirectAfterCreation($privateKey);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+
+ private function setKeyData(array $keyData)
+ {
+ $this->name = $keyData['name'];
+ $this->description = $keyData['description'];
+ $this->value = $keyData['private_key'];
+ $this->publicKey = $keyData['public_key'];
+ }
+
+ private function validatePrivateKey()
+ {
+ $validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
+ $this->publicKey = $validationResult['publicKey'];
+
+ if (! $validationResult['isValid']) {
+ $this->addError('value', 'Invalid private key');
+ }
+ }
+
+ private function redirectAfterCreation(PrivateKey $privateKey)
+ {
+ return $this->from === 'server'
+ ? redirect()->route('dashboard')
+ : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
+ }
}
diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php
new file mode 100644
index 000000000..76441a67e
--- /dev/null
+++ b/app/Livewire/Security/PrivateKey/Index.php
@@ -0,0 +1,24 @@
+get();
+
+ return view('livewire.security.private-key.index', [
+ 'privateKeys' => $privateKeys,
+ ])->layout('components.layout');
+ }
+
+ public function cleanupUnusedKeys()
+ {
+ PrivateKey::cleanupUnusedKeys();
+ $this->dispatch('success', 'Unused keys have been cleaned up.');
+ }
+}
diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php
index d86bd5d1e..249c84f14 100644
--- a/app/Livewire/Security/PrivateKey/Show.php
+++ b/app/Livewire/Security/PrivateKey/Show.php
@@ -29,25 +29,27 @@ class Show extends Component
try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
} catch (\Throwable $e) {
- return handleError($e, $this);
+ abort(404);
}
}
public function loadPublicKey()
{
- $this->public_key = $this->private_key->publicKey();
+ $this->public_key = $this->private_key->getPublicKey();
+ if ($this->public_key === 'Error loading private key') {
+ $this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
+ }
}
public function delete()
{
try {
- if ($this->private_key->isEmpty()) {
- $this->private_key->delete();
- currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
+ $this->private_key->safeDelete();
+ currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
- return redirect()->route('security.private-key.index');
- }
- $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
+ return redirect()->route('security.private-key.index');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -56,8 +58,9 @@ class Show extends Component
public function changePrivateKey()
{
try {
- $this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
- $this->private_key->save();
+ $this->private_key->updatePrivateKey([
+ 'private_key' => formatPrivateKey($this->private_key->private_key),
+ ]);
refresh_server_connection($this->private_key);
$this->dispatch('success', 'Private key updated.');
} catch (\Throwable $e) {
diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php
index f7306a5b5..f58d7b6be 100644
--- a/app/Livewire/Server/ConfigureCloudflareTunnels.php
+++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php
@@ -30,14 +30,18 @@ class ConfigureCloudflareTunnels extends Component
public function submit()
{
try {
+ if (str($this->ssh_domain)->contains('https://')) {
+ $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
+ // remove / from the end
+ $this->ssh_domain = str($this->ssh_domain)->replace('/', '');
+ }
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
- ConfigureCloudflared::run($server, $this->cloudflare_token);
+ ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
- $this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
- $this->dispatch('refreshServerShow');
+ $this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php
index 3beec0c91..ed2345b2a 100644
--- a/app/Livewire/Server/Delete.php
+++ b/app/Livewire/Server/Delete.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
@@ -11,8 +13,13 @@ class Delete extends Component
public $server;
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 9934ea345..c4f25c79d 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
+use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
use Livewire\Component;
@@ -18,13 +19,26 @@ class Form extends Component
public ?string $wildcard_domain = null;
- public int $cleanup_after_percentage;
-
public bool $dockerInstallationStarted = false;
public bool $revalidate = false;
- protected $listeners = ['serverInstalled', 'revalidate' => '$refresh'];
+ public $timezones;
+
+ public $delete_unused_volumes = false;
+
+ public $delete_unused_networks = false;
+
+ public function getListeners()
+ {
+ $teamId = auth()->user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
+ 'refreshServerShow' => 'serverInstalled',
+ 'revalidate' => '$refresh',
+ ];
+ }
protected $rules = [
'server.name' => 'required',
@@ -37,7 +51,6 @@ class Form extends Component
'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
- 'server.settings.is_force_cleanup_enabled' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean',
@@ -46,6 +59,12 @@ class Form extends Component
'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean',
+ 'server.settings.server_timezone' => 'required|string|timezone',
+ 'server.settings.force_docker_cleanup' => 'required|boolean',
+ 'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
+ 'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
+ 'server.settings.delete_unused_volumes' => 'boolean',
+ 'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
@@ -66,12 +85,37 @@ class Form extends Component
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
+ 'server.settings.server_timezone' => 'Server Timezone',
+ 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
+ 'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
- public function mount()
+ public function mount(Server $server)
{
+ $this->server = $server;
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->wildcard_domain = $this->server->settings->wildcard_domain;
- $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
+ $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
+ $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
+ $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
+ }
+
+ public function updated($field)
+ {
+ if ($field === 'server.settings.docker_cleanup_frequency') {
+ $frequency = $this->server->settings->docker_cleanup_frequency;
+ if (empty($frequency) || ! validate_cron_expression($frequency)) {
+ $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
+ $this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
+ }
+ }
+ }
+
+ public function cloudflareTunnelConfigured()
+ {
+ $this->serverInstalled();
+ $this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled()
@@ -104,6 +148,7 @@ class Form extends Component
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
+
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
@@ -116,12 +161,12 @@ class Form extends Component
}
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
ray('Starting sentinel');
-
}
} else {
ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server);
}
+ $this->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) {
@@ -172,27 +217,67 @@ class Form extends Component
public function submit()
{
- if (isCloud() && ! isDev()) {
- $this->validate();
- $this->validate([
- 'server.ip' => 'required',
- ]);
- } else {
- $this->validate();
- }
- $uniqueIPs = Server::all()->reject(function (Server $server) {
- return $server->id === $this->server->id;
- })->pluck('ip')->toArray();
- if (in_array($this->server->ip, $uniqueIPs)) {
- $this->dispatch('error', 'IP address is already in use by another team.');
+ try {
+ if (isCloud() && ! isDev()) {
+ $this->validate();
+ $this->validate([
+ 'server.ip' => 'required',
+ ]);
+ } else {
+ $this->validate();
+ }
+ $uniqueIPs = Server::all()->reject(function (Server $server) {
+ return $server->id === $this->server->id;
+ })->pluck('ip')->toArray();
+ if (in_array($this->server->ip, $uniqueIPs)) {
+ $this->dispatch('error', 'IP address is already in use by another team.');
- return;
+ return;
+ }
+ refresh_server_connection($this->server->privateKey);
+ $this->server->settings->wildcard_domain = $this->wildcard_domain;
+ if ($this->server->settings->force_docker_cleanup) {
+ $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ } else {
+ $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
+ }
+ $currentTimezone = $this->server->settings->getOriginal('server_timezone');
+ $newTimezone = $this->server->settings->server_timezone;
+ if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
+ $this->server->settings->server_timezone = $newTimezone;
+ $this->server->settings->save();
+ }
+ $this->server->settings->save();
+ $this->server->save();
+
+ $this->dispatch('success', 'Server updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- refresh_server_connection($this->server->privateKey);
- $this->server->settings->wildcard_domain = $this->wildcard_domain;
- $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
+ }
+
+ public function updatedServerSettingsServerTimezone($value)
+ {
+ $this->server->settings->server_timezone = $value;
$this->server->settings->save();
- $this->server->save();
- $this->dispatch('success', 'Server updated.');
+ $this->dispatch('success', 'Server timezone updated.');
+ }
+
+ public function manualCleanup()
+ {
+ try {
+ DockerCleanupJob::dispatch($this->server, true);
+ $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function manualCloudflareConfig()
+ {
+ $this->server->settings->is_cloudflare_tunnel = true;
+ $this->server->settings->save();
+ $this->server->refresh();
+ $this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
}
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/Proxy.php b/app/Livewire/Server/Proxy.php
index 123b29d70..55d0c4966 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -39,6 +39,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
+ $this->dispatch('proxyChanged');
}
public function selectProxy($proxy_type)
@@ -47,7 +48,7 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
- if ($this->selectedProxy !== 'NONE') {
+ if ($this->server->proxySet()) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index 2279951ee..eaa312663 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
+use Illuminate\Process\InvokedProcess;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -29,6 +31,7 @@ class Deploy extends Component
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
+ 'proxyChanged' => 'proxyStatusUpdated',
];
}
@@ -94,21 +97,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
- if ($this->server->isSwarm()) {
- instant_remote_process([
- 'docker service rm coolify-proxy_traefik',
- ], $this->server);
- } else {
- instant_remote_process([
- 'docker rm -f coolify-proxy',
- ], $this->server);
+ $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+ $timeout = 30;
+
+ $process = $this->stopContainer($containerName, $timeout);
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName);
+ break;
+ }
+ usleep(100000);
}
- $this->server->proxy->status = 'exited';
- $this->server->proxy->force_stop = $forceStop;
- $this->server->save();
- $this->dispatch('proxyStatusUpdated');
+
+ $this->removeContainer($containerName);
} catch (\Throwable $e) {
return handleError($e, $this);
+ } finally {
+ $this->server->proxy->force_stop = $forceStop;
+ $this->server->proxy->status = 'exited';
+ $this->server->save();
+ $this->dispatch('proxyStatusUpdated');
}
}
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function forceStopContainer(string $containerName)
+ {
+ instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
+ }
}
diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php
index cef909a45..d70e44e55 100644
--- a/app/Livewire/Server/Proxy/Show.php
+++ b/app/Livewire/Server/Proxy/Show.php
@@ -11,7 +11,7 @@ class Show extends Component
public $parameters = [];
- protected $listeners = ['proxyStatusUpdated'];
+ protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
public function proxyStatusUpdated()
{
diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php
index d23d7fc20..f4f18381f 100644
--- a/app/Livewire/Server/Proxy/Status.php
+++ b/app/Livewire/Server/Proxy/Status.php
@@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
-use App\Jobs\ContainerStatusJob;
+use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
@@ -44,11 +44,18 @@ class Status extends Component
}
$this->numberOfPolls++;
}
- CheckProxy::run($this->server, true);
+ $shouldStart = CheckProxy::run($this->server, true);
+ if ($shouldStart) {
+ StartProxy::run($this->server, false);
+ }
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
+ } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy has exited.');
+ } elseif ($this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
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/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php
index 578a08967..92869c44b 100644
--- a/app/Livewire/Server/ShowPrivateKey.php
+++ b/app/Livewire/Server/ShowPrivateKey.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
+use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
public $parameters;
- public function setPrivateKey($newPrivateKeyId)
+ public function setPrivateKey($privateKeyId)
{
try {
- $oldPrivateKeyId = $this->server->private_key_id;
- refresh_server_connection($this->server->privateKey);
- $this->server->update([
- 'private_key_id' => $newPrivateKeyId,
- ]);
+ $privateKey = PrivateKey::findOrFail($privateKeyId);
+ $this->server->update(['private_key_id' => $privateKey->id]);
$this->server->refresh();
- refresh_server_connection($this->server->privateKey);
- $this->checkConnection();
- } catch (\Throwable $e) {
- $this->server->update([
- 'private_key_id' => $oldPrivateKeyId,
- ]);
- $this->server->refresh();
- refresh_server_connection($this->server->privateKey);
-
- return handleError($e, $this);
+ $this->dispatch('success', 'Private key updated successfully.');
+ } catch (\Exception $e) {
+ $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
}
}
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
$this->dispatch('success', 'Server is reachable.');
} else {
ray($error);
- $this->dispatch('error', 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.');
+ $this->dispatch('error', 'Server is not reachable. Check this documentation for further help. Error: '.$error);
return;
}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index f593fb78b..754f0929b 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -40,6 +40,7 @@ class Index extends Component
'settings.is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'settings.instance_timezone' => 'required|string|timezone',
];
protected $validationAttributes = [
@@ -54,10 +55,12 @@ class Index extends Component
'update_check_frequency' => 'Update Check Frequency',
];
+ public $timezones;
+
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -65,6 +68,7 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
} else {
return redirect()->route('dashboard');
}
@@ -158,7 +162,7 @@ class Index extends Component
{
CheckForUpdatesJob::dispatchSync();
$this->dispatch('updateAvailable');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->new_version_available) {
$this->dispatch('success', 'New version available!');
} else {
@@ -166,6 +170,13 @@ class Index extends Component
}
}
+ public function updatedSettingsInstanceTimezone($value)
+ {
+ $this->settings->instance_timezone = $value;
+ $this->settings->save();
+ $this->dispatch('success', 'Instance timezone updated.');
+ }
+
public function render()
{
return view('livewire.settings.index');
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index f9402fd7b..ca0c9c1ae 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -29,7 +29,7 @@ class License extends Component
abort(404);
}
$this->instance_id = config('app.id');
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
}
public function render()
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 99b8f8d49..9240aa96d 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -42,7 +42,7 @@ class SettingsBackup extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 3eb8ea646..4515df9a7 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -43,7 +43,7 @@ class SettingsEmail extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->emails = auth()->user()->email;
} else {
return redirect()->route('dashboard');
diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php
index e025d8f7c..daf1df212 100644
--- a/app/Livewire/SharedVariables/Environment/Show.php
+++ b/app/Livewire/SharedVariables/Environment/Show.php
@@ -16,7 +16,7 @@ class Show extends Component
public array $parameters;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey'];
public function saveKey($data)
{
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 75d7fd04a..193b650ff 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -99,7 +99,7 @@ class Change extends Component
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php
index a05834ecc..c5250e1e3 100644
--- a/app/Livewire/Storage/Create.php
+++ b/app/Livewire/Storage/Create.php
@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint',
];
- public function mount()
+ public function updatedEndpoint($value)
{
- if (isDev()) {
- $this->name = 'Local MinIO';
- $this->description = 'Local MinIO';
- $this->key = 'minioadmin';
- $this->secret = 'minioadmin';
- $this->bucket = 'local';
- $this->endpoint = 'http://coolify-minio:9000';
+ if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
+ $this->endpoint = 'https://'.$value;
+ $value = $this->endpoint;
+ }
+
+ if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
+ $this->bucket = str($value)->after('//')->before('.');
+ } elseif (str($value)->contains('your-objectstorage.com')) {
+ $this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
}
}
diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php
index c278bf58e..df450cf7e 100644
--- a/app/Livewire/Subscription/Index.php
+++ b/app/Livewire/Subscription/Index.php
@@ -23,7 +23,7 @@ class Index extends Component
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 97d4fcdbf..3026cb297 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
@@ -73,8 +75,13 @@ class AdminView extends Component
$team->delete();
}
- public function delete($id)
+ public function delete($id, $password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}
diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php
new file mode 100644
index 000000000..945b25714
--- /dev/null
+++ b/app/Livewire/Terminal/Index.php
@@ -0,0 +1,76 @@
+user()->isAdmin()) {
+ abort(403);
+ }
+ $this->servers = Server::isReachable()->get();
+ $this->containers = $this->getAllActiveContainers();
+ }
+
+ private function getAllActiveContainers()
+ {
+ return collect($this->servers)->flatMap(function ($server) {
+ if (! $server->isFunctional()) {
+ return [];
+ }
+
+ return $server->loadAllContainers()->map(function ($container) use ($server) {
+ $state = data_get_str($container, 'State')->lower();
+ if ($state->contains('running')) {
+ return [
+ 'name' => data_get($container, 'Names'),
+ 'connection_name' => data_get($container, 'Names'),
+ 'uuid' => data_get($container, 'Names'),
+ 'status' => data_get_str($container, 'State')->lower(),
+ 'server' => $server,
+ 'server_uuid' => $server->uuid,
+ ];
+ }
+
+ return null;
+ })->filter();
+ });
+ }
+
+ public function updatedSelectedUuid()
+ {
+ $this->connectToContainer();
+ }
+
+ #[On('connectToContainer')]
+ public function connectToContainer()
+ {
+ if ($this->selected_uuid === 'default') {
+ $this->dispatch('error', 'Please select a server or a container.');
+
+ return;
+ }
+ $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
+ $this->dispatch('send-terminal-command',
+ isset($container),
+ $container['connection_name'] ?? $this->selected_uuid,
+ $container['server_uuid'] ?? $this->selected_uuid
+ );
+ }
+
+ public function render()
+ {
+ return view('livewire.terminal.index');
+ }
+}
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/Application.php b/app/Models/Application.php
index e2871da4b..e4ab3918a 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -102,6 +104,8 @@ class Application extends BaseModel
{
use SoftDeletes;
+ private static $parserVersion = '4';
+
protected $guarded = [];
protected $appends = ['server_status'];
@@ -125,7 +129,7 @@ class Application extends BaseModel
ApplicationSetting::create([
'application_id' => $application->id,
]);
- $application->compose_parsing_version = '2';
+ $application->compose_parsing_version = self::$parserVersion;
$application->save();
});
static::forceDeleting(function ($application) {
@@ -138,6 +142,10 @@ class Application extends BaseModel
$task->delete();
}
$application->tags()->detach();
+ $application->previews()->delete();
+ foreach ($application->deployment_queue as $deployment) {
+ $deployment->delete();
+ }
});
}
@@ -146,12 +154,64 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public function getContainersToStop(bool $previewDeployments = false): array
+ {
+ $containers = $previewDeployments
+ ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
+ : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
+
+ return $containers->pluck('Names')->toArray();
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 600)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach ($finishedProcesses as $containerName => $process) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- ray('Deleting workdir');
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
@@ -173,6 +233,13 @@ class Application extends BaseModel
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -240,7 +307,7 @@ class Application extends BaseModel
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$url = Url::fromString($route);
$url = $url->withPort(null);
@@ -412,23 +479,6 @@ class Application extends BaseModel
);
}
- public function dockerComposePrLocation(): Attribute
- {
- return Attribute::make(
- set: function ($value) {
- if (is_null($value) || $value === '') {
- return '/docker-compose.yaml';
- } else {
- if ($value !== '/') {
- return Str::start(Str::replaceEnd('/', '', $value), '/');
- }
-
- return Str::start($value, '/');
- }
- }
- );
- }
-
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -479,12 +529,12 @@ class Application extends BaseModel
$main_server_status = $this->destination->server->isFunctional();
foreach ($additional_servers_status as $status) {
$server_status = str($status)->before(':')->value();
- if ($main_server_status !== $server_status) {
+ if ($server_status !== 'running') {
return false;
}
}
- return true;
+ return $main_server_status;
}
}
);
@@ -663,6 +713,11 @@ class Application extends BaseModel
return $this->hasMany(ApplicationPreview::class);
}
+ public function deployment_queue()
+ {
+ return $this->hasMany(ApplicationDeploymentQueue::class);
+ }
+
public function destination()
{
return $this->morphTo();
@@ -1040,7 +1095,7 @@ class Application extends BaseModel
}
}
- public function parseRawCompose()
+ public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1048,6 +1103,7 @@ class Application extends BaseModel
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
+
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -1100,9 +1156,11 @@ class Application extends BaseModel
instant_remote_process($commands, $this->destination->server, false);
}
- public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null)
+ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($this->docker_compose_raw) {
+ if ((int) $this->compose_parsing_version >= 3) {
+ return newParser($this, $pull_request_id, $preview_id);
+ } elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
} else {
return collect([]);
@@ -1154,7 +1212,7 @@ class Application extends BaseModel
if ($composeFileContent) {
$this->docker_compose_raw = $composeFileContent;
$this->save();
- $parsedServices = $this->parseCompose();
+ $parsedServices = $this->parse();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
@@ -1178,7 +1236,6 @@ class Application extends BaseModel
} else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
-
}
public function parseContainerLabels(?ApplicationPreview $preview = null)
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 90d7608cc..c261c30c6 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
+ public function application(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => Application::find($this->application_id),
+ );
+ }
+
+ public function server(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => Server::find($this->server_id),
+ );
+ }
+
public function setStatus(string $status)
{
$this->update([
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index 57d20e3aa..04a0ab27e 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel
protected static function booted()
{
static::deleting(function ($preview) {
- if ($preview->application->build_pack === 'dockercompose') {
+ if (data_get($preview, 'application.build_pack') === 'dockercompose') {
$server = $preview->application->destination->server;
- $composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
+ $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 5e1d8ae13..9f8e4b342 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -6,7 +6,6 @@ use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
-use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -97,8 +96,22 @@ class EnvironmentVariable extends Model
$resource = Application::find($this->application_id);
} elseif ($this->service_id) {
$resource = Service::find($this->service_id);
- } elseif ($this->database_id) {
- $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
+ } elseif ($this->standalone_postgresql_id) {
+ $resource = StandalonePostgresql::find($this->standalone_postgresql_id);
+ } elseif ($this->standalone_redis_id) {
+ $resource = StandaloneRedis::find($this->standalone_redis_id);
+ } elseif ($this->standalone_mongodb_id) {
+ $resource = StandaloneMongodb::find($this->standalone_mongodb_id);
+ } elseif ($this->standalone_mysql_id) {
+ $resource = StandaloneMysql::find($this->standalone_mysql_id);
+ } elseif ($this->standalone_mariadb_id) {
+ $resource = StandaloneMariadb::find($this->standalone_mariadb_id);
+ } elseif ($this->standalone_keydb_id) {
+ $resource = StandaloneKeydb::find($this->standalone_keydb_id);
+ } elseif ($this->standalone_dragonfly_id) {
+ $resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
+ } elseif ($this->standalone_clickhouse_id) {
+ $resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
}
return $resource;
@@ -113,68 +126,6 @@ class EnvironmentVariable extends Model
$env = $this->get_real_environment_variables($this->value, $resource);
return data_get($env, 'value', $env);
- if (is_string($env)) {
- return $env;
- }
-
- return $env->value;
- }
- );
- }
-
- protected function isFoundInCompose(): Attribute
- {
- return Attribute::make(
- get: function () {
- if (! $this->application_id) {
- return true;
- }
- $found_in_compose = false;
- $found_in_args = false;
- $resource = $this->resource();
- $compose = data_get($resource, 'docker_compose_raw');
- if (! $compose) {
- return true;
- }
- $yaml = Yaml::parse($compose);
- $services = collect(data_get($yaml, 'services'));
- if ($services->isEmpty()) {
- return false;
- }
- foreach ($services as $service) {
- $environments = collect(data_get($service, 'environment'));
- $args = collect(data_get($service, 'build.args'));
- if ($environments->isEmpty() && $args->isEmpty()) {
- $found_in_compose = false;
- break;
- }
-
- $found_in_compose = $environments->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_compose) {
- break;
- }
-
- $found_in_args = $args->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_args) {
- break;
- }
- }
-
- return $found_in_compose || $found_in_args;
}
);
}
@@ -201,8 +152,10 @@ class EnvironmentVariable extends Model
$environment_variable = trim($environment_variable);
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
if ($sharedEnvsFound->isEmpty()) {
+
return $environment_variable;
}
+
foreach ($sharedEnvsFound as $sharedEnv) {
$type = str($sharedEnv)->match('/(.*?)\./');
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 5bd421956..bb3d1478b 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -37,6 +37,30 @@ class InstanceSettings extends Model implements SendsEmail
);
}
+ public function updateCheckFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
+ public function autoUpdateFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
public static function get()
{
return InstanceSettings::findOrFail(0);
@@ -61,4 +85,17 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]";
}
+
+ public function helperVersion(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ if (isDev()) {
+ return 'latest';
+ }
+
+ return $value;
+ }
+ );
+ }
}
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index a436f5797..d528099ff 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -24,8 +24,9 @@ class LocalFileVolume extends BaseModel
return $this->morphTo('resource');
}
- public function deleteStorageOnServer()
+ public function loadStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -35,17 +36,46 @@ class LocalFileVolume extends BaseModel
$server = $this->resource->destination->server;
}
$commands = collect([]);
- $fs_path = data_get($this, 'fs_path');
- $isFile = instant_remote_process(["test -f $fs_path && echo OK || echo NOK"], $server);
- $isDir = instant_remote_process(["test -d $fs_path && echo OK || echo NOK"], $server);
- if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
- ray($isFile, $isDir);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ if ($isFile === 'OK') {
+ $content = instant_remote_process(["cat $path"], $server, false);
+ $this->content = $content;
+ $this->is_directory = false;
+ $this->save();
+ }
+ }
+
+ public function deleteStorageOnServer()
+ {
+ $this->load(['service']);
+ $isService = data_get($this->resource, 'service');
+ if ($isService) {
+ $workdir = $this->resource->service->workdir();
+ $server = $this->resource->service->server;
+ } else {
+ $workdir = $this->resource->workdir();
+ $server = $this->resource->destination->server;
+ }
+ $commands = collect([]);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
+ if ($path && $path != '/' && $path != '.' && $path != '..') {
if ($isFile === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
} elseif ($isDir === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
- $commands->push("rmdir $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
+ $commands->push("rmdir $path > /dev/null 2>&1 || true");
}
}
if ($commands->count() > 0) {
@@ -55,6 +85,7 @@ class LocalFileVolume extends BaseModel
public function saveStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -74,30 +105,36 @@ class LocalFileVolume extends BaseModel
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
}
}
- $fileVolume = $this;
- $path = str(data_get($fileVolume, 'fs_path'));
- $content = data_get($fileVolume, 'content');
+ $path = data_get_str($this, 'fs_path');
+ $content = data_get($this, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$path = $workdir.$path;
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
- if ($isFile == 'OK' && $fileVolume->is_directory) {
+ if ($isFile == 'OK' && $this->is_directory) {
$content = instant_remote_process(["cat $path"], $server, false);
- $fileVolume->is_directory = false;
- $fileVolume->content = $content;
- $fileVolume->save();
+ $this->is_directory = false;
+ $this->content = $content;
+ $this->save();
FileStorageChanged::dispatch(data_get($server, 'team_id'));
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
- } elseif ($isDir == 'OK' && ! $fileVolume->is_directory) {
- $fileVolume->is_directory = true;
- $fileVolume->save();
- throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.');
+ } elseif ($isDir == 'OK' && ! $this->is_directory) {
+ if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) {
+ $this->is_directory = true;
+ $this->save();
+ throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.');
+ }
+ instant_remote_process([
+ "rm -fr $path",
+ "touch $path",
+ ], $server, false);
+ FileStorageChanged::dispatch(data_get($server, 'team_id'));
}
- if ($isDir == 'NOK' && ! $fileVolume->is_directory) {
- $chmod = data_get($fileVolume, 'chmod');
- $chown = data_get($fileVolume, 'chown');
+ if ($isDir == 'NOK' && ! $this->is_directory) {
+ $chmod = data_get($this, 'chmod');
+ $chown = data_get($this, 'chown');
if ($content) {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
@@ -111,7 +148,7 @@ class LocalFileVolume extends BaseModel
if ($chmod) {
$commands->push("chmod $chmod $path");
}
- } elseif ($isDir == 'NOK' && $fileVolume->is_directory) {
+ } elseif ($isDir == 'NOK' && $this->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index 45bc6bc84..065746ede 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -2,6 +2,9 @@
namespace App\Models;
+use DanHarrin\LivewireRateLimiting\WithRateLimiting;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Validation\ValidationException;
use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader;
@@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader;
)]
class PrivateKey extends BaseModel
{
+ use WithRateLimiting;
+
protected $fillable = [
'name',
'description',
'private_key',
'is_git_related',
'team_id',
+ 'fingerprint',
+ ];
+
+ protected $casts = [
+ 'private_key' => 'encrypted',
];
protected static function booted()
{
static::saving(function ($key) {
- $privateKey = data_get($key, 'private_key');
- if (substr($privateKey, -1) !== "\n") {
- $key->private_key = $privateKey."\n";
+ $key->private_key = formatPrivateKey($key->private_key);
+
+ if (! self::validatePrivateKey($key->private_key)) {
+ throw ValidationException::withMessages([
+ 'private_key' => ['The private key is invalid.'],
+ ]);
+ }
+
+ $key->fingerprint = self::generateFingerprint($key->private_key);
+ if (self::fingerprintExists($key->fingerprint, $key->id)) {
+ throw ValidationException::withMessages([
+ 'private_key' => ['This private key already exists.'],
+ ]);
}
});
+ static::deleted(function ($key) {
+ self::deleteFromStorage($key);
+ });
+ }
+
+ public function getPublicKey()
+ {
+ return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
}
public static function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
- return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
+ return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
- public function publicKey()
+ public static function validatePrivateKey($privateKey)
{
try {
- return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
+ PublicKeyLoader::load($privateKey);
+
+ return true;
} catch (\Throwable $e) {
- return 'Error loading private key';
+ return false;
}
}
- public function isEmpty()
+ public static function createAndStore(array $data)
{
- if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
- return true;
- }
+ $privateKey = new self($data);
+ $privateKey->save();
+ $privateKey->storeInFileSystem();
- return false;
+ return $privateKey;
+ }
+
+ public static function generateNewKeyPair($type = 'rsa')
+ {
+ try {
+ $instance = new self;
+ $instance->rateLimit(10);
+ $name = generate_random_name();
+ $description = 'Created by Coolify';
+ $keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
+
+ return [
+ 'name' => $name,
+ 'description' => $description,
+ 'private_key' => $keyPair['private'],
+ 'public_key' => $keyPair['public'],
+ ];
+ } catch (\Throwable $e) {
+ throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
+ }
+ }
+
+ public static function extractPublicKeyFromPrivate($privateKey)
+ {
+ try {
+ $key = PublicKeyLoader::load($privateKey);
+
+ return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+
+ public static function validateAndExtractPublicKey($privateKey)
+ {
+ $isValid = self::validatePrivateKey($privateKey);
+ $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
+
+ return [
+ 'isValid' => $isValid,
+ 'publicKey' => $publicKey,
+ ];
+ }
+
+ public function storeInFileSystem()
+ {
+ $filename = "ssh_key@{$this->uuid}";
+ Storage::disk('ssh-keys')->put($filename, $this->private_key);
+
+ return "/var/www/html/storage/app/ssh/keys/{$filename}";
+ }
+
+ public static function deleteFromStorage(self $privateKey)
+ {
+ $filename = "ssh_key@{$privateKey->uuid}";
+ Storage::disk('ssh-keys')->delete($filename);
+ }
+
+ public function getKeyLocation()
+ {
+ return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
+ }
+
+ public function updatePrivateKey(array $data)
+ {
+ $this->update($data);
+ $this->storeInFileSystem();
+
+ return $this;
}
public function servers()
@@ -85,4 +184,53 @@ class PrivateKey extends BaseModel
{
return $this->hasMany(GitlabApp::class);
}
+
+ public function isInUse()
+ {
+ return $this->servers()->exists()
+ || $this->applications()->exists()
+ || $this->githubApps()->exists()
+ || $this->gitlabApps()->exists();
+ }
+
+ public function safeDelete()
+ {
+ if (! $this->isInUse()) {
+ $this->delete();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function generateFingerprint($privateKey)
+ {
+ try {
+ $key = PublicKeyLoader::load($privateKey);
+ $publicKey = $key->getPublicKey();
+
+ return $publicKey->getFingerprint('sha256');
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+
+ private static function fingerprintExists($fingerprint, $excludeId = null)
+ {
+ $query = self::where('fingerprint', $fingerprint);
+
+ if (! is_null($excludeId)) {
+ $query->where('id', '!=', $excludeId);
+ }
+
+ return $query->exists();
+ }
+
+ public static function cleanupUnusedKeys()
+ {
+ self::ownedByCurrentTeam()->each(function ($privateKey) {
+ $privateKey->safeDelete();
+ });
+ }
}
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 77f62d770..5a9dd964a 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',
@@ -23,9 +24,11 @@ class Project extends BaseModel
{
protected $guarded = [];
+ protected $appends = ['default_environment'];
+
public static function ownedByCurrentTeam()
{
- return Project::whereTeamId(currentTeam()->id)->orderBy('name');
+ return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
}
protected static function booted()
@@ -130,7 +133,7 @@ class Project extends BaseModel
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
- public function default_environment()
+ public function getDefaultEnvironmentAttribute()
{
$default = $this->environments()->where('name', 'production')->first();
if ($default) {
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index 4c7faaa6f..a432a6e9c 100644
--- a/app/Models/S3Storage.php
+++ b/app/Models/S3Storage.php
@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}";
}
+ public function isHetzner()
+ {
+ return str($this->endpoint)->contains('your-objectstorage.com');
+ }
+
+ public function isDigitalOcean()
+ {
+ return str($this->endpoint)->contains('digitaloceanspaces.com');
+ }
+
public function testConnection(bool $shouldSave = false)
{
try {
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index edd840e7d..ce5d3a87f 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()
@@ -34,4 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
+
+ public function server()
+ {
+ if ($this->database) {
+ if ($this->database->destination && $this->database->destination->server) {
+ $server = $this->database->destination->server;
+
+ return $server;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 1cb805e8e..3cee5a875 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -26,6 +26,32 @@ class ScheduledTask extends BaseModel
public function executions(): HasMany
{
- return $this->hasMany(ScheduledTaskExecution::class);
+ // Last execution first
+ return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc');
+ }
+
+ public function server()
+ {
+ if ($this->application) {
+ if ($this->application->destination && $this->application->destination->server) {
+ $server = $this->application->destination->server;
+
+ return $server;
+ }
+ } elseif ($this->service) {
+ if ($this->service->destination && $this->service->destination->server) {
+ $server = $this->service->destination->server;
+
+ return $server;
+ }
+ } elseif ($this->database) {
+ if ($this->database->destination && $this->database->destination->server) {
+ $server = $this->database->destination->server;
+
+ return $server;
+ }
+ }
+
+ return null;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 8a7325beb..8864deef1 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -5,7 +5,6 @@ namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
-use App\Notifications\Server\Revived;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection;
@@ -37,6 +36,8 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'],
+ 'delete_unused_volumes' => ['type' => 'boolean'],
+ 'delete_unused_networks' => ['type' => 'boolean'],
]
)]
@@ -106,12 +107,24 @@ class Server extends BaseModel
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
+ 'delete_unused_volumes' => 'boolean',
+ 'delete_unused_networks' => 'boolean',
];
protected $schemalessAttributes = [
'proxy',
];
+ protected $fillable = [
+ 'name',
+ 'ip',
+ 'port',
+ 'user',
+ 'description',
+ 'private_key_id',
+ 'team_id',
+ ];
+
protected $guarded = [];
public static function isReachable()
@@ -146,6 +159,11 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class);
}
+ public function proxySet()
+ {
+ return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
+ }
+
public function setupDefault404Redirect()
{
$dynamic_conf_path = $this->proxyPath().'/dynamic';
@@ -153,11 +171,11 @@ class Server extends BaseModel
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
- } elseif ($proxy_type === 'CADDY') {
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
- if ($proxy_type === 'CADDY') {
+ if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
@@ -227,7 +245,7 @@ respond 404
$conf;
$base64 = base64_encode($conf);
- } elseif ($proxy_type === 'CADDY') {
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
@@ -243,9 +261,6 @@ respond 404
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
- if (config('app.env') == 'local') {
- ray($conf);
- }
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -253,7 +268,7 @@ respond 404
public function setupDynamicProxyConfiguration()
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$dynamic_config_path = $this->proxyPath().'/dynamic';
if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml";
@@ -295,6 +310,13 @@ respond 404
'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
],
+ 'coolify-terminal-ws' => [
+ 'entryPoints' => [
+ 0 => 'http',
+ ],
+ 'service' => 'coolify-terminal',
+ 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
+ ],
],
'services' => [
'coolify' => [
@@ -315,6 +337,15 @@ respond 404
],
],
],
+ 'coolify-terminal' => [
+ 'loadBalancer' => [
+ 'servers' => [
+ 0 => [
+ 'url' => 'http://coolify-realtime:6002',
+ ],
+ ],
+ ],
+ ],
],
],
];
@@ -344,6 +375,16 @@ respond 404
'certresolver' => 'letsencrypt',
],
];
+ $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [
+ 'entryPoints' => [
+ 0 => 'https',
+ ],
+ 'service' => 'coolify-terminal',
+ 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
+ 'tls' => [
+ 'certresolver' => 'letsencrypt',
+ ],
+ ];
}
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml =
@@ -377,6 +418,9 @@ $schema://$host {
handle /app/* {
reverse_proxy coolify-realtime:6001
}
+ handle /terminal/ws {
+ reverse_proxy coolify-realtime:6002
+ }
reverse_proxy coolify:80
}";
$base64 = base64_encode($caddy_file);
@@ -404,11 +448,19 @@ $schema://$host {
// Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK->value) {
- $proxy_path = $proxy_path;
+ // Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) {
- $proxy_path = $proxy_path.'/caddy';
+ if (isDev()) {
+ $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
+ } else {
+ $proxy_path = $proxy_path.'/caddy';
+ }
} elseif ($proxyType === ProxyTypes::NGINX->value) {
- $proxy_path = $proxy_path.'/nginx';
+ if (isDev()) {
+ $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
+ } else {
+ $proxy_path = $proxy_path.'/nginx';
+ }
}
return $proxy_path;
@@ -416,15 +468,6 @@ $schema://$host {
public function proxyType()
{
- // $proxyType = $this->proxy->get('type');
- // if ($proxyType === ProxyTypes::NONE->value) {
- // return $proxyType;
- // }
- // if (is_null($proxyType)) {
- // $this->proxy->type = ProxyTypes::TRAEFIK->value;
- // $this->proxy->status = ProxyStatus::EXITED->value;
- // $this->save();
- // }
return data_get($this->proxy, 'type');
}
@@ -649,7 +692,7 @@ $schema://$host {
}
}
- public function getDiskUsage()
+ public function getDiskUsage(): ?string
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
@@ -736,6 +779,18 @@ $schema://$host {
}
}
+ public function loadAllContainers(): Collection
+ {
+ if ($this->isFunctional()) {
+ $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
+ $containers = format_docker_command_output_to_json($containers);
+
+ return collect($containers);
+ }
+
+ return collect([]);
+ }
+
public function loadUnmanagedContainers(): Collection
{
if ($this->isFunctional()) {
@@ -782,9 +837,9 @@ $schema://$host {
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
- })->filter(function ($item) {
+ })->flatten()->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db';
- })->flatten();
+ });
}
public function applications()
@@ -828,6 +883,35 @@ $schema://$host {
return $this->hasMany(Service::class);
}
+ public function port(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ return preg_replace('/[^0-9]/', '', $value);
+ }
+ );
+ }
+
+ public function user(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
+
+ return $sanitizedValue;
+ }
+ );
+ }
+
+ public function ip(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
+ }
+ );
+ }
+
public function getIp(): Attribute
{
return Attribute::make(
@@ -880,7 +964,7 @@ $schema://$host {
public function muxFilename()
{
- return "{$this->ip}_{$this->port}_{$this->user}";
+ return $this->uuid;
}
public function team()
@@ -900,10 +984,9 @@ $schema://$host {
public function isFunctional()
{
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
- ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
+
if (! $isFunctional) {
- Storage::disk('ssh-keys')->delete($private_key_filename);
- Storage::disk('ssh-mux')->delete($mux_filename);
+ Storage::disk('ssh-mux')->delete($this->muxFilename());
}
return $isFunctional;
@@ -955,9 +1038,10 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection()
+ public function validateConnection($isManualCheck = true)
{
- config()->set('coolify.mux_enabled', false);
+ config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
+ // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
$server = Server::find($this->id);
if (! $server) {
@@ -967,7 +1051,10 @@ $schema://$host {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
- // EC2 does not have `uptime` command, lol
+ // Make sure the private key is stored
+ if ($server->privateKey) {
+ $server->privateKey->storeInFileSystem();
+ }
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,
@@ -976,7 +1063,6 @@ $schema://$host {
'unreachable_count' => 0,
]);
if (data_get($server, 'unreachable_notification_sent') === true) {
- // $server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]);
}
@@ -1105,4 +1191,38 @@ $schema://$host {
{
return $this->settings->is_build_server;
}
+
+ public static function createWithPrivateKey(array $data, PrivateKey $privateKey)
+ {
+ $server = new self($data);
+ $server->privateKey()->associate($privateKey);
+ $server->save();
+
+ return $server;
+ }
+
+ public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null)
+ {
+ $this->update($data);
+ if ($privateKey) {
+ $this->privateKey()->associate($privateKey);
+ $this->save();
+ }
+
+ return $this;
+ }
+
+ public function storageCheck(): ?string
+ {
+ $commands = [
+ 'df / --output=pcent | tr -cd 0-9',
+ ];
+
+ return instant_remote_process($commands, $this, false);
+ }
+
+ public function isIpv6(): bool
+ {
+ return str($this->ip)->contains(':');
+ }
}
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index c39982b91..c44a393b4 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
@@ -10,10 +11,10 @@ use OpenApi\Attributes as OA;
type: 'object',
properties: [
'id' => ['type' => 'integer'],
- 'cleanup_after_percentage' => ['type' => 'integer'],
'concurrent_builds' => ['type' => 'integer'],
'dynamic_timeout' => ['type' => 'integer'],
'force_disabled' => ['type' => 'boolean'],
+ 'force_server_cleanup' => ['type' => 'boolean'],
'is_build_server' => ['type' => 'boolean'],
'is_cloudflare_tunnel' => ['type' => 'boolean'],
'is_jump_server' => ['type' => 'boolean'],
@@ -37,6 +38,8 @@ use OpenApi\Attributes as OA;
'metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'],
+ 'docker_cleanup_frequency' => ['type' => 'string'],
+ 'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
@@ -47,8 +50,25 @@ class ServerSetting extends Model
{
protected $guarded = [];
+ protected $casts = [
+ 'force_docker_cleanup' => 'boolean',
+ 'docker_cleanup_threshold' => 'integer',
+ ];
+
public function server()
{
return $this->belongsTo(Server::class);
}
+
+ public function dockerCleanupFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 33238281e..bcdb74f8c 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
+use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
-use Symfony\Component\Yaml\Yaml;
+use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Service model',
@@ -23,6 +26,7 @@ use Symfony\Component\Yaml\Yaml;
'description' => ['type' => 'string', 'description' => 'The description of the service.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'],
'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'],
+ 'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'],
@@ -38,10 +42,20 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
+ private static $parserVersion = '4';
+
protected $guarded = [];
protected $appends = ['server_status'];
+ protected static function booted()
+ {
+ static::created(function ($service) {
+ $service->compose_parsing_version = self::$parserVersion;
+ $service->save();
+ });
+ }
+
public function isConfigurationChanged(bool $save = false)
{
$domains = $this->applications()->get()->pluck('fqdn')->sort()->toArray();
@@ -119,15 +133,81 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable');
}
+ public function getContainersToStop(): array
+ {
+ $containersToStop = [];
+ $applications = $this->applications()->get();
+ foreach ($applications as $application) {
+ $containersToStop[] = "{$application->name}-{$this->uuid}";
+ }
+ $dbs = $this->databases()->get();
+ foreach ($dbs as $db) {
+ $containersToStop[] = "{$db->name}-{$this->uuid}";
+ }
+
+ return $containersToStop;
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 300)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
- $server = data_get($this, 'server');
+ $server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function status()
{
$applications = $this->applications;
@@ -203,9 +283,182 @@ class Service extends BaseModel
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
- $image = str($application->image)->before(':')->value();
+ $image = str($application->image)->before(':');
+ if ($image->isEmpty()) {
+ continue;
+ }
switch ($image) {
- case str($image)?->contains('tolgee'):
+ case $image->contains('label-studio'):
+ $data = collect([]);
+ $username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first();
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => 'LABEL_STUDIO_USERNAME',
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => 'LABEL_STUDIO_PASSWORD',
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Label Studio', $data->toArray());
+ break;
+ case $image->contains('litellm'):
+ $data = collect([]);
+ $username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first();
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Litellm', $data->toArray());
+ break;
+ case $image->contains('langfuse'):
+ $data = collect([]);
+ $email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first();
+ if ($email) {
+ $data = $data->merge([
+ 'Admin Email' => [
+ 'key' => 'LANGFUSE_INIT_USER_EMAIL',
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
+ ray('password', $password);
+ if ($password) {
+ $data = $data->merge([
+ 'Admin Password' => [
+ 'key' => 'LANGFUSE_INIT_USER_PASSWORD',
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Langfuse', $data->toArray());
+ break;
+ case $image->contains('invoiceninja'):
+ $data = collect([]);
+ $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => 'IN_USER_EMAIL',
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => 'IN_PASSWORD',
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ $fields->put('Invoice Ninja', $data->toArray());
+ break;
+ case $image->contains('argilla'):
+ $data = collect([]);
+ $api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first();
+ $data = $data->merge([
+ 'API Key' => [
+ 'key' => data_get($api_key, 'key'),
+ 'value' => data_get($api_key, 'value'),
+ 'isPassword' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ $data = $data->merge([
+ 'API Key' => [
+ 'key' => data_get($api_key, 'key'),
+ 'value' => data_get($api_key, 'value'),
+ 'isPassword' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ $username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first();
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first();
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ $fields->put('Argilla', $data->toArray());
+ break;
+ case $image->contains('rabbitmq'):
+ $data = collect([]);
+ $host_port = $this->environment_variables()->where('key', 'PORT')->first();
+ $username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_RABBITMQ')->first();
+ if ($host_port) {
+ $data = $data->merge([
+ 'Host Port Binding' => [
+ 'key' => data_get($host_port, 'key'),
+ 'value' => data_get($host_port, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('RabbitMQ', $data->toArray());
+ break;
+ case $image->contains('tolgee'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
$data = $data->merge([
@@ -228,7 +481,7 @@ class Service extends BaseModel
}
$fields->put('Tolgee', $data->toArray());
break;
- case str($image)?->contains('logto'):
+ case $image->contains('logto'):
$data = collect([]);
$logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first();
$logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
@@ -252,7 +505,7 @@ class Service extends BaseModel
}
$fields->put('Logto', $data->toArray());
break;
- case str($image)?->contains('unleash-server'):
+ case $image->contains('unleash-server'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first();
$data = $data->merge([
@@ -275,7 +528,7 @@ class Service extends BaseModel
}
$fields->put('Unleash', $data->toArray());
break;
- case str($image)?->contains('grafana'):
+ case $image->contains('grafana'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
$data = $data->merge([
@@ -298,7 +551,7 @@ class Service extends BaseModel
}
$fields->put('Grafana', $data->toArray());
break;
- case str($image)?->contains('directus'):
+ case $image->contains('directus'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -324,7 +577,7 @@ class Service extends BaseModel
}
$fields->put('Directus', $data->toArray());
break;
- case str($image)?->contains('kong'):
+ case $image->contains('kong'):
$data = collect([]);
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -348,7 +601,7 @@ class Service extends BaseModel
]);
}
$fields->put('Supabase', $data->toArray());
- case str($image)?->contains('minio'):
+ case $image->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
@@ -401,7 +654,7 @@ class Service extends BaseModel
$fields->put('MinIO', $data->toArray());
break;
- case str($image)?->contains('weblate'):
+ case $image->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
@@ -427,7 +680,7 @@ class Service extends BaseModel
}
$fields->put('Weblate', $data->toArray());
break;
- case str($image)?->contains('meilisearch'):
+ case $image->contains('meilisearch'):
$data = collect([]);
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
if ($SERVICE_PASSWORD_MEILISEARCH) {
@@ -441,7 +694,7 @@ class Service extends BaseModel
}
$fields->put('Meilisearch', $data->toArray());
break;
- case str($image)?->contains('ghost'):
+ case $image->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
@@ -501,33 +754,8 @@ class Service extends BaseModel
$fields->put('Ghost', $data->toArray());
break;
- default:
- $data = collect([]);
- $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
- $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
- if ($admin_user) {
- $data = $data->merge([
- 'User' => [
- 'key' => 'SERVICE_USER_ADMIN',
- 'value' => data_get($admin_user, 'value', 'admin'),
- 'readonly' => true,
- 'rules' => 'required',
- ],
- ]);
- }
- if ($admin_password) {
- $data = $data->merge([
- 'Password' => [
- 'key' => 'SERVICE_PASSWORD_ADMIN',
- 'value' => data_get($admin_password, 'value'),
- 'rules' => 'required',
- 'isPassword' => true,
- ],
- ]);
- }
- $fields->put('Admin', $data->toArray());
- break;
- case str($image)?->contains('vaultwarden'):
+
+ case $image->contains('vaultwarden'):
$data = collect([]);
$DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first();
@@ -593,7 +821,7 @@ class Service extends BaseModel
$fields->put('Vaultwarden', $data);
break;
- case str($image)->contains('gitlab/gitlab'):
+ case $image->contains('gitlab/gitlab'):
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first();
$data = collect([]);
if ($password) {
@@ -608,7 +836,7 @@ class Service extends BaseModel
}
$data = $data->merge([
'Root User' => [
- 'key' => 'N/A',
+ 'key' => 'GITLAB_ROOT_USER',
'value' => 'root',
'rules' => 'required',
'isPassword' => true,
@@ -617,14 +845,104 @@ class Service extends BaseModel
$fields->put('GitLab', $data->toArray());
break;
+ case $image->contains('code-server'):
+ $data = collect([]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first();
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $sudoPassword = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_SUDOCODESERVER')->first();
+ if ($sudoPassword) {
+ $data = $data->merge([
+ 'Sudo Password' => [
+ 'key' => data_get($sudoPassword, 'key'),
+ 'value' => data_get($sudoPassword, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Code Server', $data->toArray());
+ break;
+ case $image->contains('elestio/strapi'):
+ $data = collect([]);
+ $license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
+ if ($license) {
+ $data = $data->merge([
+ 'License' => [
+ 'key' => data_get($license, 'key'),
+ 'value' => data_get($license, 'value'),
+ ],
+ ]);
+ }
+ $nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
+ if ($nodeEnv) {
+ $data = $data->merge([
+ 'Node Environment' => [
+ 'key' => data_get($nodeEnv, 'key'),
+ 'value' => data_get($nodeEnv, 'value'),
+ ],
+ ]);
+ }
+
+ $fields->put('Strapi', $data->toArray());
+ break;
+ default:
+ $data = collect([]);
+ $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
+ // Chaskiq
+ $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
+
+ $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
+ if ($admin_user) {
+ $data = $data->merge([
+ 'User' => [
+ 'key' => 'SERVICE_USER_ADMIN',
+ 'value' => data_get($admin_user, 'value', 'admin'),
+ 'readonly' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($admin_password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => 'SERVICE_PASSWORD_ADMIN',
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ if ($admin_email) {
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => 'ADMIN_EMAIL',
+ 'value' => data_get($admin_email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ $fields->put('Admin', $data->toArray());
+ break;
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
- $image = str($database->image)->before(':')->value();
+ $image = str($database->image)->before(':');
+ if ($image->isEmpty()) {
+ continue;
+ }
switch ($image) {
- case str($image)->contains('postgres'):
+ case $image->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
@@ -662,9 +980,9 @@ class Service extends BaseModel
}
$fields->put('PostgreSQL', $data->toArray());
break;
- case str($image)->contains('mysql'):
- $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
+ case $image->contains('mysql'):
+ $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
@@ -712,11 +1030,11 @@ class Service extends BaseModel
}
$fields->put('MySQL', $data->toArray());
break;
- case str($image)->contains('mariadb'):
- $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
- $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
+ case $image->contains('mariadb'):
+ $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
+ $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
+ $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
@@ -763,6 +1081,7 @@ class Service extends BaseModel
}
$fields->put('MariaDB', $data->toArray());
break;
+
}
}
@@ -897,12 +1216,13 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
- return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
+
+ return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function environment_variables_preview(): HasMany
{
- return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
+ return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function workdir()
@@ -913,21 +1233,50 @@ class Service extends BaseModel
public function saveComposeConfigs()
{
$workdir = $this->workdir();
- $commands[] = "mkdir -p $workdir";
+
+ instant_remote_process([
+ "mkdir -p $workdir",
+ "cd $workdir",
+ ], $this->server);
+
+ $filename = new Cuid2.'-docker-compose.yml';
+ Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose);
+ $path = Storage::path("tmp/{$filename}");
+ instant_scp($path, "{$workdir}/docker-compose.yml", $this->server);
+ Storage::disk('local')->delete("tmp/{$filename}");
+
$commands[] = "cd $workdir";
-
- $json = Yaml::parse($this->docker_compose);
- $this->docker_compose = Yaml::dump($json, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
- $docker_compose_base64 = base64_encode($this->docker_compose);
-
- $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
$commands[] = 'rm -f .env || true';
$envs_from_coolify = $this->environment_variables()->get();
- foreach ($envs_from_coolify as $env) {
- $commands[] = "echo '{$env->key}={$env->real_value}' >> .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) {
+ if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
+ $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+ } else {
+ $real_value = $env->real_value;
+ if ($env->version === '4.0.0-beta.239') {
+ $real_value = $env->real_value;
+ } else {
+ if ($env->is_literal || $env->is_multiline) {
+ $real_value = '\''.$real_value.'\'';
+ } else {
+ $real_value = escapeEnvVariables($env->real_value);
+ }
+ }
+ $commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
+ }
}
- if ($envs_from_coolify->count() === 0) {
+ if ($sorted->count() === 0) {
$commands[] = 'touch .env';
}
instant_remote_process($commands, $this->server);
@@ -935,7 +1284,14 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
- return parseDockerComposeFile($this, $isNew);
+ if ((int) $this->compose_parsing_version >= 3) {
+ return newParser($this);
+ } elseif ($this->docker_compose_raw) {
+ return parseDockerComposeFile($this, $isNew);
+ } else {
+ return collect([]);
+ }
+
}
public function networks()
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 6690f254e..0e79e1e2e 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -32,6 +32,16 @@ class ServiceApplication extends BaseModel
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public function isRunning()
+ {
+ return str($this->status)->contains('running');
+ }
+
+ public function isExited()
+ {
+ return str($this->status)->contains('exited');
+ }
+
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
@@ -102,4 +112,9 @@ class ServiceApplication extends BaseModel
{
getFilesystemVolumesFromServer($this, $isInit);
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 4a749913e..927527118 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -25,6 +25,16 @@ class ServiceDatabase extends BaseModel
remote_process(["docker restart {$container_id}"], $this->service->server);
}
+ public function isRunning()
+ {
+ return str($this->status)->contains('running');
+ }
+
+ public function isExited()
+ {
+ return str($this->status)->contains('exited');
+ }
+
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
@@ -105,4 +115,13 @@ class ServiceDatabase extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return str($this->databaseType())->contains('mysql') ||
+ str($this->databaseType())->contains('postgres') ||
+ str($this->databaseType())->contains('postgis') ||
+ str($this->databaseType())->contains('mariadb') ||
+ str($this->databaseType())->contains('mongodb');
+ }
}
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 4cd194cd8..e4341b1b9 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -75,6 +75,11 @@ class StandaloneClickhouse extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -289,4 +294,9 @@ class StandaloneClickhouse extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 8726b2546..94ab2d745 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -75,6 +75,11 @@ class StandaloneDragonfly extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -289,4 +294,9 @@ class StandaloneDragonfly extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 607cacade..335c8931c 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -75,6 +75,11 @@ class StandaloneKeydb extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -209,7 +214,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 +223,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;
@@ -289,4 +294,9 @@ class StandaloneKeydb extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index d88653e41..c6c08dee5 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -75,6 +75,11 @@ class StandaloneMariadb extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -289,4 +294,9 @@ class StandaloneMariadb extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index f09e932bf..99893b1d1 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -79,6 +79,11 @@ class StandaloneMongodb extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -309,4 +314,9 @@ class StandaloneMongodb extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index f4e56fab2..f2a5b5c14 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -76,6 +76,11 @@ class StandaloneMysql extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -290,4 +295,9 @@ class StandaloneMysql extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 311c09c36..1b18a5ca7 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -102,6 +102,11 @@ class StandalonePostgresql extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -291,4 +296,9 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 8a202ea9e..a5868e243 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -71,6 +71,11 @@ class StandaloneRedis extends BaseModel
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -285,4 +290,9 @@ class StandaloneRedis extends BaseModel
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php
index 549fc6cd3..cc7d76ebf 100644
--- a/app/Notifications/Channels/TransactionalEmailChannel.php
+++ b/app/Notifications/Channels/TransactionalEmailChannel.php
@@ -13,7 +13,7 @@ class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');
diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php
index f8195ec1d..682ed7a1a 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -44,7 +44,7 @@ class DockerCleanup extends Notification implements ShouldQueue
// $mail->view('emails.high-disk-usage', [
// 'name' => $this->server->name,
// 'disk_usage' => $this->disk_usage,
- // 'threshold' => $this->cleanup_after_percentage,
+ // 'threshold' => $this->docker_cleanup_threshold,
// ]);
// return $mail;
// }
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index c0e2a3c31..6377f2f15 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -52,7 +52,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toDiscord(): string
{
- $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
+ $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
return $message;
}
@@ -60,7 +60,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).",
+ 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
];
}
}
diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php
index 3c68afe7b..34cb22091 100644
--- a/app/Notifications/Server/HighDiskUsage.php
+++ b/app/Notifications/Server/HighDiskUsage.php
@@ -17,7 +17,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) {}
+ public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {}
public function via(object $notifiable): array
{
@@ -46,7 +46,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
- 'threshold' => $this->cleanup_after_percentage,
+ 'threshold' => $this->docker_cleanup_threshold,
]);
return $mail;
@@ -54,7 +54,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public function toDiscord(): string
{
- $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
+ $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.";
return $message;
}
@@ -62,7 +62,7 @@ class HighDiskUsage extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
+ 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
];
}
}
diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php
index 8b1c02d39..3938a8da7 100644
--- a/app/Notifications/TransactionalEmails/ResetPassword.php
+++ b/app/Notifications/TransactionalEmails/ResetPassword.php
@@ -18,7 +18,7 @@ class ResetPassword extends Notification
public function __construct($token)
{
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->token = $token;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index cd90918ad..8b4c2eef2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,10 +2,8 @@
namespace App\Providers;
-use App\Models\InstanceSettings;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
@@ -30,9 +28,5 @@ class AppServiceProvider extends ServiceProvider
])->baseUrl($api_url);
}
});
- // if (! env('CI')) {
- // View::share('instanceSettings', InstanceSettings::get());
- // }
-
}
}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index 53a2e9281..b916b6234 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -46,7 +46,7 @@ class FortifyServiceProvider extends ServiceProvider
Fortify::registerView(function () {
$isFirstUser = User::count() === 0;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
return redirect()->route('login');
}
@@ -60,7 +60,7 @@ class FortifyServiceProvider extends ServiceProvider
});
Fortify::loginView(function () {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count();
if ($users == 0) {
diff --git a/app/Providers/TelescopeServiceProvider.php b/app/Providers/TelescopeServiceProvider.php
new file mode 100644
index 000000000..b7a336631
--- /dev/null
+++ b/app/Providers/TelescopeServiceProvider.php
@@ -0,0 +1,67 @@
+hideSensitiveRequestDetails();
+
+ $isLocal = $this->app->environment('local');
+
+ Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
+ return $isLocal ||
+ $entry->isReportableException() ||
+ $entry->isFailedRequest() ||
+ $entry->isFailedJob() ||
+ $entry->isScheduledTask() ||
+ $entry->hasMonitoredTag();
+ });
+ }
+
+ /**
+ * Prevent sensitive request details from being logged by Telescope.
+ */
+ protected function hideSensitiveRequestDetails(): void
+ {
+ if ($this->app->environment('local')) {
+ return;
+ }
+
+ Telescope::hideRequestParameters(['_token']);
+
+ Telescope::hideRequestHeaders([
+ 'cookie',
+ 'x-csrf-token',
+ 'x-xsrf-token',
+ ]);
+ }
+
+ /**
+ * Register the Telescope gate.
+ *
+ * This gate determines who can access Telescope in non-local environments.
+ */
+ protected function gate(): void
+ {
+ Gate::define('viewTelescope', function ($user) {
+ $root_user = User::find(0);
+
+ return in_array($user->email, [
+ $root_user->email,
+ ]);
+ });
+ }
+}
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 9b58882eb..f8ccee9db 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -3,6 +3,7 @@
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
@@ -42,7 +43,7 @@ trait ExecuteRemoteCommand
$command = parseLineForSudo($command, $this->server);
}
}
- $remote_command = generateSshCommand($this->server, $command);
+ $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index 6c9378cac..fbd7b0b15 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -22,6 +22,7 @@ class Input extends Component
public bool $allowToPeak = true,
public bool $isMultiline = false,
public string $defaultClass = 'input',
+ public string $autocomplete = 'off',
) {}
public function render(): View|Closure|string
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index 8e14ef9ee..006b095cf 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -175,4 +175,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
+ $request->offsetUnset('use_build_server');
}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index 1a08a46eb..b3e8011b9 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
- ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
+ ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
if ($deployments->count() > $concurrent_builds) {
return false;
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index f94c9bc20..d8dc26a48 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -20,12 +20,16 @@ const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb',
'bitnami/mongodb',
- 'bitnami/mysql',
- 'bitnami/postgresql',
'bitnami/redis',
'mysql',
+ 'bitnami/mysql',
+ 'mysql/mysql-server',
'mariadb',
+ 'postgis/postgis',
'postgres',
+ 'bitnami/postgresql',
+ 'supabase/postgres',
+ 'elestio/postgres',
'mongo',
'redis',
'memcached',
@@ -33,7 +37,6 @@ const DATABASE_DOCKER_IMAGES = [
'neo4j',
'influxdb',
'clickhouse/clickhouse-server',
- 'supabase/postgres',
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
@@ -46,6 +49,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/databases.php b/bootstrap/helpers/databases.php
index b8dcc1f3c..950eb67b6 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -19,7 +19,7 @@ function generate_database_name(string $type): string
return $type.'-database-'.$cuid;
}
-function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql
+function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
{
$destination = StandaloneDocker::where('uuid', $destinationUuid)->first();
if (! $destination) {
@@ -27,6 +27,7 @@ function create_standalone_postgresql($environmentId, $destinationUuid, ?array $
}
$database = new StandalonePostgresql;
$database->name = generate_database_name('postgresql');
+ $database->image = $databaseImage;
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environmentId;
$database->destination_id = $destination->id;
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index a534dc5ff..7e902fcdd 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
return $containers;
}
+function getCurrentServiceContainerStatus(Server $server, int $id): Collection
+{
+ $containers = collect([]);
+ if (! $server->isSwarm()) {
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server);
+ $containers = format_docker_command_output_to_json($containers);
+ $containers = $containers->filter();
+
+ return $containers;
+ }
+
+ return $containers;
+}
+
function format_docker_command_output_to_json($rawOutput): Collection
{
$outputLines = explode(PHP_EOL, $rawOutput);
@@ -120,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return 'exited';
}
$container = format_docker_command_output_to_json($container);
+ if ($container->isEmpty()) {
+ return 'exited';
+ }
if ($all_data) {
return $container[0];
}
@@ -140,6 +157,8 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
{
+ // TODO: refactor generateApplicationContainerName, we do not need $application and $pull_request_id
+
$consistent_container_name = $application->settings->is_consistent_container_name_enabled;
$now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) {
@@ -213,12 +232,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
- 'value' => generateFqdn($server, 'console-'.$uuid),
+ 'value' => generateFqdn($server, 'console-'.$uuid, true),
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
- 'value' => generateFqdn($server, 'minio-'.$uuid),
+ 'value' => generateFqdn($server, 'minio-'.$uuid, true),
]);
}
$payload = collect([
@@ -251,7 +270,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
return $payload;
}
-function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both')
+function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null)
{
$labels = collect([]);
if ($serviceLabels) {
@@ -270,6 +289,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
}
+ if (is_null($port) && $predefinedPort) {
+ $port = $predefinedPort;
+ }
$labels->push("caddy_{$loop}={$schema}://{$host}");
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
@@ -303,38 +325,16 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
- $basic_auth = false;
- $basic_auth_middleware = null;
- $redirect = false;
- $redirect_middleware = null;
+ $middlewares_from_labels = collect([]);
if ($serviceLabels) {
- $basic_auth = $serviceLabels->contains(function ($value) {
- return str_contains($value, 'basicauth');
- });
- if ($basic_auth) {
- $basic_auth_middleware = $serviceLabels
- ->map(function ($item) {
- if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) {
- return $matches[1];
- }
- })
- ->filter()
- ->first();
- }
- $redirect = $serviceLabels->contains(function ($value) {
- return str_contains($value, 'redirectregex');
- });
- if ($redirect) {
- $redirect_middleware = $serviceLabels
- ->map(function ($item) {
- if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
- return $matches[1];
- }
- })
- ->filter()
- ->first();
- }
+ $middlewares_from_labels = $serviceLabels->map(function ($item) {
+ if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) {
+ return $matches[1];
+ }
+ return null;
+ })->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
@@ -382,20 +382,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
+ // Middleware handling
$middlewares = collect([]);
- if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
+ if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix");
}
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -403,10 +398,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name);
}
- if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
+ if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -415,13 +413,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
- }
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
+ }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -433,6 +425,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -468,12 +463,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -485,6 +474,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@@ -494,12 +486,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -511,6 +497,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@@ -677,18 +666,19 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
'--sysctl',
'--ulimit',
'--device',
+ '--shm-size',
]);
$mapping = collect([
'--cap-add' => 'cap_add',
'--cap-drop' => 'cap_drop',
'--security-opt' => 'security_opt',
'--sysctl' => 'sysctls',
- '--ulimit' => 'ulimits',
'--device' => 'devices',
'--init' => 'init',
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
'--ip' => 'ip',
+ '--shm-size' => 'shm_size',
]);
foreach ($matches as $match) {
$option = $match[1];
@@ -704,6 +694,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
$options = collect($options);
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
foreach ($options as $option => $value) {
+ // ray($option,$value);
if (! data_get($mapping, $option)) {
continue;
}
@@ -728,6 +719,10 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
}
});
$compose_options->put($mapping[$option], $ulimits);
+ } elseif ($option === '--shm-size') {
+ if (! is_null($value) && is_array($value) && count($value) > 0) {
+ $compose_options->put($mapping[$option], $value[0]);
+ }
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
@@ -749,6 +744,26 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
return $compose_options->toArray();
}
+function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network)
+{
+ $ipv4 = data_get($docker_run_options, 'ip.0');
+ $ipv6 = data_get($docker_run_options, 'ip6.0');
+ data_forget($docker_run_options, 'ip');
+ data_forget($docker_run_options, 'ip6');
+ if ($ipv4 || $ipv6) {
+ data_forget($docker_compose['services'][$container_name], 'networks');
+ }
+ if ($ipv4) {
+ $docker_compose['services'][$container_name]['networks'][$network]['ipv4_address'] = $ipv4;
+ }
+ if ($ipv6) {
+ $docker_compose['services'][$container_name]['networks'][$network]['ipv6_address'] = $ipv6;
+ }
+ $docker_compose['services'][$container_name] = array_merge_recursive($docker_compose['services'][$container_name], $docker_run_options);
+
+ return $docker_compose;
+}
+
function validateComposeFile(string $compose, int $server_id): string|Throwable
{
return 'OK';
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index e50983535..5d1ad5390 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
} else {
@@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
}
@@ -144,14 +148,14 @@ function generate_default_proxy_configuration(Server $server)
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.services.traefik.loadbalancer.server.port=8080',
'coolify.managed=true',
+ 'coolify.proxy=true',
];
$config = [
- 'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'traefik' => [
'container_name' => 'coolify-proxy',
- 'image' => 'traefik:v2.11',
+ 'image' => 'traefik:v3.1',
'restart' => RESTART_MODE,
'extra_hosts' => [
'host.docker.internal:host-gateway',
@@ -218,7 +222,6 @@ function generate_default_proxy_configuration(Server $server)
}
} elseif ($proxy_type === 'CADDY') {
$config = [
- 'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'caddy' => [
@@ -237,12 +240,9 @@ function generate_default_proxy_configuration(Server $server)
'80:80',
'443:443',
],
- // "healthcheck" => [
- // "test" => "wget -qO- http://localhost:80|| exit 1",
- // "interval" => "4s",
- // "timeout" => "2s",
- // "retries" => 5,
- // ],
+ 'labels' => [
+ 'coolify.managed=true',
+ ],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}/dynamic:/dynamic",
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 918aa74cc..67b60d6b7 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -3,6 +3,7 @@
use App\Actions\CoolifyTask\PrepareCoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
@@ -10,9 +11,8 @@ use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Process;
-use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Activitylog\Contracts\Activity;
@@ -26,29 +26,28 @@ function remote_process(
$callEventOnFinish = null,
$callEventData = null
): Activity {
- if (is_null($type)) {
- $type = ActivityTypes::INLINE->value;
- }
- if ($command instanceof Collection) {
- $command = $command->toArray();
- }
+ $type = $type ?? ActivityTypes::INLINE->value;
+ $command = $command instanceof Collection ? $command->toArray() : $command;
+
if ($server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $server);
}
+
$command_string = implode("\n", $command);
- if (auth()->user()) {
- $teams = auth()->user()->teams->pluck('id');
+
+ if (Auth::check()) {
+ $teams = Auth::user()->teams->pluck('id');
if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
throw new \Exception('User is not part of the team that owns this server');
}
}
+ SshMultiplexingHelper::ensureMultiplexedConnection($server);
+
return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs(
server_uuid: $server->uuid,
- command: <<uuid}";
- $location = '/var/www/html/storage/app/ssh/keys/'.$private_key_filename;
- $mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename();
- return [
- 'location' => $location,
- 'mux_filename' => $mux_filename,
- 'private_key_filename' => $private_key_filename,
- ];
-}
-function savePrivateKeyToFs(Server $server)
-{
- if (data_get($server, 'privateKey.private_key') === null) {
- throw new \Exception("Server {$server->name} does not have a private key");
- }
- ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
- Storage::disk('ssh-keys')->makeDirectory('.');
- Storage::disk('ssh-mux')->makeDirectory('.');
- Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
-
- return $location;
-}
-
-function generateScpCommand(Server $server, string $source, string $dest)
-{
- $user = $server->user;
- $port = $server->port;
- $privateKeyLocation = savePrivateKeyToFs($server);
- $timeout = config('constants.ssh.command_timeout');
- $connectionTimeout = config('constants.ssh.connection_timeout');
- $serverInterval = config('constants.ssh.server_interval');
-
- $scp_command = "timeout $timeout scp ";
- $scp_command .= "-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 {$port} "
- ."{$source} "
- ."{$user}@{$server->ip}:{$dest}";
-
- return $scp_command;
-}
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{
- $timeout = config('constants.ssh.command_timeout');
- $scp_command = generateScpCommand($server, $source, $dest);
- $process = Process::timeout($timeout)->run($scp_command);
+ $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
+ $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
- if (! $throwError) {
- return null;
- }
-
- return excludeCertainErrors($process->errorOutput(), $exitCode);
- }
- if ($output === 'null') {
- $output = null;
+ return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
}
- return $output;
+ return $output === 'null' ? null : $output;
}
-function generateSshCommand(Server $server, string $command)
+
+function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
{
- if ($server->settings->force_disabled) {
- throw new \RuntimeException('Server is disabled.');
- }
- $user = $server->user;
- $port = $server->port;
- $privateKeyLocation = savePrivateKeyToFs($server);
- $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');
-
- $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 ";
- }
- if (data_get($server, 'settings.is_cloudflare_tunnel')) {
- $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
- }
- $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
- $delimiter = Hash::make($command);
- $command = str_replace($delimiter, '', $command);
- $ssh_command .= "-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 {$port} "
- ."{$user}@{$server->ip} "
- ." 'bash -se' << \\$delimiter".PHP_EOL
- .$command.PHP_EOL
- .$delimiter;
-
- // ray($ssh_command);
- return $ssh_command;
-}
-function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false)
-{
- $timeout = config('constants.ssh.command_timeout');
- if ($command instanceof Collection) {
- $command = $command->toArray();
- }
+ $command = $command instanceof Collection ? $command->toArray() : $command;
if ($server->isNonRoot() && ! $no_sudo) {
$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 = SshMultiplexingHelper::generateSshCommand($server, $command_string);
+ $process = Process::timeout(config('constants.ssh.command_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;
- }
-
- return excludeCertainErrors($process->errorOutput(), $exitCode);
- }
- if ($output === 'null') {
- $output = null;
+ return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
}
- return $output;
+ return $output === 'null' ? null : $output;
}
+
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
{
$ignoredErrors = collect([
'Permission denied (publickey',
'Could not resolve hostname',
]);
- $ignored = false;
- foreach ($ignoredErrors as $ignoredError) {
- if (Str::contains($errorOutput, $ignoredError)) {
- $ignored = true;
- break;
- }
- }
+ $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error));
if ($ignored) {
// TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorOutput, $exitCode);
}
throw new \RuntimeException($errorOutput, $exitCode);
}
+
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
{
- $application = Application::find(data_get($application_deployment_queue, 'application_id'));
- $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
if (is_null($application_deployment_queue)) {
return collect([]);
}
- // ray(data_get($application_deployment_queue, 'logs'));
+ $application = Application::find(data_get($application_deployment_queue, 'application_id'));
+ $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
try {
$decoded = json_decode(
data_get($application_deployment_queue, 'logs'),
@@ -233,41 +127,70 @@ 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);
}
- $formatted = $formatted
+
+ return $formatted
->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) {
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');
+ });
- return $formatted;
+ 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());
}
+
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
-function remove_mux_and_private_key(Server $server)
-{
- $muxFilename = $server->muxFilename();
- $privateKeyLocation = savePrivateKeyToFs($server);
- Storage::disk('ssh-mux')->delete($muxFilename);
- Storage::disk('ssh-keys')->delete($privateKeyLocation);
-}
+
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {
return;
}
foreach ($private_key->servers as $server) {
- Storage::disk('ssh-mux')->delete($server->muxFilename());
+ SshMultiplexingHelper::removeMuxFile($server);
}
}
@@ -277,24 +200,16 @@ 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;
+ if (! $commandFound) {
+ break;
}
- ray('could not install '.$command);
- break;
}
}
diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php
index 4a2252016..2ee7bf44a 100644
--- a/bootstrap/helpers/s3.php
+++ b/bootstrap/helpers/s3.php
@@ -1,14 +1,11 @@
endpoint) {
- $is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com');
- }
+
config()->set('filesystems.disks.custom-s3', [
'driver' => 's3',
'region' => $s3['region'],
@@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3)
'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true,
- 'bucket_endpoint' => $is_digital_ocean,
+ 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(),
'aws_url' => $s3->awsUrl(),
]);
}
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index 1cc2ac36d..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)
@@ -53,7 +54,9 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
if ($isFile == 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
- $fileVolume->content = $filesystemContent;
+ if ($fileVolume->is_based_on_git) {
+ $fileVolume->content = $filesystemContent;
+ }
$fileVolume->is_directory = false;
$fileVolume->save();
} elseif ($isDir == 'OK') {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index aae4fafd4..65fe3322d 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{
if (! $settings) {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
}
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
@@ -281,7 +281,7 @@ function base_ip(): string
if (isDev()) {
return 'localhost';
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->public_ipv4) {
return "$settings->public_ipv4";
}
@@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn)
*/
function base_url(bool $withPort = true): string
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->fqdn) {
return $settings->fqdn;
}
@@ -343,6 +343,11 @@ function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
}
+
+function isProduction(): bool
+{
+ return ! isDev();
+}
function isDev(): bool
{
return config('app.env') === 'local';
@@ -353,6 +358,14 @@ function isCloud(): bool
return ! config('coolify.self_hosted');
}
+function translate_cron_expression($expression_to_validate): string
+{
+ if (isset(VALID_CRON_STRINGS[$expression_to_validate])) {
+ return VALID_CRON_STRINGS[$expression_to_validate];
+ }
+
+ return $expression_to_validate;
+}
function validate_cron_expression($expression_to_validate): bool
{
$isValid = false;
@@ -376,7 +389,7 @@ function send_internal_notification(string $message): void
}
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
throw new Exception('No email settings found.');
@@ -470,7 +483,7 @@ function data_get_str($data, $key, $default = null): Stringable
return str($str);
}
-function generateFqdn(Server $server, string $random)
+function generateFqdn(Server $server, string $random, bool $forceHttps = false): string
{
$wildcard = data_get($server, 'settings.wildcard_domain');
if (is_null($wildcard) || $wildcard === '') {
@@ -480,6 +493,9 @@ function generateFqdn(Server $server, string $random)
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
+ if ($forceHttps) {
+ $scheme = 'https';
+ }
$finalFqdn = "$scheme://{$random}.$host$path";
return $finalFqdn;
@@ -494,12 +510,23 @@ function sslip(Server $server)
return "http://$baseIp.sslip.io";
}
+ // ipv6
+ if (str($server->ip)->contains(':')) {
+ $ipv6 = str($server->ip)->replace(':', '-');
+
+ return "http://{$ipv6}.sslip.io";
+ }
return "http://{$server->ip}.sslip.io";
}
function get_service_templates(bool $force = false): Collection
{
+ if (isDev()) {
+ $services = File::get(base_path('templates/service-templates.json'));
+
+ return collect(json_decode($services))->sortKeys();
+ }
if ($force) {
try {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
@@ -686,7 +713,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -736,7 +765,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -758,6 +789,713 @@ function getTopLevelNetworks(Service|Application $resource)
return $topLevelNetworks->keys();
}
}
+function sourceIsLocal(Stringable $source)
+{
+ if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~') || $source->startsWith('..') || $source->startsWith('~/') || $source->startsWith('../')) {
+ return true;
+ }
+
+ return false;
+}
+
+function replaceLocalSource(Stringable $source, Stringable $replacedWith)
+{
+ if ($source->startsWith('.')) {
+ $source = $source->replaceFirst('.', $replacedWith->value());
+ }
+ if ($source->startsWith('~')) {
+ $source = $source->replaceFirst('~', $replacedWith->value());
+ }
+ if ($source->startsWith('..')) {
+ $source = $source->replaceFirst('..', $replacedWith->value());
+ }
+ if ($source->endsWith('/') && $source->value() !== '/') {
+ $source = $source->replaceLast('/', '');
+ }
+
+ return $source;
+}
+
+function convertToArray($collection)
+{
+ if ($collection instanceof Collection) {
+ return $collection->map(function ($item) {
+ return convertToArray($item);
+ })->toArray();
+ } elseif ($collection instanceof Stringable) {
+ return (string) $collection;
+ } elseif (is_array($collection)) {
+ return array_map(function ($item) {
+ return convertToArray($item);
+ }, $collection);
+ }
+
+ return $collection;
+}
+
+function parseCommandFromMagicEnvVariable(Str|string $key): Stringable
+{
+ $value = str($key);
+ $count = substr_count($value->value(), '_');
+ if ($count === 2) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ } else {
+ // SERVICE_BASE64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ }
+ }
+ if ($count === 3) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI_1000
+ $command = $value->after('SERVICE_')->before('_');
+ } else {
+ // SERVICE_BASE64_64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ }
+ }
+
+ return str($command);
+}
+function parseEnvVariable(Str|string $value)
+{
+ $value = str($value);
+ $count = substr_count($value->value(), '_');
+ $command = null;
+ $forService = null;
+ $generatedValue = null;
+ $port = null;
+ if ($value->startsWith('SERVICE')) {
+ if ($count === 2) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ $forService = $value->afterLast('_');
+ } else {
+ // SERVICE_BASE64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ }
+ }
+ if ($count === 3) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI_1000
+ $command = $value->after('SERVICE_')->before('_');
+ $forService = $value->after('SERVICE_')->after('_')->before('_');
+ $port = $value->afterLast('_');
+ if (filter_var($port, FILTER_VALIDATE_INT) === false) {
+ $port = null;
+ }
+ } else {
+ // SERVICE_BASE64_64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ ray($command);
+ }
+ }
+ }
+
+ return [
+ 'command' => $command,
+ 'forService' => $forService,
+ 'generatedValue' => $generatedValue,
+ 'port' => $port,
+ ];
+}
+function generateEnvValue(string $command, Service|Application|null $service = null)
+{
+ switch ($command) {
+ case 'PASSWORD':
+ $generatedValue = Str::password(symbols: false);
+ break;
+ case 'PASSWORD_64':
+ $generatedValue = Str::password(length: 64, symbols: false);
+ break;
+ // This is not base64, it's just a random string
+ case 'BASE64_64':
+ $generatedValue = Str::random(64);
+ break;
+ case 'BASE64_128':
+ $generatedValue = Str::random(128);
+ break;
+ case 'BASE64':
+ case 'BASE64_32':
+ $generatedValue = Str::random(32);
+ break;
+ // This is base64,
+ case 'REALBASE64_64':
+ $generatedValue = base64_encode(Str::random(64));
+ break;
+ case 'REALBASE64_128':
+ $generatedValue = base64_encode(Str::random(128));
+ break;
+ case 'REALBASE64':
+ case 'REALBASE64_32':
+ $generatedValue = base64_encode(Str::random(32));
+ break;
+ case 'USER':
+ $generatedValue = Str::random(16);
+ break;
+ case 'SUPABASEANON':
+ $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
+ if (is_null($signingKey)) {
+ return;
+ } else {
+ $signingKey = $signingKey->value;
+ }
+ $key = InMemory::plainText($signingKey);
+ $algorithm = new Sha256;
+ $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
+ $now = new DateTimeImmutable;
+ $now = $now->setTime($now->format('H'), $now->format('i'));
+ $token = $tokenBuilder
+ ->issuedBy('supabase')
+ ->issuedAt($now)
+ ->expiresAt($now->modify('+100 year'))
+ ->withClaim('role', 'anon')
+ ->getToken($algorithm, $key);
+ $generatedValue = $token->toString();
+ break;
+ case 'SUPABASESERVICE':
+ $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
+ if (is_null($signingKey)) {
+ return;
+ } else {
+ $signingKey = $signingKey->value;
+ }
+ $key = InMemory::plainText($signingKey);
+ $algorithm = new Sha256;
+ $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
+ $now = new DateTimeImmutable;
+ $now = $now->setTime($now->format('H'), $now->format('i'));
+ $token = $tokenBuilder
+ ->issuedBy('supabase')
+ ->issuedAt($now)
+ ->expiresAt($now->modify('+100 year'))
+ ->withClaim('role', 'service_role')
+ ->getToken($algorithm, $key);
+ $generatedValue = $token->toString();
+ break;
+ default:
+ // $generatedValue = Str::random(16);
+ $generatedValue = null;
+ break;
+ }
+
+ return $generatedValue;
+}
+
+function getRealtime()
+{
+ $envDefined = env('PUSHER_PORT');
+ if (empty($envDefined)) {
+ $url = Url::fromString(Request::getSchemeAndHttpHost());
+ $port = $url->getPort();
+ if ($port) {
+ return '6001';
+ } else {
+ return null;
+ }
+ } else {
+ return $envDefined;
+ }
+}
+
+function validate_dns_entry(string $fqdn, Server $server)
+{
+ // https://www.cloudflare.com/ips-v4/#
+ $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']);
+
+ $url = Url::fromString($fqdn);
+ $host = $url->getHost();
+ if (str($host)->contains('sslip.io')) {
+ return true;
+ }
+ $settings = instanceSettings();
+ $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
+ if (! $is_dns_validation_enabled) {
+ return true;
+ }
+ $dns_servers = data_get($settings, 'custom_dns_servers');
+ $dns_servers = str($dns_servers)->explode(',');
+ if ($server->id === 0) {
+ $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip));
+ } else {
+ $ip = $server->ip;
+ }
+ $found_matching_ip = false;
+ $type = \PurplePixie\PhpDns\DNSTypes::NAME_A;
+ foreach ($dns_servers as $dns_server) {
+ try {
+ ray("Checking $host on $dns_server");
+ $query = new DNSQuery($dns_server);
+ $results = $query->query($host, $type);
+ if ($results === false || $query->hasError()) {
+ ray('Error: '.$query->getLasterror());
+ } else {
+ foreach ($results as $result) {
+ if ($result->getType() == $type) {
+ if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) {
+ ray("Found match in Cloudflare IPs: $match");
+ $found_matching_ip = true;
+ break;
+ }
+ if ($result->getData() === $ip) {
+ ray($host.' has IP address '.$result->getData());
+ ray($result->getString());
+ $found_matching_ip = true;
+ break;
+ }
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ }
+ }
+ ray("Found match: $found_matching_ip");
+
+ return $found_matching_ip;
+}
+
+function ip_match($ip, $cidrs, &$match = null)
+{
+ foreach ((array) $cidrs as $cidr) {
+ [$subnet, $mask] = explode('/', $cidr);
+ if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) {
+ $match = $cidr;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null)
+{
+ if (is_null($teamId)) {
+ return response()->json(['error' => 'Team ID is required.'], 400);
+ }
+ if (is_array($domains)) {
+ $domains = collect($domains);
+ }
+
+ $domains = $domains->map(function ($domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+
+ return str($domain);
+ });
+ $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
+ $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
+ if ($uuid) {
+ $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid);
+ $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid);
+ }
+ $domainFound = false;
+ foreach ($applications as $app) {
+ if (is_null($app->fqdn)) {
+ continue;
+ }
+ $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
+ foreach ($list_of_domains as $domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ $domainFound = true;
+ break;
+ }
+ }
+ }
+ if ($domainFound) {
+ return true;
+ }
+ foreach ($serviceApplications as $app) {
+ if (str($app->fqdn)->isEmpty()) {
+ continue;
+ }
+ $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
+ foreach ($list_of_domains as $domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ $domainFound = true;
+ break;
+ }
+ }
+ }
+ if ($domainFound) {
+ return true;
+ }
+ $settings = instanceSettings();
+ if (data_get($settings, 'fqdn')) {
+ $domain = data_get($settings, 'fqdn');
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ return true;
+ }
+ }
+}
+function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
+{
+ if ($resource) {
+ if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
+ $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
+ $domains = collect($domains);
+ } else {
+ $domains = collect($resource->fqdns);
+ }
+ } elseif ($domain) {
+ $domains = collect($domain);
+ } else {
+ throw new \RuntimeException('No resource or FQDN provided.');
+ }
+ $domains = $domains->map(function ($domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+
+ return str($domain);
+ });
+ $apps = Application::all();
+ foreach ($apps as $app) {
+ $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
+ foreach ($list_of_domains as $domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ if (data_get($resource, 'uuid')) {
+ if ($resource->uuid !== $app->uuid) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
+ }
+ } elseif ($domain) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
+ }
+ }
+ }
+ }
+ $apps = ServiceApplication::all();
+ foreach ($apps as $app) {
+ $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
+ foreach ($list_of_domains as $domain) {
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ if (data_get($resource, 'uuid')) {
+ if ($resource->uuid !== $app->uuid) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
+ }
+ } elseif ($domain) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
+ }
+ }
+ }
+ }
+ if ($resource) {
+ $settings = instanceSettings();
+ if (data_get($settings, 'fqdn')) {
+ $domain = data_get($settings, 'fqdn');
+ if (str($domain)->endsWith('/')) {
+ $domain = str($domain)->beforeLast('/');
+ }
+ $naked_domain = str($domain)->value();
+ if ($domains->contains($naked_domain)) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
+ }
+ }
+ }
+}
+
+function parseCommandsByLineForSudo(Collection $commands, Server $server): array
+{
+ $commands = $commands->map(function ($line) {
+ if (
+ ! str(trim($line))->startsWith([
+ 'cd',
+ 'command',
+ 'echo',
+ 'true',
+ 'if',
+ 'fi',
+ ])
+ ) {
+ return "sudo $line";
+ }
+
+ if (str(trim($line))->startsWith('if')) {
+ return str_replace('if', 'if sudo', $line);
+ }
+
+ return $line;
+ });
+
+ $commands = $commands->map(function ($line) use ($server) {
+ if (Str::startsWith($line, 'sudo mkdir -p')) {
+ return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
+ }
+
+ return $line;
+ });
+
+ $commands = $commands->map(function ($line) {
+ $line = str($line);
+ if (str($line)->contains('$(')) {
+ $line = $line->replace('$(', '$(sudo ');
+ }
+ if (str($line)->contains('||')) {
+ $line = $line->replace('||', '|| sudo');
+ }
+ if (str($line)->contains('&&')) {
+ $line = $line->replace('&&', '&& sudo');
+ }
+ if (str($line)->contains(' | ')) {
+ $line = $line->replace(' | ', ' | sudo ');
+ }
+
+ return $line->value();
+ });
+
+ return $commands->toArray();
+}
+function parseLineForSudo(string $command, Server $server): string
+{
+ if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) {
+ $command = "sudo $command";
+ }
+ if (Str::startsWith($command, 'sudo mkdir -p')) {
+ $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p');
+ }
+ if (str($command)->contains('$(') || str($command)->contains('`')) {
+ $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
+ }
+ if (str($command)->contains('||')) {
+ $command = str($command)->replace('||', '|| sudo ')->value();
+ }
+ if (str($command)->contains('&&')) {
+ $command = str($command)->replace('&&', '&& sudo ')->value();
+ }
+
+ return $command;
+}
+
+function get_public_ips()
+{
+ try {
+ [$first, $second] = Process::concurrently(function (Pool $pool) {
+ $pool->path(__DIR__)->command('curl -4s https://ifconfig.io');
+ $pool->path(__DIR__)->command('curl -6s https://ifconfig.io');
+ });
+ $ipv4 = $first->output();
+ if ($ipv4) {
+ $ipv4 = trim($ipv4);
+ $validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP);
+ if ($validate_ipv4 == false) {
+ echo "Invalid ipv4: $ipv4\n";
+
+ return;
+ }
+ InstanceSettings::get()->update(['public_ipv4' => $ipv4]);
+ }
+ } catch (\Exception $e) {
+ echo "Error: {$e->getMessage()}\n";
+ }
+ try {
+ $ipv6 = $second->output();
+ if ($ipv6) {
+ $ipv6 = trim($ipv6);
+ $validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP);
+ if ($validate_ipv6 == false) {
+ echo "Invalid ipv6: $ipv6\n";
+
+ return;
+ }
+ InstanceSettings::get()->update(['public_ipv6' => $ipv6]);
+ }
+ } catch (\Throwable $e) {
+ echo "Error: {$e->getMessage()}\n";
+ }
+}
+
+function isAnyDeploymentInprogress()
+{
+ // Only use it in the deployment script
+ $count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count();
+ if ($count > 0) {
+ echo "There are $count deployments in progress. Exiting...\n";
+ exit(1);
+ }
+ echo "No deployments in progress.\n";
+ exit(0);
+}
+
+function generateSentinelToken()
+{
+ $token = Str::random(64);
+
+ return $token;
+}
+
+function isBase64Encoded($strValue)
+{
+ return base64_encode(base64_decode($strValue, true)) === $strValue;
+}
+function customApiValidator(Collection|array $item, array $rules)
+{
+ if (is_array($item)) {
+ $item = collect($item);
+ }
+
+ return Validator::make($item->toArray(), $rules, [
+ 'required' => 'This field is required.',
+ ]);
+}
+
+function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull_request_id = 0)
+{
+ $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
+ $type = null;
+ $source = null;
+ $target = null;
+ $content = null;
+ $isDirectory = false;
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ $foundConfig = $resource->fileStorages()->whereMountPath($target)->first();
+ if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
+ $type = str('bind');
+ if ($foundConfig) {
+ $contentNotNull = data_get($foundConfig, 'content');
+ if ($contentNotNull) {
+ $content = $contentNotNull;
+ }
+ $isDirectory = data_get($foundConfig, 'is_directory');
+ } else {
+ // By default, we cannot determine if the bind is a directory or not, so we set it to directory
+ $isDirectory = true;
+ }
+ } else {
+ $type = str('volume');
+ }
+ } elseif (is_array($volume)) {
+ $type = data_get_str($volume, 'type');
+ $source = data_get_str($volume, 'source');
+ $target = data_get_str($volume, 'target');
+ $content = data_get($volume, 'content');
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+ $foundConfig = $resource->fileStorages()->whereMountPath($target)->first();
+ if ($foundConfig) {
+ $contentNotNull = data_get($foundConfig, 'content');
+ if ($contentNotNull) {
+ $content = $contentNotNull;
+ }
+ $isDirectory = data_get($foundConfig, 'is_directory');
+ } else {
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+ if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
+ // if isDirectory is not set (or false) & content is also not set, we assume it is a directory
+ ray('setting isDirectory to true');
+ $isDirectory = true;
+ }
+ }
+ }
+ if ($type?->value() === 'bind') {
+ if ($source->value() === '/var/run/docker.sock') {
+ return $volume;
+ }
+ if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
+ return $volume;
+ }
+ if (get_class($resource) === "App\Models\Application") {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ } else {
+ $dir = base_configuration_dir().'/services/'.$resource->service->uuid;
+ }
+
+ if ($source->startsWith('.')) {
+ $source = $source->replaceFirst('.', $dir);
+ }
+ if ($source->startsWith('~')) {
+ $source = $source->replaceFirst('~', $dir);
+ }
+ if ($pull_request_id !== 0) {
+ $source = $source."-pr-$pull_request_id";
+ }
+ if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) {
+ LocalFileVolume::updateOrCreate(
+ [
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ],
+ [
+ 'fs_path' => $source,
+ 'mount_path' => $target,
+ 'content' => $content,
+ 'is_directory' => $isDirectory,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ]
+ );
+ }
+ } elseif ($type->value() === 'volume') {
+ if ($topLevelVolumes->has($source->value())) {
+ $v = $topLevelVolumes->get($source->value());
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ return $volume;
+ }
+ }
+ $slugWithoutUuid = Str::slug($source, '-');
+ if (get_class($resource) === "App\Models\Application") {
+ $name = "{$resource->uuid}_{$slugWithoutUuid}";
+ } else {
+ $name = "{$resource->service->uuid}_{$slugWithoutUuid}";
+ }
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ $source = $name;
+ $volume = "$source:$target";
+ } elseif (is_array($volume)) {
+ data_set($volume, 'source', $name);
+ }
+ $topLevelVolumes->put($name, [
+ 'name' => $name,
+ ]);
+ LocalPersistentVolume::updateOrCreate(
+ [
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ],
+ [
+ 'name' => $name,
+ 'mount_path' => $target,
+ 'resource_id' => $resource->id,
+ 'resource_type' => get_class($resource),
+ ]
+ );
+ }
+ dispatch(new ServerFilesFromServerJob($resource));
+
+ return $volume;
+ });
+
+ return [
+ 'serviceVolumes' => $serviceVolumes,
+ 'topLevelVolumes' => $topLevelVolumes,
+ ];
+}
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
{
@@ -905,7 +1643,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -1062,6 +1802,23 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray());
}
+ // convert - SESSION_SECRET: 123 to - SESSION_SECRET=123
+ $convertedServiceVariables = collect([]);
+ foreach ($serviceVariables as $variableName => $variable) {
+ if (is_numeric($variableName)) {
+ if (is_array($variable)) {
+ $key = str(collect($variable)->keys()->first());
+ $value = str(collect($variable)->values()->first());
+ $variable = "$key=$value";
+ $convertedServiceVariables->put($variableName, $variable);
+ } elseif (is_string($variable)) {
+ $convertedServiceVariables->put($variableName, $variable);
+ }
+ } elseif (is_string($variableName)) {
+ $convertedServiceVariables->put($variableName, $variable);
+ }
+ }
+ $serviceVariables = $convertedServiceVariables;
// Get variables from the service
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
@@ -1177,7 +1934,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([
@@ -1360,14 +2117,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
- data_set($service, 'logging', [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ]);
+ data_set($service, 'logging', generate_fluentd_configuration());
}
if ($serviceLabels->count() > 0) {
if ($resource->is_container_label_escape_enabled) {
@@ -1393,7 +2143,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
updateCompose($savedService);
return $service;
-
});
$envs_from_coolify = $resource->environment_variables()->get();
@@ -1416,6 +2165,21 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
$parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}");
+
+ // TODO: move this in a shared function
+ if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) {
+ $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\"");
+ }
+ if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) {
+ $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\"");
+ }
+ if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) {
+ $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\"");
+ }
+ if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) {
+ $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\"");
+ }
+
$parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) {
if (! str($value)->startsWith('$')) {
$found_env = $envs_from_coolify->where('key', $key)->first();
@@ -1441,6 +2205,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$yaml = data_forget($yaml, 'services.*.volumes.*.content');
$resource->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$resource->docker_compose = Yaml::dump($finalServices, 10, 2);
+
$resource->save();
$resource->saveComposeConfigs();
@@ -1449,10 +2214,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return collect([]);
}
} elseif ($resource->getMorphClass() === 'App\Models\Application') {
- $isSameDockerComposeFile = false;
- if ($resource->dockerComposePrLocation() === $resource->dockerComposeLocation()) {
- $isSameDockerComposeFile = true;
- }
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
@@ -1799,7 +2560,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -1819,7 +2582,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
if ($collectedPorts->count() > 0) {
- // ray($collectedPorts->implode(','));
+ ray($collectedPorts->implode(','));
}
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
@@ -1934,7 +2697,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([
@@ -2126,15 +2889,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$defaultLabels = defaultLabels($resource->id, $containerName, $pull_request_id, type: 'application');
$serviceLabels = $serviceLabels->merge($defaultLabels);
- if ($server->isLogDrainEnabled() && $resource->isLogDrainEnabled()) {
- data_set($service, 'logging', [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ]);
+ if ($server->isLogDrainEnabled()) {
+ if ($resource instanceof Application && $resource->isLogDrainEnabled()) {
+ data_set($service, 'logging', generate_fluentd_configuration());
+ }
}
if ($serviceLabels->count() > 0) {
if ($resource->settings->is_container_label_escape_enabled) {
@@ -2167,498 +2925,1059 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'configs' => $topLevelConfigs->toArray(),
'secrets' => $topLevelSecrets->toArray(),
];
- if ($isSameDockerComposeFile) {
- $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2);
- $resource->docker_compose = Yaml::dump($finalServices, 10, 2);
- } else {
- $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2);
- $resource->docker_compose = Yaml::dump($finalServices, 10, 2);
- }
+ $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2);
+ $resource->docker_compose = Yaml::dump($finalServices, 10, 2);
+ data_forget($resource, 'environment_variables');
+ data_forget($resource, 'environment_variables_preview');
$resource->save();
return collect($finalServices);
}
}
-function parseEnvVariable(Str|string $value)
+function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
{
- $value = str($value);
- $count = substr_count($value->value(), '_');
- $command = null;
- $forService = null;
- $generatedValue = null;
- $port = null;
- if ($value->startsWith('SERVICE')) {
- if ($count === 2) {
- if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
- // SERVICE_FQDN_UMAMI
- $command = $value->after('SERVICE_')->beforeLast('_');
- $forService = $value->afterLast('_');
- } else {
- // SERVICE_BASE64_UMAMI
- $command = $value->after('SERVICE_')->beforeLast('_');
+ $isApplication = $resource instanceof Application;
+ $isService = $resource instanceof Service;
+
+ $uuid = data_get($resource, 'uuid');
+ $compose = data_get($resource, 'docker_compose_raw');
+ if (! $compose) {
+ return collect([]);
+ }
+
+ if ($isApplication) {
+ $nameOfId = 'application_id';
+ $pullRequestId = $pull_request_id;
+ $isPullRequest = $pullRequestId == 0 ? false : true;
+ $server = data_get($resource, 'destination.server');
+ $fileStorages = $resource->fileStorages();
+ } elseif ($isService) {
+ $nameOfId = 'service_id';
+ $server = data_get($resource, 'server');
+ $allServices = get_service_templates();
+ } else {
+ return collect([]);
+ }
+
+ try {
+ $yaml = Yaml::parse($compose);
+ } catch (\Exception $e) {
+ return collect([]);
+ }
+
+ $services = data_get($yaml, 'services', collect([]));
+ $topLevel = collect([
+ 'volumes' => collect(data_get($yaml, 'volumes', [])),
+ 'networks' => collect(data_get($yaml, 'networks', [])),
+ 'configs' => collect(data_get($yaml, 'configs', [])),
+ 'secrets' => collect(data_get($yaml, 'secrets', [])),
+ ]);
+ // If there are predefined volumes, make sure they are not null
+ if ($topLevel->get('volumes')->count() > 0) {
+ $temp = collect([]);
+ foreach ($topLevel['volumes'] as $volumeName => $volume) {
+ if (is_null($volume)) {
+ continue;
}
+ $temp->put($volumeName, $volume);
}
- if ($count === 3) {
- if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
- // SERVICE_FQDN_UMAMI_1000
- $command = $value->after('SERVICE_')->before('_');
- $forService = $value->after('SERVICE_')->after('_')->before('_');
- $port = $value->afterLast('_');
- if (filter_var($port, FILTER_VALIDATE_INT) === false) {
- $port = null;
+ $topLevel['volumes'] = $temp;
+ }
+ // Get the base docker network
+ $baseNetwork = collect([$uuid]);
+ if ($isApplication && $isPullRequest) {
+ $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]);
+ }
+
+ $parsedServices = collect([]);
+ // ray()->clearAll();
+
+ $allMagicEnvironments = collect([]);
+ foreach ($services as $serviceName => $service) {
+ $predefinedPort = null;
+ $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) {
+ $containerName = "$serviceName-{$resource->uuid}";
+
+ if ($serviceName === 'registry') {
+ $tempServiceName = 'docker-registry';
+ } else {
+ $tempServiceName = $serviceName;
+ }
+ if (str(data_get($service, 'image'))->contains('glitchtip')) {
+ $tempServiceName = 'glitchtip';
+ }
+ if ($serviceName === 'supabase-kong') {
+ $tempServiceName = 'supabase';
+ }
+ $serviceDefinition = data_get($allServices, $tempServiceName);
+ $predefinedPort = data_get($serviceDefinition, 'port');
+ if ($serviceName === 'plausible') {
+ $predefinedPort = '8000';
+ }
+ if ($isDatabase) {
+ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
+ if ($applicationFound) {
+ $savedService = $applicationFound;
+ $savedService = ServiceDatabase::firstOrCreate([
+ 'name' => $applicationFound->name,
+ 'image' => $applicationFound->image,
+ 'service_id' => $applicationFound->service_id,
+ ]);
+ $applicationFound->delete();
+ } else {
+ $savedService = ServiceDatabase::firstOrCreate([
+ 'name' => $serviceName,
+ 'image' => $image,
+ 'service_id' => $resource->id,
+ ]);
}
} else {
- // SERVICE_BASE64_64_UMAMI
- $command = $value->after('SERVICE_')->beforeLast('_');
+ $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
+ if (substr_count(str($key)->value(), '_') === 3) {
+ $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
+ $port = $key->afterLast('_')->value();
+ } else {
+ $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
+ $port = null;
+ }
+ if ($isApplication) {
+ $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
+ } elseif ($isService) {
+ if ($fqdnFor) {
+ $fqdn = generateFqdn($server, "$fqdnFor-$uuid");
+ } else {
+ $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
+ }
+ }
+
+ if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
+ $path = $value->value();
+ if ($path !== '/') {
+ $fqdn = "$fqdn$path";
+ }
+ }
+ $fqdnWithPort = $fqdn;
+ if ($port) {
+ $fqdnWithPort = "$fqdn:$port";
+ }
+ if ($isApplication && is_null($resource->fqdn)) {
+ data_forget($resource, 'environment_variables');
+ data_forget($resource, 'environment_variables_preview');
+ $resource->fqdn = $fqdnWithPort;
+ $resource->save();
+ } elseif ($isService && is_null($savedService->fqdn)) {
+ $savedService->fqdn = $fqdnWithPort;
+ $savedService->save();
+ }
+
+ if (substr_count(str($key)->value(), '_') === 2) {
+ $resource->environment_variables()->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 = parseCommandFromMagicEnvVariable($key);
+ $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);
+ $logging = data_get($service, 'logging');
+
+ if ($server->isLogDrainEnabled()) {
+ if ($resource instanceof Application && $resource->isLogDrainEnabled()) {
+ $logging = generate_fluentd_configuration();
+ }
+ }
+ $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([]);
+
+ if ($isApplication) {
+ $baseName = generateApplicationContainerName(
+ application: $resource,
+ pull_request_id: $pullRequestId
+ );
+ $containerName = "$serviceName-$baseName";
+ $predefinedPort = null;
+ } elseif ($isService) {
+ $containerName = "$serviceName-{$resource->uuid}";
+
+ if ($serviceName === 'registry') {
+ $tempServiceName = 'docker-registry';
+ } else {
+ $tempServiceName = $serviceName;
+ }
+ if (str(data_get($service, 'image'))->contains('glitchtip')) {
+ $tempServiceName = 'glitchtip';
+ }
+ if ($serviceName === 'supabase-kong') {
+ $tempServiceName = 'supabase';
+ }
+ $serviceDefinition = data_get($allServices, $tempServiceName);
+ $predefinedPort = data_get($serviceDefinition, 'port');
+ if ($serviceName === 'plausible') {
+ $predefinedPort = '8000';
+ }
+
+ if ($isDatabase) {
+ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
+ if ($applicationFound) {
+ $savedService = $applicationFound;
+ $savedService = ServiceDatabase::firstOrCreate([
+ 'name' => $applicationFound->name,
+ 'image' => $applicationFound->image,
+ 'service_id' => $applicationFound->service_id,
+ ]);
+ $applicationFound->delete();
+ } else {
+ $savedService = ServiceDatabase::firstOrCreate([
+ 'name' => $serviceName,
+ 'image' => $image,
+ 'service_id' => $resource->id,
+ ]);
+ }
+ } else {
+ $savedService = ServiceApplication::firstOrCreate([
+ 'name' => $serviceName,
+ 'image' => $image,
+ 'service_id' => $resource->id,
+ ]);
+ }
+ $fileStorages = $savedService->fileStorages();
+ if ($savedService->image !== $image) {
+ $savedService->image = $image;
+ $savedService->save();
+ }
+ }
+
+ $originalResource = $isApplication ? $resource : $savedService;
+
+ if ($volumes->count() > 0) {
+ foreach ($volumes as $index => $volume) {
+ $type = null;
+ $source = null;
+ $target = null;
+ $content = null;
+ $isDirectory = false;
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ $foundConfig = $fileStorages->whereMountPath($target)->first();
+ if (sourceIsLocal($source)) {
+ $type = str('bind');
+ if ($foundConfig) {
+ $contentNotNull_temp = data_get($foundConfig, 'content');
+ if ($contentNotNull_temp) {
+ $content = $contentNotNull_temp;
+ }
+ $isDirectory = data_get($foundConfig, 'is_directory');
+ } else {
+ // By default, we cannot determine if the bind is a directory or not, so we set it to directory
+ $isDirectory = true;
+ }
+ } else {
+ $type = str('volume');
+ }
+ } elseif (is_array($volume)) {
+ $type = data_get_str($volume, 'type');
+ $source = data_get_str($volume, 'source');
+ $target = data_get_str($volume, 'target');
+ $content = data_get($volume, 'content');
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+
+ $foundConfig = $fileStorages->whereMountPath($target)->first();
+ if ($foundConfig) {
+ $contentNotNull_temp = data_get($foundConfig, 'content');
+ if ($contentNotNull_temp) {
+ $content = $contentNotNull_temp;
+ }
+ $isDirectory = data_get($foundConfig, 'is_directory');
+ } else {
+ // if isDirectory is not set (or false) & content is also not set, we assume it is a directory
+ if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
+ $isDirectory = true;
+ }
+ }
+ }
+ if ($type->value() === 'bind') {
+ if ($source->value() === '/var/run/docker.sock') {
+ $volume = $source->value().':'.$target->value();
+ } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') {
+ $volume = $source->value().':'.$target->value();
+ } else {
+ if ((int) $resource->compose_parsing_version >= 4) {
+ if ($isApplication) {
+ $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
+ } elseif ($isService) {
+ $mainDirectory = str(base_configuration_dir().'/services/'.$uuid);
+ }
+ } else {
+ $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
+ }
+ $source = replaceLocalSource($source, $mainDirectory);
+ if ($isApplication && $isPullRequest) {
+ $source = $source."-pr-$pullRequestId";
+ }
+ LocalFileVolume::updateOrCreate(
+ [
+ 'mount_path' => $target,
+ 'resource_id' => $originalResource->id,
+ 'resource_type' => get_class($originalResource),
+ ],
+ [
+ 'fs_path' => $source,
+ 'mount_path' => $target,
+ 'content' => $content,
+ 'is_directory' => $isDirectory,
+ 'resource_id' => $originalResource->id,
+ 'resource_type' => get_class($originalResource),
+ ]
+ );
+ if (isDev()) {
+ if ((int) $resource->compose_parsing_version >= 4) {
+ if ($isApplication) {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
+ } elseif ($isService) {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid);
+ }
+ } else {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
+ }
+ }
+ $volume = "$source:$target";
+ }
+ } elseif ($type->value() === 'volume') {
+ if ($topLevel->get('volumes')->has($source->value())) {
+ $temp = $topLevel->get('volumes')->get($source->value());
+ if (data_get($temp, 'driver_opts.type') === 'cifs') {
+ continue;
+ }
+ if (data_get($temp, 'driver_opts.type') === 'nfs') {
+ continue;
+ }
+ }
+ $slugWithoutUuid = Str::slug($source, '-');
+ $name = "{$uuid}_{$slugWithoutUuid}";
+
+ if ($isApplication && $isPullRequest) {
+ $name = "{$name}-pr-$pullRequestId";
+ }
+ if (is_string($volume)) {
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
+ $source = $name;
+ $volume = "$source:$target";
+ } elseif (is_array($volume)) {
+ data_set($volume, 'source', $name);
+ }
+ $topLevel->get('volumes')->put($name, [
+ 'name' => $name,
+ ]);
+ LocalPersistentVolume::updateOrCreate(
+ [
+ 'name' => $name,
+ 'resource_id' => $originalResource->id,
+ 'resource_type' => get_class($originalResource),
+ ],
+ [
+ 'name' => $name,
+ 'mount_path' => $target,
+ 'resource_id' => $originalResource->id,
+ 'resource_type' => get_class($originalResource),
+ ]
+ );
+ }
+ dispatch(new ServerFilesFromServerJob($originalResource));
+ $volumesParsed->put($index, $volume);
+ }
+ }
+
+ if ($depends_on?->count() > 0) {
+ if ($isApplication && $isPullRequest) {
+ $newDependsOn = collect([]);
+ $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) {
+ if (is_numeric($condition)) {
+ $dependency = "$dependency-pr-$pullRequestId";
+
+ $newDependsOn->put($condition, $dependency);
+ } else {
+ $condition = "$condition-pr-$pullRequestId";
+ $newDependsOn->put($condition, $dependency);
+ }
+ });
+ $depends_on = $newDependsOn;
+ }
+ }
+ 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,
+ ]);
+ }
+ }
+ }
+
+ // Collect/create/update ports
+ $collectedPorts = collect([]);
+ if ($ports->count() > 0) {
+ foreach ($ports as $sport) {
+ if (is_string($sport) || is_numeric($sport)) {
+ $collectedPorts->push($sport);
+ }
+ if (is_array($sport)) {
+ $target = data_get($sport, 'target');
+ $published = data_get($sport, 'published');
+ $protocol = data_get($sport, 'protocol');
+ $collectedPorts->push("$target:$published/$protocol");
+ }
+ }
+ }
+ if ($isService) {
+ $originalResource->ports = $collectedPorts->implode(',');
+ $originalResource->save();
+ }
+
+ $networks_temp = collect();
+
+ 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;
+ $networks_temp->put($network, null);
+ $topLevel->get('networks')->put($network, [
+ 'name' => $network,
+ 'external' => true,
+ ]);
+ }
+ }
+ }
+
+ $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);
+ $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,
+ ], [
+ 'value' => $value,
+ '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()) {
+ // This means the variable does not have a default value, so it needs to be created in Coolify
+ $parsedKeyValue = replaceVariables($value);
+ $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([
+ 'key' => $parsedKeyValue,
+ $nameOfId => $resource->id,
+ ], [
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ // Add the variable to the environment so it will be shown in the deployable compose file
+ $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->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) {
+ $branch = $originalResource->git_branch;
+ if ($pullRequestId !== 0) {
+ $branch = "pull/{$pullRequestId}/head";
+ }
+ if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\"");
+ }
+ }
+
+ // Add COOLIFY_CONTAINER_NAME to environment
+ if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\"");
+ }
+
+ if ($isApplication) {
+ $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]);
+ $fqdns = data_get($domains, "$serviceName.domain");
+ if ($fqdns) {
+ $fqdns = str($fqdns)->explode(',');
+ if ($isPullRequest) {
+ $preview = $resource->previews()->find($preview_id);
+ $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains')));
+ if ($docker_compose_domains->count() > 0) {
+ $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain");
+ if ($found_fqdn) {
+ $fqdns = collect($found_fqdn);
+ } else {
+ $fqdns = collect([]);
+ }
+ } else {
+ $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) {
+ $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId);
+ $url = Url::fromString($fqdn);
+ $template = $resource->preview_url_template;
+ $host = $url->getHost();
+ $schema = $url->getScheme();
+ $random = new Cuid2;
+ $preview_fqdn = str_replace('{{random}}', $random, $template);
+ $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
+ $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn);
+ $preview_fqdn = "$schema://$preview_fqdn";
+ $preview->fqdn = $preview_fqdn;
+ $preview->save();
+
+ return $preview_fqdn;
+ });
+ }
+ }
+ }
+ $defaultLabels = defaultLabels(
+ id: $resource->id,
+ name: $containerName,
+ pull_request_id: $pullRequestId,
+ type: 'application'
+ );
+ } elseif ($isService) {
+ if ($savedService->serviceType()) {
+ $fqdns = generateServiceSpecificFqdns($savedService);
+ } else {
+ $fqdns = collect(data_get($savedService, 'fqdns'))->filter();
+ }
+ $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
+ }
+ // Add COOLIFY_FQDN & COOLIFY_URL to environment
+ if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
+ $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(','));
+
+ $urls = $fqdns->map(function ($fqdn) {
+ return str($fqdn)->replace('http://', '')->replace('https://', '');
+ });
+ $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(','));
+ }
+ 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_');
+ })->map(function ($value, $key) use ($resource) {
+ // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
+ if (str($value)->isEmpty()) {
+ if ($resource->environment_variables()->where('key', $key)->exists()) {
+ $value = $resource->environment_variables()->where('key', $key)->first()->value;
+ } else {
+ $value = null;
+ }
+ }
+
+ return $value;
+ });
+ }
+ $serviceLabels = $labels->merge($defaultLabels);
+ if ($serviceLabels->count() > 0) {
+ if ($isApplication) {
+ $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled');
+ } else {
+ $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled');
+ }
+ if ($isContainerLabelEscapeEnabled) {
+ $serviceLabels = $serviceLabels->map(function ($value, $key) {
+ return escapeDollarSign($value);
+ });
+ }
+ }
+ if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
+ if ($isApplication) {
+ $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
+ $uuid = $resource->uuid;
+ $network = data_get($resource, 'destination.network');
+ if ($isPullRequest) {
+ $uuid = "{$resource->uuid}-{$pullRequestId}";
+ }
+ if ($isPullRequest) {
+ $network = "{$resource->destination->network}-{$pullRequestId}";
+ }
+ } else {
+ $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
+ $uuid = $resource->uuid;
+ $network = data_get($resource, 'destination.network');
+ }
+ if ($shouldGenerateLabelsExactly) {
+ switch ($server->proxyType()) {
+ case ProxyTypes::TRAEFIK->value:
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
+ uuid: $uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $originalResource->isGzipEnabled(),
+ is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: $image
+ ));
+ break;
+ case ProxyTypes::CADDY->value:
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
+ network: $network,
+ uuid: $uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $originalResource->isGzipEnabled(),
+ is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: $image,
+ predefinedPort: $predefinedPort
+ ));
+ break;
+ }
+ } else {
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
+ uuid: $uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $originalResource->isGzipEnabled(),
+ is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: $image
+ ));
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
+ network: $network,
+ uuid: $uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $originalResource->isGzipEnabled(),
+ is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: $image,
+ predefinedPort: $predefinedPort
+
+ ));
+ }
+ }
+ if ($isService) {
+ if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) {
+ $savedService->update(['exclude_from_status' => true]);
+ }
+ }
+ data_forget($service, 'volumes.*.content');
+ data_forget($service, 'volumes.*.isDirectory');
+ data_forget($service, 'volumes.*.is_directory');
+ data_forget($service, 'exclude_from_hc');
+
+ $volumesParsed = $volumesParsed->map(function ($volume) {
+ data_forget($volume, 'content');
+ data_forget($volume, 'is_directory');
+ data_forget($volume, 'isDirectory');
+
+ return $volume;
+ });
+
+ $payload = collect($service)->merge([
+ 'container_name' => $containerName,
+ 'restart' => $restart->value(),
+ '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 || $coolifyEnvironments->count() > 0) {
+ $payload['environment'] = $environment->merge($coolifyEnvironments);
+ }
+ if ($logging) {
+ $payload['logging'] = $logging;
+ }
+ if ($depends_on->count() > 0) {
+ $payload['depends_on'] = $depends_on;
+ }
+ if ($isApplication && $isPullRequest) {
+ $serviceName = "{$serviceName}-pr-{$pullRequestId}";
+ }
+
+ $parsedServices->put($serviceName, $payload);
+ }
+ $topLevel->put('services', $parsedServices);
+
+ $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
+
+ $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {
+ return array_search($key, $customOrder);
+ });
+
+ $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2);
+ data_forget($resource, 'environment_variables');
+ data_forget($resource, 'environment_variables_preview');
+ $resource->save();
+
+ return $topLevel;
+}
+
+function generate_fluentd_configuration(): array
+{
return [
- 'command' => $command,
- 'forService' => $forService,
- 'generatedValue' => $generatedValue,
- 'port' => $port,
+ 'driver' => 'fluentd',
+ 'options' => [
+ 'fluentd-address' => 'tcp://127.0.0.1:24224',
+ 'fluentd-async' => 'true',
+ 'fluentd-sub-second-precision' => 'true',
+ // env vars are used in the LogDrain configurations
+ 'env' => 'COOLIFY_APP_NAME,COOLIFY_PROJECT_NAME,COOLIFY_SERVER_IP,COOLIFY_ENVIRONMENT_NAME',
+ ],
];
}
-function generateEnvValue(string $command, ?Service $service = null)
+
+function isAssociativeArray($array)
{
- switch ($command) {
- case 'PASSWORD':
- $generatedValue = Str::password(symbols: false);
- break;
- case 'PASSWORD_64':
- $generatedValue = Str::password(length: 64, symbols: false);
- break;
- // This is not base64, it's just a random string
- case 'BASE64_64':
- $generatedValue = Str::random(64);
- break;
- case 'BASE64_128':
- $generatedValue = Str::random(128);
- break;
- case 'BASE64':
- case 'BASE64_32':
- $generatedValue = Str::random(32);
- break;
- // This is base64,
- case 'REALBASE64_64':
- $generatedValue = base64_encode(Str::random(64));
- break;
- case 'REALBASE64_128':
- $generatedValue = base64_encode(Str::random(128));
- break;
- case 'REALBASE64':
- case 'REALBASE64_32':
- $generatedValue = base64_encode(Str::random(32));
- break;
- case 'USER':
- $generatedValue = Str::random(16);
- break;
- case 'SUPABASEANON':
- $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
- if (is_null($signingKey)) {
- return;
- } else {
- $signingKey = $signingKey->value;
- }
- $key = InMemory::plainText($signingKey);
- $algorithm = new Sha256;
- $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
- $now = $now->setTime($now->format('H'), $now->format('i'));
- $token = $tokenBuilder
- ->issuedBy('supabase')
- ->issuedAt($now)
- ->expiresAt($now->modify('+100 year'))
- ->withClaim('role', 'anon')
- ->getToken($algorithm, $key);
- $generatedValue = $token->toString();
- break;
- case 'SUPABASESERVICE':
- $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first();
- if (is_null($signingKey)) {
- return;
- } else {
- $signingKey = $signingKey->value;
- }
- $key = InMemory::plainText($signingKey);
- $algorithm = new Sha256;
- $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
- $now = new DateTimeImmutable;
- $now = $now->setTime($now->format('H'), $now->format('i'));
- $token = $tokenBuilder
- ->issuedBy('supabase')
- ->issuedAt($now)
- ->expiresAt($now->modify('+100 year'))
- ->withClaim('role', 'service_role')
- ->getToken($algorithm, $key);
- $generatedValue = $token->toString();
- break;
- default:
- $generatedValue = Str::random(16);
- break;
+ if ($array instanceof Collection) {
+ $array = $array->toArray();
}
- return $generatedValue;
+ if (! is_array($array)) {
+ throw new \InvalidArgumentException('Input must be an array or a Collection.');
+ }
+
+ if ($array === []) {
+ return false;
+ }
+
+ return array_keys($array) !== range(0, count($array) - 1);
}
-function getRealtime()
+/**
+ * This method adds the default environment variables to the resource.
+ * - COOLIFY_APP_NAME
+ * - COOLIFY_PROJECT_NAME
+ * - COOLIFY_SERVER_IP
+ * - COOLIFY_ENVIRONMENT_NAME
+ *
+ * Theses variables are added in place to the $where_to_add array.
+ */
+function add_coolify_default_environment_variables(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|Application|Service $resource, Collection &$where_to_add, ?Collection $where_to_check = null)
{
- $envDefined = env('PUSHER_PORT');
- if (empty($envDefined)) {
- $url = Url::fromString(Request::getSchemeAndHttpHost());
- $port = $url->getPort();
- if ($port) {
- return '6001';
+ if ($resource instanceof Service) {
+ $ip = $resource->server->ip;
+ } else {
+ $ip = $resource->destination->server->ip;
+ }
+ if (isAssociativeArray($where_to_add)) {
+ $isAssociativeArray = true;
+ } else {
+ $isAssociativeArray = false;
+ }
+ if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) {
+ if ($isAssociativeArray) {
+ $where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\"");
} else {
- return null;
- }
- } else {
- return $envDefined;
- }
-}
-
-function validate_dns_entry(string $fqdn, Server $server)
-{
- // https://www.cloudflare.com/ips-v4/#
- $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']);
-
- $url = Url::fromString($fqdn);
- $host = $url->getHost();
- if (str($host)->contains('sslip.io')) {
- return true;
- }
- $settings = \App\Models\InstanceSettings::get();
- $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
- if (! $is_dns_validation_enabled) {
- return true;
- }
- $dns_servers = data_get($settings, 'custom_dns_servers');
- $dns_servers = str($dns_servers)->explode(',');
- if ($server->id === 0) {
- $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip));
- } else {
- $ip = $server->ip;
- }
- $found_matching_ip = false;
- $type = \PurplePixie\PhpDns\DNSTypes::NAME_A;
- foreach ($dns_servers as $dns_server) {
- try {
- ray("Checking $host on $dns_server");
- $query = new DNSQuery($dns_server);
- $results = $query->query($host, $type);
- if ($results === false || $query->hasError()) {
- ray('Error: '.$query->getLasterror());
- } else {
- foreach ($results as $result) {
- if ($result->getType() == $type) {
- if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) {
- ray("Found match in Cloudflare IPs: $match");
- $found_matching_ip = true;
- break;
- }
- if ($result->getData() === $ip) {
- ray($host.' has IP address '.$result->getData());
- ray($result->getString());
- $found_matching_ip = true;
- break;
- }
- }
- }
- }
- } catch (\Exception $e) {
+ $where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\"");
}
}
- ray("Found match: $found_matching_ip");
-
- return $found_matching_ip;
-}
-
-function ip_match($ip, $cidrs, &$match = null)
-{
- foreach ((array) $cidrs as $cidr) {
- [$subnet, $mask] = explode('/', $cidr);
- if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) {
- $match = $cidr;
-
- return true;
- }
- }
-
- return false;
-}
-function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null)
-{
- if (is_null($teamId)) {
- return response()->json(['error' => 'Team ID is required.'], 400);
- }
- if (is_array($domains)) {
- $domains = collect($domains);
- }
-
- $domains = $domains->map(function ($domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
-
- return str($domain);
- });
- $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
- $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']);
- if ($uuid) {
- $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid);
- $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid);
- }
- $domainFound = false;
- foreach ($applications as $app) {
- if (is_null($app->fqdn)) {
- continue;
- }
- $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
- foreach ($list_of_domains as $domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- $domainFound = true;
- break;
- }
- }
- }
- if ($domainFound) {
- return true;
- }
- foreach ($serviceApplications as $app) {
- if (str($app->fqdn)->isEmpty()) {
- continue;
- }
- $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
- foreach ($list_of_domains as $domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- $domainFound = true;
- break;
- }
- }
- }
- if ($domainFound) {
- return true;
- }
- $settings = \App\Models\InstanceSettings::get();
- if (data_get($settings, 'fqdn')) {
- $domain = data_get($settings, 'fqdn');
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- return true;
- }
- }
-}
-function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
-{
- if ($resource) {
- if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') {
- $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain');
- $domains = collect($domains);
+ if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) {
+ if ($isAssociativeArray) {
+ $where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\"");
} else {
- $domains = collect($resource->fqdns);
+ $where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\"");
}
- } elseif ($domain) {
- $domains = collect($domain);
+ }
+ if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) {
+ if ($isAssociativeArray) {
+ $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\"");
+ } else {
+ $where_to_add->push("COOLIFY_ENVIRONMENT_NAME=\"{$resource->environment->name}\"");
+ }
+ }
+ if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) {
+ if ($isAssociativeArray) {
+ $where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\"");
+ } else {
+ $where_to_add->push("COOLIFY_PROJECT_NAME=\"{$resource->project()->name}\"");
+ }
+ }
+}
+
+function convertComposeEnvironmentToArray($environment)
+{
+ $convertedServiceVariables = collect([]);
+ if (isAssociativeArray($environment)) {
+ // Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux'];
+ if ($environment instanceof Collection) {
+ $changedEnvironment = collect([]);
+ $environment->each(function ($value, $key) use ($changedEnvironment) {
+ if (is_numeric($key)) {
+ $parts = explode('=', $value, 2);
+ if (count($parts) === 2) {
+ $key = $parts[0];
+ $realValue = $parts[1] ?? '';
+ $changedEnvironment->put($key, $realValue);
+ } else {
+ $changedEnvironment->put($key, $value);
+ }
+ } else {
+ $changedEnvironment->put($key, $value);
+ }
+ });
+
+ return $changedEnvironment;
+ }
+ $convertedServiceVariables = $environment;
} else {
- throw new \RuntimeException('No resource or FQDN provided.');
- }
- $domains = $domains->map(function ($domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
-
- return str($domain);
- });
- $apps = Application::all();
- foreach ($apps as $app) {
- $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
- foreach ($list_of_domains as $domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- if (data_get($resource, 'uuid')) {
- if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
- }
- } elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
+ // Example: $environment = ['FOO=bar', 'BAZ=qux'];
+ foreach ($environment as $value) {
+ if (is_string($value)) {
+ $parts = explode('=', $value, 2);
+ $key = $parts[0];
+ $realValue = $parts[1] ?? '';
+ if ($key) {
+ $convertedServiceVariables->put($key, $realValue);
}
}
}
}
- $apps = ServiceApplication::all();
- foreach ($apps as $app) {
- $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
- foreach ($list_of_domains as $domain) {
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- if (data_get($resource, 'uuid')) {
- if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
- }
- } elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: {$app->name}.");
- }
- }
- }
- }
- if ($resource) {
- $settings = \App\Models\InstanceSettings::get();
- if (data_get($settings, 'fqdn')) {
- $domain = data_get($settings, 'fqdn');
- if (str($domain)->endsWith('/')) {
- $domain = str($domain)->beforeLast('/');
- }
- $naked_domain = str($domain)->value();
- if ($domains->contains($naked_domain)) {
- throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance.");
- }
- }
- }
-}
-function parseCommandsByLineForSudo(Collection $commands, Server $server): array
+ return $convertedServiceVariables;
+
+}
+function instanceSettings()
{
- $commands = $commands->map(function ($line) {
- if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) {
- return "sudo $line";
- }
-
- return $line;
- });
- $commands = $commands->map(function ($line) use ($server) {
- if (Str::startsWith($line, 'sudo mkdir -p')) {
- return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
- }
-
- return $line;
- });
- $commands = $commands->map(function ($line) {
- $line = str($line);
- if (str($line)->contains('$(')) {
- $line = $line->replace('$(', '$(sudo ');
- }
- if (str($line)->contains('||')) {
- $line = $line->replace('||', '|| sudo');
- }
- if (str($line)->contains('&&')) {
- $line = $line->replace('&&', '&& sudo');
- }
- if (str($line)->contains(' | ')) {
- $line = $line->replace(' | ', ' | sudo ');
- }
-
- return $line->value();
- });
-
- return $commands->toArray();
-}
-function parseLineForSudo(string $command, Server $server): string
-{
- if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) {
- $command = "sudo $command";
- }
- if (Str::startsWith($command, 'sudo mkdir -p')) {
- $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p');
- }
- if (str($command)->contains('$(') || str($command)->contains('`')) {
- $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
- }
- if (str($command)->contains('||')) {
- $command = str($command)->replace('||', '|| sudo ')->value();
- }
- if (str($command)->contains('&&')) {
- $command = str($command)->replace('&&', '&& sudo ')->value();
- }
-
- return $command;
-}
-
-function get_public_ips()
-{
- try {
- echo "Refreshing public ips!\n";
- $settings = \App\Models\InstanceSettings::get();
- [$first, $second] = Process::concurrently(function (Pool $pool) {
- $pool->path(__DIR__)->command('curl -4s https://ifconfig.io');
- $pool->path(__DIR__)->command('curl -6s https://ifconfig.io');
- });
- $ipv4 = $first->output();
- if ($ipv4) {
- $ipv4 = trim($ipv4);
- $validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP);
- if ($validate_ipv4 == false) {
- echo "Invalid ipv4: $ipv4\n";
-
- return;
- }
- $settings->update(['public_ipv4' => $ipv4]);
- }
- $ipv6 = $second->output();
- if ($ipv6) {
- $ipv6 = trim($ipv6);
- $validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP);
- if ($validate_ipv6 == false) {
- echo "Invalid ipv6: $ipv6\n";
-
- return;
- }
- $settings->update(['public_ipv6' => $ipv6]);
- }
- } catch (\Throwable $e) {
- echo "Error: {$e->getMessage()}\n";
- }
-}
-
-function isAnyDeploymentInprogress()
-{
- // Only use it in the deployment script
- $count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count();
- if ($count > 0) {
- echo "There are $count deployments in progress. Exiting...\n";
- exit(1);
- }
- echo "No deployments in progress.\n";
- exit(0);
-}
-
-function generateSentinelToken()
-{
- $token = Str::random(64);
-
- return $token;
-}
-
-function isBase64Encoded($strValue)
-{
- return base64_encode(base64_decode($strValue, true)) === $strValue;
-}
-function customApiValidator(Collection|array $item, array $rules)
-{
- if (is_array($item)) {
- $item = collect($item);
- }
-
- return Validator::make($item->toArray(), $rules, [
- 'required' => 'This field is required.',
- ]);
+ return InstanceSettings::get();
}
diff --git a/composer.json b/composer.json
index a3189d341..03adf9823 100644
--- a/composer.json
+++ b/composer.json
@@ -14,10 +14,11 @@
"guzzlehttp/guzzle": "^7.5.0",
"laravel/fortify": "^v1.16.0",
"laravel/framework": "^v11",
- "laravel/horizon": "^5.23.1",
+ "laravel/horizon": "^5.27.1",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v4.0",
"laravel/socialite": "^v5.14.0",
+ "laravel/telescope": "^5.2",
"laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
@@ -47,6 +48,7 @@
"zircote/swagger-php": "^4.10"
},
"require-dev": {
+ "barryvdh/laravel-debugbar": "^3.13",
"fakerphp/faker": "^v1.21.0",
"laravel/dusk": "^v8.0",
"laravel/pint": "^1.16",
@@ -83,7 +85,11 @@
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate"
],
- "post-install-cmd": [],
+ "post-install-cmd": [
+ "cp -r 'hooks/' '.git/hooks/'",
+ "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
+ "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
+ ],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
@@ -93,7 +99,9 @@
},
"extra": {
"laravel": {
- "dont-discover": []
+ "dont-discover": [
+ "laravel/telescope"
+ ]
}
},
"config": {
diff --git a/composer.lock b/composer.lock
index 367dda58d..420d87ec0 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "cb17445966de6094aef5a92ee59d1d77",
+ "content-hash": "42c28ab141b70fcabf75b51afa96c670",
"packages": [
{
"name": "amphp/amp",
@@ -740,16 +740,16 @@
},
{
"name": "amphp/sync",
- "version": "v2.2.0",
+ "version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/sync.git",
- "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec"
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/sync/zipball/375ef5b54a0d12c38e12728dde05a55e30f2fbec",
- "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec",
+ "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1",
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1",
"shasum": ""
},
"require": {
@@ -803,7 +803,7 @@
],
"support": {
"issues": "https://github.com/amphp/sync/issues",
- "source": "https://github.com/amphp/sync/tree/v2.2.0"
+ "source": "https://github.com/amphp/sync/tree/v2.3.0"
},
"funding": [
{
@@ -811,7 +811,7 @@
"type": "github"
}
],
- "time": "2024-03-12T01:00:01+00:00"
+ "time": "2024-08-03T19:31:26+00:00"
},
{
"name": "amphp/windows-registry",
@@ -921,16 +921,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.316.1",
+ "version": "3.321.9",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "888cee2adf890a5b749cc22c0f05051b53619d33"
+ "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/888cee2adf890a5b749cc22c0f05051b53619d33",
- "reference": "888cee2adf890a5b749cc22c0f05051b53619d33",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5de5099cfe0e17cb3eb2fe51de0101c99bc9442a",
+ "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a",
"shasum": ""
},
"require": {
@@ -983,7 +983,10 @@
],
"psr-4": {
"Aws\\": "src/"
- }
+ },
+ "exclude-from-classmap": [
+ "src/data/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -1010,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.316.1"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.321.9"
},
- "time": "2024-07-09T18:09:27+00:00"
+ "time": "2024-09-11T18:15:49+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -1253,23 +1256,23 @@
},
{
"name": "dasprid/enum",
- "version": "1.0.5",
+ "version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
- "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016"
+ "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016",
- "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
+ "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
- "phpunit/phpunit": "^7 | ^8 | ^9",
+ "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
@@ -1297,9 +1300,9 @@
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
- "source": "https://github.com/DASPRiD/Enum/tree/1.0.5"
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
},
- "time": "2023-08-25T16:18:39+00:00"
+ "time": "2024-08-09T14:30:48+00:00"
},
{
"name": "daverandom/libdns",
@@ -1515,16 +1518,16 @@
},
{
"name": "doctrine/dbal",
- "version": "3.8.6",
+ "version": "3.9.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1"
+ "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1",
- "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
+ "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
"shasum": ""
},
"require": {
@@ -1540,12 +1543,12 @@
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
- "phpstan/phpstan": "1.11.5",
+ "phpstan/phpstan": "1.12.0",
"phpstan/phpstan-strict-rules": "^1.6",
- "phpunit/phpunit": "9.6.19",
+ "phpunit/phpunit": "9.6.20",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
- "squizlabs/php_codesniffer": "3.10.1",
+ "squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0",
"vimeo/psalm": "4.30.0"
@@ -1608,7 +1611,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/3.8.6"
+ "source": "https://github.com/doctrine/dbal/tree/3.9.1"
},
"funding": [
{
@@ -1624,7 +1627,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T10:38:17+00:00"
+ "time": "2024-09-01T13:49:23+00:00"
},
{
"name": "doctrine/deprecations",
@@ -2196,24 +2199,24 @@
},
{
"name": "graham-campbell/result-type",
- "version": "v1.1.2",
+ "version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
- "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862"
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862",
- "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862",
+ "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
- "phpoption/phpoption": "^1.9.2"
+ "phpoption/phpoption": "^1.9.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
@@ -2242,7 +2245,7 @@
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
- "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2"
+ "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
@@ -2254,26 +2257,26 @@
"type": "tidelift"
}
],
- "time": "2023-11-12T22:16:48+00:00"
+ "time": "2024-07-20T21:45:45+00:00"
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.8.1",
+ "version": "7.9.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
- "reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
+ "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
- "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+ "guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -2284,9 +2287,9 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
- "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+ "guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@@ -2364,7 +2367,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.8.1"
+ "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
},
"funding": [
{
@@ -2380,20 +2383,20 @@
"type": "tidelift"
}
],
- "time": "2023-12-03T20:35:24+00:00"
+ "time": "2024-07-24T11:22:20+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.0.2",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223"
+ "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223",
- "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8",
+ "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8",
"shasum": ""
},
"require": {
@@ -2401,7 +2404,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"type": "library",
"extra": {
@@ -2447,7 +2450,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.0.2"
+ "source": "https://github.com/guzzle/promises/tree/2.0.3"
},
"funding": [
{
@@ -2463,20 +2466,20 @@
"type": "tidelift"
}
],
- "time": "2023-12-03T20:19:20+00:00"
+ "time": "2024-07-18T10:29:17+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.6.2",
+ "version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221"
+ "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221",
- "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
+ "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201",
"shasum": ""
},
"require": {
@@ -2491,8 +2494,8 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "http-interop/http-factory-tests": "^0.9",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -2563,7 +2566,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.6.2"
+ "source": "https://github.com/guzzle/psr7/tree/2.7.0"
},
"funding": [
{
@@ -2579,7 +2582,7 @@
"type": "tidelift"
}
],
- "time": "2023-12-03T20:05:35+00:00"
+ "time": "2024-07-18T11:15:46+00:00"
},
{
"name": "guzzlehttp/uri-template",
@@ -2786,16 +2789,16 @@
},
{
"name": "laravel/fortify",
- "version": "v1.21.5",
+ "version": "v1.24.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
- "reference": "3eaf01ec826c4f653628202640a4450784f78b15"
+ "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/fortify/zipball/3eaf01ec826c4f653628202640a4450784f78b15",
- "reference": "3eaf01ec826c4f653628202640a4450784f78b15",
+ "url": "https://api.github.com/repos/laravel/fortify/zipball/8158ba0960bb5f4aae509d01d74a95e16e30de20",
+ "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20",
"shasum": ""
},
"require": {
@@ -2847,20 +2850,20 @@
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
- "time": "2024-07-04T14:36:27+00:00"
+ "time": "2024-09-03T10:02:14+00:00"
},
{
"name": "laravel/framework",
- "version": "v11.15.0",
+ "version": "v11.23.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "ba85f1c019bed59b3c736c9c4502805efd0ba84b"
+ "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/ba85f1c019bed59b3c736c9c4502805efd0ba84b",
- "reference": "ba85f1c019bed59b3c736c9c4502805efd0ba84b",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3",
+ "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3",
"shasum": ""
},
"require": {
@@ -2922,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",
@@ -2964,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",
@@ -3022,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": {
@@ -3053,20 +3058,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2024-07-09T15:38:12+00:00"
+ "time": "2024-09-11T21:59:23+00:00"
},
{
"name": "laravel/horizon",
- "version": "v5.25.0",
+ "version": "v5.28.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
- "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab"
+ "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/horizon/zipball/81e62cee5b3feaf169d683b8890e33bf454698ab",
- "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab",
+ "url": "https://api.github.com/repos/laravel/horizon/zipball/9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f",
+ "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f",
"shasum": ""
},
"require": {
@@ -3130,22 +3135,22 @@
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
- "source": "https://github.com/laravel/horizon/tree/v5.25.0"
+ "source": "https://github.com/laravel/horizon/tree/v5.28.1"
},
- "time": "2024-07-05T16:46:31+00:00"
+ "time": "2024-09-04T14:06:50+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.1.24",
+ "version": "v0.1.25",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "409b0b4305273472f3754826e68f4edbd0150149"
+ "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/409b0b4305273472f3754826e68f4edbd0150149",
- "reference": "409b0b4305273472f3754826e68f4edbd0150149",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95",
+ "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95",
"shasum": ""
},
"require": {
@@ -3188,9 +3193,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.1.24"
+ "source": "https://github.com/laravel/prompts/tree/v0.1.25"
},
- "time": "2024-06-17T13:58:22+00:00"
+ "time": "2024-08-12T22:06:33+00:00"
},
{
"name": "laravel/sanctum",
@@ -3258,26 +3263,27 @@
},
{
"name": "laravel/serializable-closure",
- "version": "v1.3.3",
+ "version": "v1.3.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "3dbf8a8e914634c48d389c1234552666b3d43754"
+ "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754",
- "reference": "3dbf8a8e914634c48d389c1234552666b3d43754",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/61b87392d986dc49ad5ef64e75b1ff5fee24ef81",
+ "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81",
"shasum": ""
},
"require": {
"php": "^7.3|^8.0"
},
"require-dev": {
- "nesbot/carbon": "^2.61",
+ "illuminate/support": "^8.0|^9.0|^10.0|^11.0",
+ "nesbot/carbon": "^2.61|^3.0",
"pestphp/pest": "^1.21.3",
"phpstan/phpstan": "^1.8.2",
- "symfony/var-dumper": "^5.4.11"
+ "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0"
},
"type": "library",
"extra": {
@@ -3314,20 +3320,20 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2023-11-08T14:08:06+00:00"
+ "time": "2024-08-02T07:48:17+00:00"
},
{
"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": {
@@ -3386,7 +3392,76 @@
"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",
+ "version": "v5.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/telescope.git",
+ "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
+ "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^8.37|^9.0|^10.0|^11.0",
+ "php": "^8.0",
+ "symfony/console": "^5.3|^6.0|^7.0",
+ "symfony/var-dumper": "^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "ext-gd": "*",
+ "guzzlehttp/guzzle": "^6.0|^7.0",
+ "laravel/octane": "^1.4|^2.0|dev-develop",
+ "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.0|^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Telescope\\TelescopeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Telescope\\": "src/",
+ "Laravel\\Telescope\\Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Mohamed Said",
+ "email": "mohamed@laravel.com"
+ }
+ ],
+ "description": "An elegant debug assistant for the Laravel framework.",
+ "keywords": [
+ "debugging",
+ "laravel",
+ "monitoring"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/telescope/issues",
+ "source": "https://github.com/laravel/telescope/tree/v5.2.2"
+ },
+ "time": "2024-08-26T12:40:52+00:00"
},
{
"name": "laravel/tinker",
@@ -3592,16 +3667,16 @@
},
{
"name": "league/commonmark",
- "version": "2.4.2",
+ "version": "2.5.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf"
+ "reference": "b650144166dfa7703e62a22e493b853b58d874b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf",
- "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0",
+ "reference": "b650144166dfa7703e62a22e493b853b58d874b0",
"shasum": ""
},
"require": {
@@ -3614,8 +3689,8 @@
},
"require-dev": {
"cebe/markdown": "^1.0",
- "commonmark/cmark": "0.30.3",
- "commonmark/commonmark.js": "0.30.0",
+ "commonmark/cmark": "0.31.1",
+ "commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
@@ -3637,7 +3712,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "2.6-dev"
}
},
"autoload": {
@@ -3694,7 +3769,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-02T11:59:32+00:00"
+ "time": "2024-08-16T11:46:16+00:00"
},
{
"name": "league/config",
@@ -4459,16 +4534,16 @@
},
{
"name": "lorisleiva/laravel-actions",
- "version": "v2.8.0",
+ "version": "v2.8.4",
"source": {
"type": "git",
"url": "https://github.com/lorisleiva/laravel-actions.git",
- "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f"
+ "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/d5c2ca544f40d85f877b38eb6d23e9c967ecb69f",
- "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f",
+ "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/5a168bfdd3b75dd6ff259019d4aeef784bbd5403",
+ "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403",
"shasum": ""
},
"require": {
@@ -4477,7 +4552,7 @@
"php": "^8.1"
},
"require-dev": {
- "orchestra/testbench": "^9.0",
+ "orchestra/testbench": "^8.0|^9.0",
"pestphp/pest": "^1.23|^2.34",
"phpunit/phpunit": "^9.6|^10.0"
},
@@ -4518,11 +4593,12 @@
"controller",
"job",
"laravel",
+ "listener",
"object"
],
"support": {
"issues": "https://github.com/lorisleiva/laravel-actions/issues",
- "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.0"
+ "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.4"
},
"funding": [
{
@@ -4530,7 +4606,7 @@
"type": "github"
}
],
- "time": "2024-03-13T12:47:32+00:00"
+ "time": "2024-09-10T09:57:29+00:00"
},
{
"name": "lorisleiva/lody",
@@ -4707,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": {
@@ -4733,7 +4809,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7-dev"
+ "dev-master": "2.8-dev"
}
},
"autoload": {
@@ -4767,22 +4843,22 @@
],
"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",
- "version": "3.6.0",
+ "version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "39c8ef752db6865717cc3fba63970c16f057982c"
+ "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/39c8ef752db6865717cc3fba63970c16f057982c",
- "reference": "39c8ef752db6865717cc3fba63970c16f057982c",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f",
+ "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f",
"shasum": ""
},
"require": {
@@ -4875,7 +4951,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-20T15:52:59+00:00"
+ "time": "2024-08-19T06:22:39+00:00"
},
{
"name": "nette/schema",
@@ -4941,20 +5017,20 @@
},
{
"name": "nette/utils",
- "version": "v4.0.4",
+ "version": "v4.0.5",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218"
+ "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218",
- "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218",
+ "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
+ "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
"shasum": ""
},
"require": {
- "php": ">=8.0 <8.4"
+ "php": "8.0 - 8.4"
},
"conflict": {
"nette/finder": "<3",
@@ -5021,9 +5097,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.0.4"
+ "source": "https://github.com/nette/utils/tree/v4.0.5"
},
- "time": "2024-01-17T16:50:36+00:00"
+ "time": "2024-08-07T15:39:19+00:00"
},
{
"name": "nikic/php-parser",
@@ -5138,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": {
@@ -5157,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"
@@ -5206,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": [
{
@@ -5222,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": {
@@ -5288,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": [
{
@@ -5300,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": {
@@ -5367,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",
@@ -5562,16 +5638,16 @@
},
{
"name": "php-di/php-di",
- "version": "7.0.6",
+ "version": "7.0.7",
"source": {
"type": "git",
"url": "https://github.com/PHP-DI/PHP-DI.git",
- "reference": "8097948a89f6ec782839b3e958432f427cac37fd"
+ "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/8097948a89f6ec782839b3e958432f427cac37fd",
- "reference": "8097948a89f6ec782839b3e958432f427cac37fd",
+ "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
+ "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
"shasum": ""
},
"require": {
@@ -5619,7 +5695,7 @@
],
"support": {
"issues": "https://github.com/PHP-DI/PHP-DI/issues",
- "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.6"
+ "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.7"
},
"funding": [
{
@@ -5631,7 +5707,7 @@
"type": "tidelift"
}
],
- "time": "2023-11-02T10:04:50+00:00"
+ "time": "2024-07-21T15:55:45+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -5746,16 +5822,16 @@
},
{
"name": "phpoption/phpoption",
- "version": "1.9.2",
+ "version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820"
+ "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820",
- "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
@@ -5763,13 +5839,13 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
- "forward-command": true
+ "forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
@@ -5805,7 +5881,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.2"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
@@ -5817,20 +5893,20 @@
"type": "tidelift"
}
],
- "time": "2023-11-12T21:59:55+00:00"
+ "time": "2024-07-20T21:41:07+00:00"
},
{
"name": "phpseclib/phpseclib",
- "version": "3.0.39",
+ "version": "3.0.41",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
- "reference": "211ebc399c6e73c225a018435fe5ae209d1d1485"
+ "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/211ebc399c6e73c225a018435fe5ae209d1d1485",
- "reference": "211ebc399c6e73c225a018435fe5ae209d1d1485",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/621c73f7dcb310b61de34d1da4c4204e8ace6ceb",
+ "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb",
"shasum": ""
},
"require": {
@@ -5911,7 +5987,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
- "source": "https://github.com/phpseclib/phpseclib/tree/3.0.39"
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.41"
},
"funding": [
{
@@ -5927,20 +6003,20 @@
"type": "tidelift"
}
],
- "time": "2024-06-24T06:27:33+00:00"
+ "time": "2024-08-12T00:13:54+00:00"
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.29.1",
+ "version": "1.30.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4"
+ "reference": "51b95ec8670af41009e2b2b56873bad96682413e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4",
- "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e",
+ "reference": "51b95ec8670af41009e2b2b56873bad96682413e",
"shasum": ""
},
"require": {
@@ -5972,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.29.1"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1"
},
- "time": "2024-05-31T08:52:43+00:00"
+ "time": "2024-09-07T20:13:05+00:00"
},
{
"name": "phpstan/phpstan",
- "version": "1.11.7",
+ "version": "1.12.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d"
+ "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/52d2bbfdcae7f895915629e4694e9497d0f8e28d",
- "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0fcbf194ab63d8159bb70d9aa3e1350051632009",
+ "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009",
"shasum": ""
},
"require": {
@@ -6032,7 +6108,7 @@
"type": "github"
}
],
- "time": "2024-07-06T11:17:41+00:00"
+ "time": "2024-09-09T08:10:35+00:00"
},
{
"name": "pion/laravel-chunk-upload",
@@ -6146,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",
@@ -6192,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",
@@ -6558,16 +6634,16 @@
},
{
"name": "psr/log",
- "version": "3.0.0",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
- "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
@@ -6602,9 +6678,9 @@
"psr-3"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/3.0.0"
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
},
- "time": "2021-07-14T16:46:02+00:00"
+ "time": "2024-09-11T13:17:53+00:00"
},
{
"name": "psr/simple-cache",
@@ -7072,21 +7148,21 @@
},
{
"name": "rector/rector",
- "version": "1.2.0",
+ "version": "1.2.5",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "2fa387553db22b6f9bcccf5ff16f2c2c18a52a65"
+ "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/2fa387553db22b6f9bcccf5ff16f2c2c18a52a65",
- "reference": "2fa387553db22b6f9bcccf5ff16f2c2c18a52a65",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/e98aa793ca3fcd17e893cfaf9103ac049775d339",
+ "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
- "phpstan/phpstan": "^1.11"
+ "phpstan/phpstan": "^1.12.2"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -7119,7 +7195,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/1.2.0"
+ "source": "https://github.com/rectorphp/rector/tree/1.2.5"
},
"funding": [
{
@@ -7127,7 +7203,7 @@
"type": "github"
}
],
- "time": "2024-07-01T14:24:45+00:00"
+ "time": "2024-09-08T17:43:24+00:00"
},
{
"name": "resend/resend-laravel",
@@ -7329,16 +7405,16 @@
},
{
"name": "sentry/sentry",
- "version": "4.8.0",
+ "version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
- "reference": "3cf5778ff425a23f2d22ed41b423691d36f47163"
+ "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/3cf5778ff425a23f2d22ed41b423691d36f47163",
- "reference": "3cf5778ff425a23f2d22ed41b423691d36f47163",
+ "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/788ec170f51ebb22f2809a1e3f78b19ccd39b70d",
+ "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d",
"shasum": ""
},
"require": {
@@ -7402,7 +7478,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
- "source": "https://github.com/getsentry/sentry-php/tree/4.8.0"
+ "source": "https://github.com/getsentry/sentry-php/tree/4.9.0"
},
"funding": [
{
@@ -7414,27 +7490,27 @@
"type": "custom"
}
],
- "time": "2024-06-05T13:18:43+00:00"
+ "time": "2024-08-08T14:40:50+00:00"
},
{
"name": "sentry/sentry-laravel",
- "version": "4.6.1",
+ "version": "4.8.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
- "reference": "7f5fd9f362e440c4c0c492f386b93095321f9101"
+ "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/7f5fd9f362e440c4c0c492f386b93095321f9101",
- "reference": "7f5fd9f362e440c4c0c492f386b93095321f9101",
+ "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/2bbcb7e81097993cf64d5b296eaa6d396cddd5a7",
+ "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"nyholm/psr7": "^1.0",
"php": "^7.2 | ^8.0",
- "sentry/sentry": "^4.7",
+ "sentry/sentry": "^4.9",
"symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0"
},
"require-dev": {
@@ -7491,7 +7567,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-laravel/issues",
- "source": "https://github.com/getsentry/sentry-laravel/tree/4.6.1"
+ "source": "https://github.com/getsentry/sentry-laravel/tree/4.8.0"
},
"funding": [
{
@@ -7503,7 +7579,7 @@
"type": "custom"
}
],
- "time": "2024-06-18T15:06:09+00:00"
+ "time": "2024-08-15T19:03:01+00:00"
},
{
"name": "socialiteproviders/manager",
@@ -7632,16 +7708,16 @@
},
{
"name": "spatie/backtrace",
- "version": "1.6.1",
+ "version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
- "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23"
+ "reference": "1a9a145b044677ae3424693f7b06479fc8c137a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/backtrace/zipball/8373b9d51638292e3bfd736a9c19a654111b4a23",
- "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23",
+ "url": "https://api.github.com/repos/spatie/backtrace/zipball/1a9a145b044677ae3424693f7b06479fc8c137a9",
+ "reference": "1a9a145b044677ae3424693f7b06479fc8c137a9",
"shasum": ""
},
"require": {
@@ -7679,7 +7755,7 @@
"spatie"
],
"support": {
- "source": "https://github.com/spatie/backtrace/tree/1.6.1"
+ "source": "https://github.com/spatie/backtrace/tree/1.6.2"
},
"funding": [
{
@@ -7691,7 +7767,7 @@
"type": "other"
}
],
- "time": "2024-04-24T13:22:11+00:00"
+ "time": "2024-07-22T08:21:24+00:00"
},
{
"name": "spatie/laravel-activitylog",
@@ -7871,16 +7947,16 @@
},
{
"name": "spatie/laravel-package-tools",
- "version": "1.16.4",
+ "version": "1.16.5",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
- "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53"
+ "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53",
- "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53",
+ "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/c7413972cf22ffdff97b68499c22baa04eddb6a2",
+ "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2",
"shasum": ""
},
"require": {
@@ -7919,7 +7995,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
- "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4"
+ "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.5"
},
"funding": [
{
@@ -7927,20 +8003,20 @@
"type": "github"
}
],
- "time": "2024-03-20T07:29:11+00:00"
+ "time": "2024-08-27T18:56:10+00:00"
},
{
"name": "spatie/laravel-ray",
- "version": "1.37.0",
+ "version": "1.37.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ray.git",
- "reference": "f57b294a3815be37effa9d13f54f2fbe5a2fff37"
+ "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/f57b294a3815be37effa9d13f54f2fbe5a2fff37",
- "reference": "f57b294a3815be37effa9d13f54f2fbe5a2fff37",
+ "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/c2bedfd1172648df2c80aaceb2541d70f1d9a5b9",
+ "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9",
"shasum": ""
},
"require": {
@@ -8002,7 +8078,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-ray/issues",
- "source": "https://github.com/spatie/laravel-ray/tree/1.37.0"
+ "source": "https://github.com/spatie/laravel-ray/tree/1.37.1"
},
"funding": [
{
@@ -8014,7 +8090,7 @@
"type": "other"
}
],
- "time": "2024-07-03T08:48:44+00:00"
+ "time": "2024-07-12T12:35:17+00:00"
},
{
"name": "spatie/laravel-schemaless-attributes",
@@ -8144,16 +8220,16 @@
},
{
"name": "spatie/php-structure-discoverer",
- "version": "2.1.1",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/php-structure-discoverer.git",
- "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b"
+ "reference": "271542206169d95dd2ffe346ddf11f37672553a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/24f5221641560ec0f7dce23dd814e7d555b0098b",
- "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b",
+ "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/271542206169d95dd2ffe346ddf11f37672553a2",
+ "reference": "271542206169d95dd2ffe346ddf11f37672553a2",
"shasum": ""
},
"require": {
@@ -8212,7 +8288,7 @@
],
"support": {
"issues": "https://github.com/spatie/php-structure-discoverer/issues",
- "source": "https://github.com/spatie/php-structure-discoverer/tree/2.1.1"
+ "source": "https://github.com/spatie/php-structure-discoverer/tree/2.2.0"
},
"funding": [
{
@@ -8220,7 +8296,7 @@
"type": "github"
}
],
- "time": "2024-03-13T16:08:30+00:00"
+ "time": "2024-08-29T10:43:45+00:00"
},
{
"name": "spatie/ray",
@@ -8504,16 +8580,16 @@
},
{
"name": "symfony/console",
- "version": "v7.1.2",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "0aa29ca177f432ab68533432db0de059f39c92ae"
+ "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae",
- "reference": "0aa29ca177f432ab68533432db0de059f39c92ae",
+ "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111",
+ "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111",
"shasum": ""
},
"require": {
@@ -8577,7 +8653,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.1.2"
+ "source": "https://github.com/symfony/console/tree/v7.1.4"
},
"funding": [
{
@@ -8593,7 +8669,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T10:03:55+00:00"
+ "time": "2024-08-15T22:48:53+00:00"
},
{
"name": "symfony/css-selector",
@@ -8729,16 +8805,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.1.2",
+ "version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32"
+ "reference": "432bb369952795c61ca1def65e078c4a80dad13c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/2412d3dddb5c9ea51a39cfbff1c565fc9844ca32",
- "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/432bb369952795c61ca1def65e078c4a80dad13c",
+ "reference": "432bb369952795c61ca1def65e078c4a80dad13c",
"shasum": ""
},
"require": {
@@ -8784,7 +8860,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.1.2"
+ "source": "https://github.com/symfony/error-handler/tree/v7.1.3"
},
"funding": [
{
@@ -8800,7 +8876,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-25T19:55:06+00:00"
+ "time": "2024-07-26T13:02:51+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -8960,16 +9036,16 @@
},
{
"name": "symfony/finder",
- "version": "v7.1.1",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6"
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6",
- "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
"shasum": ""
},
"require": {
@@ -9004,7 +9080,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.1.1"
+ "source": "https://github.com/symfony/finder/tree/v7.1.4"
},
"funding": [
{
@@ -9020,20 +9096,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-08-13T14:28:19+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v7.1.1",
+ "version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa"
+ "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/74d171d5b6a1d9e4bfee09a41937c17a7536acfa",
- "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f602d5c17d1fa02f8019ace2687d9d136b7f4a1a",
+ "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a",
"shasum": ""
},
"require": {
@@ -9081,7 +9157,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.1.1"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.1.3"
},
"funding": [
{
@@ -9097,20 +9173,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-07-26T12:41:01+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.1.2",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6"
+ "reference": "6efcbd1b3f444f631c386504fc83eeca25963747"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6",
- "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6efcbd1b3f444f631c386504fc83eeca25963747",
+ "reference": "6efcbd1b3f444f631c386504fc83eeca25963747",
"shasum": ""
},
"require": {
@@ -9195,7 +9271,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.1.2"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.1.4"
},
"funding": [
{
@@ -9211,7 +9287,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T13:13:31+00:00"
+ "time": "2024-08-30T17:02:28+00:00"
},
{
"name": "symfony/mailer",
@@ -9295,16 +9371,16 @@
},
{
"name": "symfony/mime",
- "version": "v7.1.2",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc"
+ "reference": "ccaa6c2503db867f472a587291e764d6a1e58758"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
- "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758",
+ "reference": "ccaa6c2503db867f472a587291e764d6a1e58758",
"shasum": ""
},
"require": {
@@ -9359,7 +9435,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.1.2"
+ "source": "https://github.com/symfony/mime/tree/v7.1.4"
},
"funding": [
{
@@ -9375,7 +9451,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T10:03:55+00:00"
+ "time": "2024-08-13T14:28:19+00:00"
},
{
"name": "symfony/options-resolver",
@@ -9446,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": "*"
@@ -9505,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": [
{
@@ -9521,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": "*"
@@ -9585,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": [
{
@@ -9601,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"
@@ -9663,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": [
{
@@ -9679,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"
@@ -9747,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": [
{
@@ -9763,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"
@@ -9828,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": [
{
@@ -9844,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": "*"
@@ -9908,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": [
{
@@ -9924,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": {
@@ -10061,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": [
{
@@ -10077,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": {
@@ -10137,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": [
{
@@ -10153,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": "*"
@@ -10216,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": [
{
@@ -10232,20 +10234,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
- "version": "v7.1.1",
+ "version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "febf90124323a093c7ee06fdb30e765ca3c20028"
+ "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028",
- "reference": "febf90124323a093c7ee06fdb30e765ca3c20028",
+ "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca",
+ "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca",
"shasum": ""
},
"require": {
@@ -10277,7 +10279,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.1.1"
+ "source": "https://github.com/symfony/process/tree/v7.1.3"
},
"funding": [
{
@@ -10293,20 +10295,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-07-26T12:44:47+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
- "version": "v7.1.1",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/psr-http-message-bridge.git",
- "reference": "9a5dbb606da711f5d40a7596ad577856f9402140"
+ "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9a5dbb606da711f5d40a7596ad577856f9402140",
- "reference": "9a5dbb606da711f5d40a7596ad577856f9402140",
+ "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/405a7bcd872f1563966f64be19f1362d94ce71ab",
+ "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab",
"shasum": ""
},
"require": {
@@ -10360,7 +10362,7 @@
"psr-7"
],
"support": {
- "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.1"
+ "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.4"
},
"funding": [
{
@@ -10376,20 +10378,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-08-15T22:48:53+00:00"
},
{
"name": "symfony/routing",
- "version": "v7.1.1",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0"
+ "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/60c31bab5c45af7f13091b87deb708830f3c96c0",
- "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7",
+ "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7",
"shasum": ""
},
"require": {
@@ -10441,7 +10443,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.1.1"
+ "source": "https://github.com/symfony/routing/tree/v7.1.4"
},
"funding": [
{
@@ -10457,7 +10459,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-08-29T08:16:25+00:00"
},
{
"name": "symfony/service-contracts",
@@ -10606,16 +10608,16 @@
},
{
"name": "symfony/string",
- "version": "v7.1.2",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8"
+ "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8",
- "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8",
+ "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
+ "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
"shasum": ""
},
"require": {
@@ -10673,7 +10675,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.1.2"
+ "source": "https://github.com/symfony/string/tree/v7.1.4"
},
"funding": [
{
@@ -10689,20 +10691,20 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T09:27:18+00:00"
+ "time": "2024-08-12T09:59:40+00:00"
},
{
"name": "symfony/translation",
- "version": "v7.1.1",
+ "version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3"
+ "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
- "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/8d5e50c813ba2859a6dfc99a0765c550507934a1",
+ "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1",
"shasum": ""
},
"require": {
@@ -10767,7 +10769,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.1.1"
+ "source": "https://github.com/symfony/translation/tree/v7.1.3"
},
"funding": [
{
@@ -10783,7 +10785,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-07-26T12:41:01+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -10865,16 +10867,16 @@
},
{
"name": "symfony/uid",
- "version": "v7.1.1",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277"
+ "reference": "82177535395109075cdb45a70533aa3d7a521cdf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/bb59febeecc81528ff672fad5dab7f06db8c8277",
- "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/82177535395109075cdb45a70533aa3d7a521cdf",
+ "reference": "82177535395109075cdb45a70533aa3d7a521cdf",
"shasum": ""
},
"require": {
@@ -10919,7 +10921,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v7.1.1"
+ "source": "https://github.com/symfony/uid/tree/v7.1.4"
},
"funding": [
{
@@ -10935,20 +10937,20 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-08-12T09:59:40+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v7.1.2",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d"
+ "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5857c57c6b4b86524c08cf4f4bc95327270a816d",
- "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5fa7481b199090964d6fd5dab6294d5a870c7aa",
+ "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa",
"shasum": ""
},
"require": {
@@ -11002,7 +11004,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.1.2"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.1.4"
},
"funding": [
{
@@ -11018,20 +11020,20 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T08:00:31+00:00"
+ "time": "2024-08-30T16:12:47+00:00"
},
{
"name": "symfony/yaml",
- "version": "v6.4.8",
+ "version": "v6.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "52903de178d542850f6f341ba92995d3d63e60c9"
+ "reference": "be37e7f13195e05ab84ca5269365591edd240335"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9",
- "reference": "52903de178d542850f6f341ba92995d3d63e60c9",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/be37e7f13195e05ab84ca5269365591edd240335",
+ "reference": "be37e7f13195e05ab84ca5269365591edd240335",
"shasum": ""
},
"require": {
@@ -11074,7 +11076,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v6.4.8"
+ "source": "https://github.com/symfony/yaml/tree/v6.4.11"
},
"funding": [
{
@@ -11090,7 +11092,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:49:08+00:00"
+ "time": "2024-08-12T09:55:28+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -11206,23 +11208,23 @@
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.6.0",
+ "version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4"
+ "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
- "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
+ "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"shasum": ""
},
"require": {
"ext-pcre": "*",
- "graham-campbell/result-type": "^1.1.2",
+ "graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
- "phpoption/phpoption": "^1.9.2",
+ "phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
@@ -11239,7 +11241,7 @@
"extra": {
"bamarni-bin": {
"bin-links": true,
- "forward-command": true
+ "forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
@@ -11274,7 +11276,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0"
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
},
"funding": [
{
@@ -11286,7 +11288,7 @@
"type": "tidelift"
}
],
- "time": "2023-11-12T22:43:29+00:00"
+ "time": "2024-07-20T21:52:34+00:00"
},
{
"name": "voku/portable-ascii",
@@ -11532,16 +11534,16 @@
},
{
"name": "zbateson/mail-mime-parser",
- "version": "3.0.1",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mail-mime-parser.git",
- "reference": "6ade63b0a43047935791d7977e22717a68cc388b"
+ "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/6ade63b0a43047935791d7977e22717a68cc388b",
- "reference": "6ade63b0a43047935791d7977e22717a68cc388b",
+ "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/e0d4423fe27850c9dd301190767dbc421acc2f19",
+ "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19",
"shasum": ""
},
"require": {
@@ -11604,7 +11606,7 @@
"type": "github"
}
],
- "time": "2024-04-29T21:53:01+00:00"
+ "time": "2024-08-10T18:44:09+00:00"
},
{
"name": "zbateson/mb-wrapper",
@@ -11740,16 +11742,16 @@
},
{
"name": "zircote/swagger-php",
- "version": "4.10.3",
+ "version": "4.10.6",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
- "reference": "ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998"
+ "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zircote/swagger-php/zipball/ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998",
- "reference": "ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998",
+ "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e462ff5269ea0ec91070edd5d51dc7215bdea3b6",
+ "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6",
"shasum": ""
},
"require": {
@@ -11815,12 +11817,96 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
- "source": "https://github.com/zircote/swagger-php/tree/4.10.3"
+ "source": "https://github.com/zircote/swagger-php/tree/4.10.6"
},
- "time": "2024-07-04T07:53:11+00:00"
+ "time": "2024-07-26T03:04:43+00:00"
}
],
"packages-dev": [
+ {
+ "name": "barryvdh/laravel-debugbar",
+ "version": "v3.13.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/laravel-debugbar.git",
+ "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07",
+ "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/routing": "^9|^10|^11",
+ "illuminate/session": "^9|^10|^11",
+ "illuminate/support": "^9|^10|^11",
+ "maximebf/debugbar": "~1.22.0",
+ "php": "^8.0",
+ "symfony/finder": "^6|^7"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.3",
+ "orchestra/testbench-dusk": "^5|^6|^7|^8|^9",
+ "phpunit/phpunit": "^9.6|^10.5",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.13-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Barryvdh\\Debugbar\\ServiceProvider"
+ ],
+ "aliases": {
+ "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
+ }
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Barryvdh\\Debugbar\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "PHP Debugbar integration for Laravel",
+ "keywords": [
+ "debug",
+ "debugbar",
+ "laravel",
+ "profiler",
+ "webprofiler"
+ ],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+ "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2024-04-12T11:20:37+00:00"
+ },
{
"name": "brianium/paratest",
"version": "v7.4.3",
@@ -11980,16 +12066,16 @@
},
{
"name": "fidry/cpu-core-counter",
- "version": "1.1.0",
+ "version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
- "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42"
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42",
- "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f",
+ "reference": "8520451a140d3f46ac33042715115e290cf5785f",
"shasum": ""
},
"require": {
@@ -12029,7 +12115,7 @@
],
"support": {
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
- "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0"
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0"
},
"funding": [
{
@@ -12037,7 +12123,7 @@
"type": "github"
}
],
- "time": "2024-02-07T09:43:46+00:00"
+ "time": "2024-08-06T10:04:20+00:00"
},
{
"name": "filp/whoops",
@@ -12163,16 +12249,16 @@
},
{
"name": "laravel/dusk",
- "version": "v8.2.1",
+ "version": "v8.2.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
- "reference": "f2c0957aa4fbb4a78394e77b8caf969903f28050"
+ "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/dusk/zipball/f2c0957aa4fbb4a78394e77b8caf969903f28050",
- "reference": "f2c0957aa4fbb4a78394e77b8caf969903f28050",
+ "url": "https://api.github.com/repos/laravel/dusk/zipball/e641800393ce4ad39f0a47133f51aae67ceb01ad",
+ "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad",
"shasum": ""
},
"require": {
@@ -12229,22 +12315,22 @@
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
- "source": "https://github.com/laravel/dusk/tree/v8.2.1"
+ "source": "https://github.com/laravel/dusk/tree/v8.2.5"
},
- "time": "2024-07-08T06:42:12+00:00"
+ "time": "2024-08-26T12:34:33+00:00"
},
{
"name": "laravel/pint",
- "version": "v1.16.2",
+ "version": "v1.17.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca"
+ "reference": "9d77be916e145864f10788bb94531d03e1f7b482"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/51f1ba679a6afe0315621ad143d788bd7ded0eca",
- "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482",
+ "reference": "9d77be916e145864f10788bb94531d03e1f7b482",
"shasum": ""
},
"require": {
@@ -12255,13 +12341,13 @@
"php": "^8.1.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.59.3",
- "illuminate/view": "^10.48.12",
- "larastan/larastan": "^2.9.7",
+ "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.34.8"
+ "pestphp/pest": "^2.35.1"
},
"bin": [
"builds/pint"
@@ -12297,7 +12383,75 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2024-07-09T15:58:08+00:00"
+ "time": "2024-09-03T15:00:28+00:00"
+ },
+ {
+ "name": "maximebf/debugbar",
+ "version": "v1.22.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maximebf/php-debugbar.git",
+ "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
+ "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8",
+ "psr/log": "^1|^2|^3",
+ "symfony/var-dumper": "^4|^5|^6|^7"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1",
+ "phpunit/phpunit": "^8|^9",
+ "symfony/panther": "^1|^2.1",
+ "twig/twig": "^1.38|^2.7|^3.0"
+ },
+ "suggest": {
+ "kriswallsmith/assetic": "The best way to manage assets",
+ "monolog/monolog": "Log using Monolog",
+ "predis/predis": "Redis storage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.22-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugBar\\": "src/DebugBar/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maxime Bouroumeau-Fuseau",
+ "email": "maxime.bouroumeau@gmail.com",
+ "homepage": "http://maximebf.com"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Debug bar in the browser for php application",
+ "homepage": "https://github.com/maximebf/php-debugbar",
+ "keywords": [
+ "debug",
+ "debugbar"
+ ],
+ "support": {
+ "issues": "https://github.com/maximebf/php-debugbar/issues",
+ "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5"
+ },
+ "time": "2024-09-09T08:05:55+00:00"
},
{
"name": "mockery/mockery",
@@ -12444,38 +12598,38 @@
},
{
"name": "nunomaduro/collision",
- "version": "v8.1.1",
+ "version": "v8.4.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "13e5d538b95a744d85f447a321ce10adb28e9af9"
+ "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/13e5d538b95a744d85f447a321ce10adb28e9af9",
- "reference": "13e5d538b95a744d85f447a321ce10adb28e9af9",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/e7d1aa8ed753f63fa816932bbc89678238843b4a",
+ "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a",
"shasum": ""
},
"require": {
"filp/whoops": "^2.15.4",
"nunomaduro/termwind": "^2.0.1",
"php": "^8.2.0",
- "symfony/console": "^7.0.4"
+ "symfony/console": "^7.1.3"
},
"conflict": {
"laravel/framework": "<11.0.0 || >=12.0.0",
"phpunit/phpunit": "<10.5.1 || >=12.0.0"
},
"require-dev": {
- "larastan/larastan": "^2.9.2",
- "laravel/framework": "^11.0.0",
- "laravel/pint": "^1.14.0",
- "laravel/sail": "^1.28.2",
- "laravel/sanctum": "^4.0.0",
+ "larastan/larastan": "^2.9.8",
+ "laravel/framework": "^11.19.0",
+ "laravel/pint": "^1.17.1",
+ "laravel/sail": "^1.31.0",
+ "laravel/sanctum": "^4.0.2",
"laravel/tinker": "^2.9.0",
- "orchestra/testbench-core": "^9.0.0",
- "pestphp/pest": "^2.34.1 || ^3.0.0",
- "sebastian/environment": "^6.0.1 || ^7.0.0"
+ "orchestra/testbench-core": "^9.2.3",
+ "pestphp/pest": "^2.35.0 || ^3.0.0",
+ "sebastian/environment": "^6.1.0 || ^7.0.0"
},
"type": "library",
"extra": {
@@ -12537,25 +12691,25 @@
"type": "patreon"
}
],
- "time": "2024-03-06T16:20:09+00:00"
+ "time": "2024-08-03T15:32:23+00:00"
},
{
"name": "pestphp/pest",
- "version": "v2.34.8",
+ "version": "v2.35.1",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57"
+ "reference": "b13acb630df52c06123588d321823c31fc685545"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/e8f122bf47585c06431e0056189ec6bfd6f41f57",
- "reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/b13acb630df52c06123588d321823c31fc685545",
+ "reference": "b13acb630df52c06123588d321823c31fc685545",
"shasum": ""
},
"require": {
"brianium/paratest": "^7.3.1",
- "nunomaduro/collision": "^7.10.0|^8.1.1",
+ "nunomaduro/collision": "^7.10.0|^8.4.0",
"nunomaduro/termwind": "^1.15.1|^2.0.1",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.7.0",
@@ -12569,8 +12723,8 @@
},
"require-dev": {
"pestphp/pest-dev-tools": "^2.16.0",
- "pestphp/pest-plugin-type-coverage": "^2.8.3",
- "symfony/process": "^6.4.0|^7.1.1"
+ "pestphp/pest-plugin-type-coverage": "^2.8.5",
+ "symfony/process": "^6.4.0|^7.1.3"
},
"bin": [
"bin/pest"
@@ -12633,7 +12787,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v2.34.8"
+ "source": "https://github.com/pestphp/pest/tree/v2.35.1"
},
"funding": [
{
@@ -12645,7 +12799,7 @@
"type": "github"
}
],
- "time": "2024-06-10T22:02:16+00:00"
+ "time": "2024-08-20T21:41:50+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -13038,32 +13192,32 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "10.1.15",
+ "version": "10.1.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae"
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae",
- "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^4.18 || ^5.0",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
"php": ">=8.1",
- "phpunit/php-file-iterator": "^4.0",
- "phpunit/php-text-template": "^3.0",
- "sebastian/code-unit-reverse-lookup": "^3.0",
- "sebastian/complexity": "^3.0",
- "sebastian/environment": "^6.0",
- "sebastian/lines-of-code": "^2.0",
- "sebastian/version": "^4.0",
- "theseer/tokenizer": "^1.2.0"
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "sebastian/code-unit-reverse-lookup": "^3.0.0",
+ "sebastian/complexity": "^3.2.0",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/lines-of-code": "^2.0.2",
+ "sebastian/version": "^4.0.1",
+ "theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^10.1"
@@ -13075,7 +13229,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "10.1-dev"
+ "dev-main": "10.1.x-dev"
}
},
"autoload": {
@@ -13104,7 +13258,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
},
"funding": [
{
@@ -13112,7 +13266,7 @@
"type": "github"
}
],
- "time": "2024-06-29T08:25:15+00:00"
+ "time": "2024-08-22T04:31:57+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -13628,16 +13782,16 @@
},
{
"name": "sebastian/comparator",
- "version": "5.0.1",
+ "version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "2db5010a484d53ebf536087a70b4a5423c102372"
+ "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372",
- "reference": "2db5010a484d53ebf536087a70b4a5423c102372",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53",
+ "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53",
"shasum": ""
},
"require": {
@@ -13648,7 +13802,7 @@
"sebastian/exporter": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.3"
+ "phpunit/phpunit": "^10.4"
},
"type": "library",
"extra": {
@@ -13693,7 +13847,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.2"
},
"funding": [
{
@@ -13701,7 +13855,7 @@
"type": "github"
}
],
- "time": "2023-08-14T13:18:12+00:00"
+ "time": "2024-08-12T06:03:08+00:00"
},
{
"name": "sebastian/complexity",
@@ -14421,16 +14575,16 @@
},
{
"name": "spatie/error-solutions",
- "version": "1.0.5",
+ "version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/error-solutions.git",
- "reference": "4bb6c734dc992b2db3e26df1ef021c75d2218b13"
+ "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/error-solutions/zipball/4bb6c734dc992b2db3e26df1ef021c75d2218b13",
- "reference": "4bb6c734dc992b2db3e26df1ef021c75d2218b13",
+ "url": "https://api.github.com/repos/spatie/error-solutions/zipball/ae7393122eda72eed7cc4f176d1e96ea444f2d67",
+ "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67",
"shasum": ""
},
"require": {
@@ -14483,7 +14637,7 @@
],
"support": {
"issues": "https://github.com/spatie/error-solutions/issues",
- "source": "https://github.com/spatie/error-solutions/tree/1.0.5"
+ "source": "https://github.com/spatie/error-solutions/tree/1.1.1"
},
"funding": [
{
@@ -14491,20 +14645,20 @@
"type": "github"
}
],
- "time": "2024-07-09T12:13:32+00:00"
+ "time": "2024-07-25T11:06:04+00:00"
},
{
"name": "spatie/flare-client-php",
- "version": "1.7.0",
+ "version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
- "reference": "097040ff51e660e0f6fc863684ac4b02c93fa234"
+ "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/097040ff51e660e0f6fc863684ac4b02c93fa234",
- "reference": "097040ff51e660e0f6fc863684ac4b02c93fa234",
+ "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
+ "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
"shasum": ""
},
"require": {
@@ -14522,7 +14676,7 @@
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
- "spatie/phpunit-snapshot-assertions": "^4.0|^5.0"
+ "spatie/pest-plugin-snapshots": "^1.0|^2.0"
},
"type": "library",
"extra": {
@@ -14552,7 +14706,7 @@
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
- "source": "https://github.com/spatie/flare-client-php/tree/1.7.0"
+ "source": "https://github.com/spatie/flare-client-php/tree/1.8.0"
},
"funding": [
{
@@ -14560,7 +14714,7 @@
"type": "github"
}
],
- "time": "2024-06-12T14:39:14+00:00"
+ "time": "2024-08-01T08:27:26+00:00"
},
{
"name": "spatie/ignition",
@@ -14738,16 +14892,16 @@
},
{
"name": "symfony/http-client",
- "version": "v6.4.9",
+ "version": "v6.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045"
+ "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/6e9db0025db565bcf8f1d46ed734b549e51e6045",
- "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65",
+ "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65",
"shasum": ""
},
"require": {
@@ -14811,7 +14965,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.9"
+ "source": "https://github.com/symfony/http-client/tree/v6.4.11"
},
"funding": [
{
@@ -14827,7 +14981,7 @@
"type": "tidelift"
}
],
- "time": "2024-06-28T07:59:05+00:00"
+ "time": "2024-08-26T06:30:21+00:00"
},
{
"name": "symfony/http-client-contracts",
diff --git a/config/app.php b/config/app.php
index 41e94c09b..34484fe41 100644
--- a/config/app.php
+++ b/config/app.php
@@ -199,6 +199,7 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
+ App\Providers\TelescopeServiceProvider::class,
],
diff --git a/config/constants.php b/config/constants.php
index 861b645ed..5792b358c 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -6,7 +6,8 @@ return [
'contact' => 'https://coolify.io/docs/contact',
],
'ssh' => [
- 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1m'),
+ 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
+ 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),
'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/database.php b/config/database.php
index 248c6150a..f48a68082 100644
--- a/config/database.php
+++ b/config/database.php
@@ -35,34 +35,6 @@ return [
'connections' => [
- 'sqlite' => [
- 'driver' => 'sqlite',
- 'url' => env('DATABASE_URL'),
- 'database' => env('DB_DATABASE', database_path('database.sqlite')),
- 'prefix' => '',
- 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
- ],
-
- 'mysql' => [
- 'driver' => 'mysql',
- 'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', '127.0.0.1'),
- 'port' => env('DB_PORT', '3306'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'unix_socket' => env('DB_SOCKET', ''),
- 'charset' => 'utf8mb4',
- 'collation' => 'utf8mb4_unicode_ci',
- 'prefix' => '',
- 'prefix_indexes' => true,
- 'strict' => true,
- 'engine' => null,
- 'options' => extension_loaded('pdo_mysql') ? array_filter([
- PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
- ]) : [],
- ],
-
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
@@ -77,22 +49,6 @@ return [
'search_path' => 'public',
'sslmode' => 'prefer',
],
-
- 'sqlsrv' => [
- 'driver' => 'sqlsrv',
- 'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', 'localhost'),
- 'port' => env('DB_PORT', '1433'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- 'prefix_indexes' => true,
- // 'encrypt' => env('DB_ENCRYPT', 'yes'),
- // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
- ],
-
],
/*
diff --git a/config/debugbar.php b/config/debugbar.php
new file mode 100644
index 000000000..eae406ba7
--- /dev/null
+++ b/config/debugbar.php
@@ -0,0 +1,325 @@
+ env('DEBUGBAR_ENABLED', null),
+ 'except' => [
+ 'telescope*',
+ 'horizon*',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Storage settings
+ |--------------------------------------------------------------------------
+ |
+ | DebugBar stores data for session/ajax requests.
+ | You can disable this, so the debugbar stores data in headers/session,
+ | but this can cause problems with large data collectors.
+ | By default, file storage (in the storage folder) is used. Redis and PDO
+ | can also be used. For PDO, run the package migrations first.
+ |
+ | Warning: Enabling storage.open will allow everyone to access previous
+ | request, do not enable open storage in publicly available environments!
+ | Specify a callback if you want to limit based on IP or authentication.
+ | Leaving it to null will allow localhost only.
+ */
+ 'storage' => [
+ 'enabled' => true,
+ 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback.
+ 'driver' => 'file', // redis, file, pdo, socket, custom
+ 'path' => storage_path('debugbar'), // For file driver
+ 'connection' => null, // Leave null for default connection (Redis/PDO)
+ 'provider' => '', // Instance of StorageInterface for custom driver
+ 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver
+ 'port' => 2304, // Port to use with the "socket" driver
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Editor
+ |--------------------------------------------------------------------------
+ |
+ | Choose your preferred editor to use when clicking file name.
+ |
+ | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote",
+ | "vscode-insiders-remote", "vscodium", "textmate", "emacs",
+ | "sublime", "atom", "nova", "macvim", "idea", "netbeans",
+ | "xdebug", "espresso"
+ |
+ */
+
+ 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Remote Path Mapping
+ |--------------------------------------------------------------------------
+ |
+ | If you are using a remote dev server, like Laravel Homestead, Docker, or
+ | even a remote VPS, it will be necessary to specify your path mapping.
+ |
+ | Leaving one, or both of these, empty or null will not trigger the remote
+ | URL changes and Debugbar will treat your editor links as local files.
+ |
+ | "remote_sites_path" is an absolute base path for your sites or projects
+ | in Homestead, Vagrant, Docker, or another remote development server.
+ |
+ | Example value: "/home/vagrant/Code"
+ |
+ | "local_sites_path" is an absolute base path for your sites or projects
+ | on your local computer where your IDE or code editor is running on.
+ |
+ | Example values: "/Users//Code", "C:\Users\\Documents\Code"
+ |
+ */
+
+ 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'),
+ 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Vendors
+ |--------------------------------------------------------------------------
+ |
+ | Vendor files are included by default, but can be set to false.
+ | This can also be set to 'js' or 'css', to only include javascript or css vendor files.
+ | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
+ | and for js: jquery and highlight.js
+ | So if you want syntax highlighting, set it to true.
+ | jQuery is set to not conflict with existing jQuery scripts.
+ |
+ */
+
+ 'include_vendors' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Capture Ajax Requests
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
+ | you can use this option to disable sending the data through the headers.
+ |
+ | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
+ |
+ | Note for your request to be identified as ajax requests they must either send the header
+ | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header.
+ |
+ | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar.
+ | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading.
+ */
+
+ 'capture_ajax' => true,
+ 'add_ajax_timing' => false,
+ 'ajax_handler_auto_show' => true,
+ 'ajax_handler_enable_tab' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Error Handler for Deprecated warnings
+ |--------------------------------------------------------------------------
+ |
+ | When enabled, the Debugbar shows deprecated warnings for Symfony components
+ | in the Messages tab.
+ |
+ */
+ 'error_handler' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Clockwork integration
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can emulate the Clockwork headers, so you can use the Chrome
+ | Extension, without the server-side code. It uses Debugbar collectors instead.
+ |
+ */
+ 'clockwork' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | DataCollectors
+ |--------------------------------------------------------------------------
+ |
+ | Enable/disable DataCollectors
+ |
+ */
+
+ 'collectors' => [
+ 'phpinfo' => true, // Php version
+ 'messages' => true, // Messages
+ 'time' => true, // Time Datalogger
+ 'memory' => true, // Memory usage
+ 'exceptions' => true, // Exception displayer
+ 'log' => true, // Logs from Monolog (merged in messages if enabled)
+ 'db' => true, // Show database (PDO) queries and bindings
+ 'views' => true, // Views with their data
+ 'route' => true, // Current route information
+ 'auth' => false, // Display Laravel authentication status
+ 'gate' => true, // Display Laravel Gate checks
+ 'session' => true, // Display session data
+ 'symfony_request' => true, // Only one can be enabled..
+ 'mail' => true, // Catch mail messages
+ 'laravel' => false, // Laravel version and environment
+ 'events' => false, // All events fired
+ 'default_request' => false, // Regular or special Symfony request logger
+ 'logs' => false, // Add the latest log messages
+ 'files' => false, // Show the included files
+ 'config' => false, // Display config settings
+ 'cache' => false, // Display cache events
+ 'models' => true, // Display models
+ 'livewire' => true, // Display Livewire (when available)
+ 'jobs' => false, // Display dispatched jobs
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Extra options
+ |--------------------------------------------------------------------------
+ |
+ | Configure some DataCollectors
+ |
+ */
+
+ 'options' => [
+ 'time' => [
+ 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate
+ ],
+ 'messages' => [
+ 'trace' => true, // Trace the origin of the debug message
+ ],
+ 'memory' => [
+ 'reset_peak' => false, // run memory_reset_peak_usage before collecting
+ 'with_baseline' => false, // Set boot memory usage as memory peak baseline
+ 'precision' => 0, // Memory rounding precision
+ ],
+ 'auth' => [
+ 'show_name' => true, // Also show the users name/email in the debugbar
+ 'show_guards' => true, // Show the guards that are used
+ ],
+ 'db' => [
+ 'with_params' => true, // Render SQL with the parameters substituted
+ 'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
+ 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults)
+ 'timeline' => false, // Add the queries to the timeline
+ 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute.
+ 'explain' => [ // Show EXPLAIN output on queries
+ 'enabled' => false,
+ 'types' => ['SELECT'], // Deprecated setting, is always only SELECT
+ ],
+ 'hints' => false, // Show hints for common mistakes
+ 'show_copy' => false, // Show copy button next to the query,
+ 'slow_threshold' => false, // Only track queries that last longer than this time in ms
+ 'memory_usage' => false, // Show queries memory usage
+ 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured
+ 'hard_limit' => 500, // After the hard limit, queries are ignored
+ ],
+ 'mail' => [
+ 'timeline' => false, // Add mails to the timeline
+ 'show_body' => true,
+ ],
+ 'views' => [
+ 'timeline' => false, // Add the views to the timeline (Experimental)
+ 'data' => false, //true for all data, 'keys' for only names, false for no parameters.
+ 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force
+ 'exclude_paths' => [ // Add the paths which you don't want to appear in the views
+ 'vendor/filament', // Exclude Filament components by default
+ ],
+ ],
+ 'route' => [
+ 'label' => true, // show complete route on bar
+ ],
+ 'session' => [
+ 'hiddens' => [], // hides sensitive values using array paths
+ ],
+ 'symfony_request' => [
+ 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password
+ ],
+ 'events' => [
+ 'data' => false, // collect events data, listeners
+ ],
+ 'logs' => [
+ 'file' => null,
+ ],
+ 'cache' => [
+ 'values' => true, // collect cache values
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Inject Debugbar in Response
+ |--------------------------------------------------------------------------
+ |
+ | Usually, the debugbar is added just before