diff --git a/.dockerignore b/.dockerignore
index d6abd1451..0adca0b32 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,3 +22,6 @@ yarn-error.log
/_data
.rnd
/.ssh
+.ignition.json
+.env.dusk.local
+docker/coolify-realtime/node_modules
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.dusk.ci b/.env.dusk.ci
new file mode 100644
index 000000000..9660de7b4
--- /dev/null
+++ b/.env.dusk.ci
@@ -0,0 +1,15 @@
+APP_ENV=production
+APP_NAME="Coolify Staging"
+APP_ID=development
+APP_KEY=
+APP_URL=http://localhost
+APP_PORT=8000
+SSH_MUX_ENABLED=true
+
+# PostgreSQL Database Configuration
+DB_DATABASE=coolify
+DB_USERNAME=coolify
+DB_PASSWORD=password
+DB_HOST=localhost
+DB_PORT=5432
+
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/.env.windows-docker-desktop.example b/.env.windows-docker-desktop.example
index 02a5a4174..b067b4c5c 100644
--- a/.env.windows-docker-desktop.example
+++ b/.env.windows-docker-desktop.example
@@ -4,6 +4,7 @@ APP_ID=coolify-windows-docker-desktop
APP_NAME=Coolify
APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80=
+DB_USERNAME=coolify
DB_PASSWORD=coolify
REDIS_PASSWORD=coolify
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 5ee5c3970..000000000
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
+++ /dev/null
@@ -1,37 +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
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 2d354057e..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/feature-requests-ideas
- 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/browser-tests.yml b/.github/workflows/browser-tests.yml
new file mode 100644
index 000000000..b06c9e97c
--- /dev/null
+++ b/.github/workflows/browser-tests.yml
@@ -0,0 +1,65 @@
+name: Dusk
+on:
+ push:
+ branches: [ "not-existing" ]
+jobs:
+ dusk:
+ runs-on: ubuntu-latest
+
+ services:
+ redis:
+ image: redis
+ env:
+ REDIS_HOST: localhost
+ REDIS_PORT: 6379
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up PostgreSQL
+ run: |
+ sudo systemctl start postgresql
+ sudo -u postgres psql -c "CREATE DATABASE coolify;"
+ sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';"
+ sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';"
+ sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';"
+ sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';"
+ sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;"
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ - name: Copy .env
+ run: cp .env.dusk.ci .env
+ - name: Install Dependencies
+ run: composer install --no-progress --prefer-dist --optimize-autoloader
+ - name: Generate key
+ run: php artisan key:generate
+ - name: Install Chrome binaries
+ run: php artisan dusk:chrome-driver --detect
+ - name: Start Chrome Driver
+ run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 &
+ - name: Build assets
+ run: npm install && npm run build
+ - name: Run Laravel Server
+ run: php artisan serve --no-reload &
+ - name: Execute tests
+ run: php artisan dusk
+ - name: Upload Screenshots
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: screenshots
+ path: tests/Browser/screenshots
+ - name: Upload Console Logs
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: console
+ path: tests/Browser/console
diff --git a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml
new file mode 100644
index 000000000..d00853964
--- /dev/null
+++ b/.github/workflows/chore-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/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml
new file mode 100644
index 000000000..2afc996cb
--- /dev/null
+++ b/.github/workflows/chore-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/chore-remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml
new file mode 100644
index 000000000..ea097e328
--- /dev/null
+++ b/.github/workflows/chore-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/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml
index d9921b363..4354294b1 100644
--- a/.github/workflows/coolify-helper-next.yml
+++ b/.github/workflows/coolify-helper-next.yml
@@ -1,4 +1,4 @@
-name: Coolify Helper Image Development (v4)
+name: Coolify Helper Image Development
on:
push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
@@ -19,21 +20,38 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ labels: |
+ coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -41,21 +59,39 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ labels: |
+ coolify.managed=true
+
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -63,22 +99,44 @@ jobs:
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: actions/checkout@v4
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ 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 on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.DOCKER_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..6d852a2b3 100644
--- a/.github/workflows/coolify-helper.yml
+++ b/.github/workflows/coolify-helper.yml
@@ -1,4 +1,4 @@
-name: Coolify Helper Image (v4)
+name: Coolify Helper Image
on:
push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
@@ -19,21 +20,38 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ labels: |
+ coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -41,21 +59,38 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ labels: |
+ coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -63,22 +98,45 @@ jobs:
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: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ 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 on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_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-production-build.yml b/.github/workflows/coolify-production-build.yml
new file mode 100644
index 000000000..79dfc3fc0
--- /dev/null
+++ b/.github/workflows/coolify-production-build.yml
@@ -0,0 +1,139 @@
+name: Production Build (v4)
+
+on:
+ push:
+ branches: ["main"]
+ paths-ignore:
+ - .github/workflows/coolify-helper.yml
+ - .github/workflows/coolify-helper-next.yml
+ - .github/workflows/coolify-realtime.yml
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-helper/Dockerfile
+ - docker/coolify-realtime/Dockerfile
+ - docker/testing-host/Dockerfile
+ - templates/*
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
+ IMAGE_NAME: "coollabsio/coolify"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [amd64, aarch64]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_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..ef247170f
--- /dev/null
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -0,0 +1,147 @@
+name: Coolify Realtime Development
+
+on:
+ push:
+ branches: [ "next" ]
+ paths:
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-realtime/Dockerfile
+ - docker/coolify-realtime/terminal-server.js
+ - docker/coolify-realtime/package.json
+ - docker/coolify-realtime/package-lock.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.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 ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ ${{ env.GITHUB_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 ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ ${{ env.GITHUB_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:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.DOCKER_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-realtime.yml b/.github/workflows/coolify-realtime.yml
new file mode 100644
index 000000000..9654a21b0
--- /dev/null
+++ b/.github/workflows/coolify-realtime.yml
@@ -0,0 +1,147 @@
+name: Coolify Realtime
+
+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/package-lock.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.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 ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_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 ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_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:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_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-staging-build.yml b/.github/workflows/coolify-staging-build.yml
new file mode 100644
index 000000000..1aafc2f0b
--- /dev/null
+++ b/.github/workflows/coolify-staging-build.yml
@@ -0,0 +1,125 @@
+name: Staging Build
+
+on:
+ push:
+ branches-ignore: ["main", "v3"]
+ paths-ignore:
+ - .github/workflows/coolify-helper.yml
+ - .github/workflows/coolify-helper-next.yml
+ - .github/workflows/coolify-realtime.yml
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-helper/Dockerfile
+ - docker/coolify-realtime/Dockerfile
+ - docker/testing-host/Dockerfile
+ - templates/*
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
+ IMAGE_NAME: "coollabsio/coolify"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
+
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [amd64, aarch64]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml
index 5fdc32991..95a228114 100644
--- a/.github/workflows/coolify-testing-host.yml
+++ b/.github/workflows/coolify-testing-host.yml
@@ -1,14 +1,15 @@
-name: Coolify Testing Host (v4-non-prod)
+name: Coolify Testing Host
on:
push:
- branches: [ "main", "next" ]
+ branches: [ "next" ]
paths:
- .github/workflows/coolify-testing-host.yml
- docker/testing-host/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-testing-host"
jobs:
@@ -19,21 +20,34 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ labels: |
+ coolify.managed=true
+
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -41,21 +55,34 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ labels: |
+ coolify.managed=true
+
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -63,21 +90,36 @@ jobs:
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: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
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.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
- uses: sarisia/actions-status-discord@v1
if: always()
with:
diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml
deleted file mode 100644
index 268b885ac..000000000
--- a/.github/workflows/development-build.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Development Build (v4)
-
-on:
- push:
- 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.ref_name }}
- 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.ref_name }}-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.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- - uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_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.yml b/.github/workflows/fix-php-code-style-issues.yml
deleted file mode 100644
index aebce91bc..000000000
--- a/.github/workflows/fix-php-code-style-issues.yml
+++ /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/production-build.yml b/.github/workflows/production-build.yml
deleted file mode 100644
index e4bad6a65..000000000
--- a/.github/workflows/production-build.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-name: Production Build (v4)
-
-on:
- push:
- branches: ["main"]
- paths-ignore:
- - templates/service-templates.json
-
-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: Get Version
- id: version
- run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- - 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 }}:${{ steps.version.outputs.VERSION }}
- aarch64:
- runs-on: [self-hosted, arm64]
- 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 php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- - 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 }}:${{ steps.version.outputs.VERSION }}-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: Get Version
- id: version
- run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|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/.gitignore b/.gitignore
index ac8a1e090..dd6b141b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,6 @@ _ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*
+.ignition.json
+.env.dusk.local
+docker/coolify-realtime/node_modules
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 56bee004e..14a741088 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,19 @@
+
+
[](https://console.algora.io/org/coollabsio/bounties/new)
-[](https://console.algora.io/org/coollabsio/bounties?status=open)
-[](https://console.algora.io/org/coollabsio/bounties?status=completed)
+
# About the Project
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
-It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
+It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else.
-Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**.
+Imagine having the ease of a cloud but with your own servers. That is **Coolify**.
-No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
+No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️
-For more information, take a look at our landing page [here](https://coolify.io).
+For more information, take a look at our landing page at [coolify.io](https://coolify.io).
# Installation
@@ -22,36 +24,56 @@ You can find the installation script source [here](./scripts/install.sh).
# Support
-Contact us [here](https://coolify.io/docs/contact).
+Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
# Donations
-To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
+To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development.
-https://coolify.io/sponsorships
+[coolify.io/sponsorships](https://coolify.io/sponsorships)
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+)
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -61,8 +83,11 @@ Special thanks to our biggest sponsors!
+
+
+
## Organizations
@@ -83,9 +108,9 @@ Special thanks to our biggest sponsors!
# Cloud
-If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
+If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
-For more information & pricing, take a look at our landing page [here](https://coolify.io).
+For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io).
## Why should I use the Cloud version?
The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month.
@@ -109,7 +134,7 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
-
+
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/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php
new file mode 100644
index 000000000..991146b48
--- /dev/null
+++ b/app/Actions/Application/GenerateConfig.php
@@ -0,0 +1,16 @@
+generateConfig(is_json: $is_json);
+ }
+}
diff --git a/app/Actions/Application/IsHorizonQueueEmpty.php b/app/Actions/Application/IsHorizonQueueEmpty.php
new file mode 100644
index 000000000..17966b8a0
--- /dev/null
+++ b/app/Actions/Application/IsHorizonQueueEmpty.php
@@ -0,0 +1,37 @@
+getRecent();
+ if ($recent) {
+ $running = $recent->filter(function ($job) use ($hostname) {
+ $payload = json_decode($job->payload);
+ $tags = data_get($payload, 'tags');
+
+ return $job->status != 'completed' &&
+ $job->status != 'failed' &&
+ isset($tags) &&
+ is_array($tags) &&
+ in_array('server:'.$hostname, $tags);
+ });
+ if ($running->count() > 0) {
+ echo 'false';
+
+ return false;
+ }
+ }
+ echo 'true';
+
+ return true;
+ }
+}
diff --git a/app/Actions/Application/LoadComposeFile.php b/app/Actions/Application/LoadComposeFile.php
new file mode 100644
index 000000000..838b541e2
--- /dev/null
+++ b/app/Actions/Application/LoadComposeFile.php
@@ -0,0 +1,16 @@
+loadComposeFile();
+ }
+}
diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 446659e5b..cab7e45f0 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,35 +10,32 @@ class StopApplication
{
use AsAction;
- public function handle(Application $application)
+ 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';
}
- $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerName = data_get($container, 'Names');
- if ($containerName) {
- instant_remote_process(
- ["docker rm -f {$containerName}"],
- $server
- );
- }
- }
+
+ 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') {
+ $application->delete_connected_networks($application->uuid);
+ }
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ } catch (\Exception $e) {
+ return $e->getMessage();
}
}
}
diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php
index da8c700fe..b13b10efd 100644
--- a/app/Actions/Application/StopApplicationOneServer.php
+++ b/app/Actions/Application/StopApplicationOneServer.php
@@ -32,8 +32,6 @@ class StopApplicationOneServer
}
}
} catch (\Exception $e) {
- ray($e->getMessage());
-
return $e->getMessage();
}
}
diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
index d4cdf64e2..6676b7937 100644
--- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php
+++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
@@ -3,6 +3,7 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
+use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -40,8 +41,17 @@ class PrepareCoolifyTask
public function __invoke(): Activity
{
- $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data);
- dispatch($job);
+ $job = new CoolifyTask(
+ activity: $this->activity,
+ ignore_errors: $this->remoteProcessArgs->ignore_errors,
+ call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
+ call_event_data: $this->remoteProcessArgs->call_event_data,
+ );
+ if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
+ dispatch($job)->onQueue('high');
+ } else {
+ dispatch($job);
+ }
$this->activity->refresh();
return $this->activity;
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index be986a76f..981b81378 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -4,10 +4,12 @@ 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;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
@@ -38,8 +40,7 @@ class RunRemoteProcess
*/
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null)
{
-
- if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) {
+ if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) {
throw new \RuntimeException('Incompatible Activity to run a remote command.');
}
@@ -124,7 +125,7 @@ class RunRemoteProcess
]));
}
} catch (\Throwable $e) {
- ray($e);
+ Log::error('Error calling event: '.$e->getMessage());
}
}
@@ -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/RestartDatabase.php b/app/Actions/Database/RestartDatabase.php
new file mode 100644
index 000000000..0400d924d
--- /dev/null
+++ b/app/Actions/Database/RestartDatabase.php
@@ -0,0 +1,29 @@
+destination->server;
+ if (! $server->isFunctional()) {
+ return 'Server is not functional';
+ }
+ StopDatabase::run($database);
+
+ return StartDatabase::run($database);
+ }
+}
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index d9518cd80..13667e829 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -52,6 +51,8 @@ class StartClickhouse
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
@@ -80,14 +81,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;
@@ -103,6 +97,11 @@ class StartClickhouse
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -155,14 +154,16 @@ class StartClickhouse
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
$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/StartDatabase.php b/app/Actions/Database/StartDatabase.php
new file mode 100644
index 000000000..869a88521
--- /dev/null
+++ b/app/Actions/Database/StartDatabase.php
@@ -0,0 +1,57 @@
+destination->server;
+ if (! $server->isFunctional()) {
+ return 'Server is not functional';
+ }
+ switch ($database->getMorphClass()) {
+ case \App\Models\StandalonePostgresql::class:
+ $activity = StartPostgresql::run($database);
+ break;
+ case \App\Models\StandaloneRedis::class:
+ $activity = StartRedis::run($database);
+ break;
+ case \App\Models\StandaloneMongodb::class:
+ $activity = StartMongodb::run($database);
+ break;
+ case \App\Models\StandaloneMysql::class:
+ $activity = StartMysql::run($database);
+ break;
+ case \App\Models\StandaloneMariadb::class:
+ $activity = StartMariadb::run($database);
+ break;
+ case \App\Models\StandaloneKeydb::class:
+ $activity = StartKeydb::run($database);
+ break;
+ case \App\Models\StandaloneDragonfly::class:
+ $activity = StartDragonfly::run($database);
+ break;
+ case \App\Models\StandaloneClickhouse::class:
+ $activity = StartClickhouse::run($database);
+ break;
+ }
+ if ($database->is_public && $database->public_port) {
+ StartDatabaseProxy::dispatch($database)->onQueue('high');
+ }
+
+ return $activity;
+ }
+}
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index a514c51b4..d7a3bc697 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -26,7 +26,7 @@ class StartDatabaseProxy
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
- if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$databaseType = $database->databaseType();
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
$network = $database->service->uuid;
@@ -34,54 +34,54 @@ class StartDatabaseProxy
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
- $type = 'App\Models\StandaloneMariadb';
+ $type = \App\Models\StandaloneMariadb::class;
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
- $type = 'App\Models\StandaloneMongodb';
+ $type = \App\Models\StandaloneMongodb::class;
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
- $type = 'App\Models\StandaloneMysql';
+ $type = \App\Models\StandaloneMysql::class;
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
- $type = 'App\Models\StandalonePostgresql';
+ $type = \App\Models\StandalonePostgresql::class;
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
- $type = 'App\Models\StandaloneRedis';
+ $type = \App\Models\StandaloneRedis::class;
$containerName = "redis-{$database->service->uuid}";
break;
case 'standalone-keydb':
- $type = 'App\Models\StandaloneKeydb';
+ $type = \App\Models\StandaloneKeydb::class;
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
- $type = 'App\Models\StandaloneDragonfly';
+ $type = \App\Models\StandaloneDragonfly::class;
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
- $type = 'App\Models\StandaloneClickhouse';
+ $type = \App\Models\StandaloneClickhouse::class;
$containerName = "clickhouse-{$database->service->uuid}";
break;
}
}
- if ($type === 'App\Models\StandaloneRedis') {
+ if ($type === \App\Models\StandaloneRedis::class) {
$internalPort = 6379;
- } elseif ($type === 'App\Models\StandalonePostgresql') {
+ } elseif ($type === \App\Models\StandalonePostgresql::class) {
$internalPort = 5432;
- } elseif ($type === 'App\Models\StandaloneMongodb') {
+ } elseif ($type === \App\Models\StandaloneMongodb::class) {
$internalPort = 27017;
- } elseif ($type === 'App\Models\StandaloneMysql') {
+ } elseif ($type === \App\Models\StandaloneMysql::class) {
$internalPort = 3306;
- } elseif ($type === 'App\Models\StandaloneMariadb') {
+ } elseif ($type === \App\Models\StandaloneMariadb::class) {
$internalPort = 3306;
- } elseif ($type === 'App\Models\StandaloneKeydb') {
+ } elseif ($type === \App\Models\StandaloneKeydb::class) {
$internalPort = 6379;
- } elseif ($type === 'App\Models\StandaloneDragonfly') {
+ } elseif ($type === \App\Models\StandaloneDragonfly::class) {
$internalPort = 6379;
- } elseif ($type === 'App\Models\StandaloneClickhouse') {
+ } elseif ($type === \App\Models\StandaloneClickhouse::class) {
$internalPort = 9000;
}
$configuration_dir = database_proxy_dir($database->uuid);
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 19b1c5814..c72714e1c 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneDragonfly;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -47,11 +46,10 @@ class StartDragonfly
'networks' => [
$this->database->destination->network,
],
- 'ulimits' => [
- 'memlock' => '-1',
- ],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
@@ -80,14 +78,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;
@@ -103,6 +94,11 @@ class StartDragonfly
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -155,7 +151,7 @@ class StartDragonfly
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index a632f6e8c..bd98258ab 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -4,7 +4,6 @@ namespace App\Actions\Database;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -51,6 +50,8 @@ class StartKeydb
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
@@ -79,14 +80,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;
@@ -111,6 +105,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 = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -163,10 +161,12 @@ class StartKeydb
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$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 31d3f0640..696dd7ff4 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -46,6 +45,8 @@ class StartMariadb
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
@@ -74,14 +75,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;
@@ -105,6 +99,11 @@ class StartMariadb
'read_only' => true,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -157,21 +156,23 @@ class StartMariadb
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$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 8db34b20f..26a0f82d0 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -26,6 +25,10 @@ class StartMongodb
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ if (isDev()) {
+ $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
+ }
+
$this->commands = [
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
@@ -50,6 +53,8 @@ class StartMongodb
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -82,14 +87,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;
@@ -122,6 +120,10 @@ class StartMongodb
'read_only' => true,
];
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -174,18 +176,20 @@ class StartMongodb
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$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 8280faa56..a3694648f 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMysql;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -46,6 +45,8 @@ class StartMysql
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
@@ -74,14 +75,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;
@@ -105,6 +99,11 @@ class StartMysql
'read_only' => true,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -157,21 +156,23 @@ class StartMysql
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$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 23b9742c7..f5e85087f 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -50,6 +49,8 @@ class StartPostgresql
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -81,14 +82,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;
@@ -127,6 +121,10 @@ class StartPostgresql
'config_file=/etc/postgresql/postgresql.conf',
];
}
+ // Add custom docker run options
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -179,21 +177,23 @@ class StartPostgresql
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('PGUSER'))->isEmpty()) {
$environment_variables->push("PGUSER={$this->database->postgres_user}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_DB'))->isEmpty()) {
$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 065df5e52..7a2d2b34d 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -4,7 +4,6 @@ namespace App\Actions\Database;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -22,8 +21,6 @@ class StartRedis
{
$this->database = $database;
- $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
-
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
@@ -38,6 +35,8 @@ class StartRedis
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
+ $startCommand = $this->buildStartCommand();
+
$docker_compose = [
'services' => [
$container_name => [
@@ -51,6 +50,8 @@ class StartRedis
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -83,14 +84,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;
@@ -113,8 +107,12 @@ class StartRedis
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
- $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 = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($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";
@@ -163,17 +161,54 @@ class StartRedis
private function generate_environment_variables()
{
$environment_variables = collect();
+
foreach ($this->database->runtime_environment_variables as $env) {
- $environment_variables->push("$env->key=$env->real_value");
+ if ($env->is_shared) {
+ $environment_variables->push("$env->key=$env->real_value");
+
+ if ($env->key === 'REDIS_PASSWORD') {
+ $this->database->update(['redis_password' => $env->real_value]);
+ }
+
+ if ($env->key === 'REDIS_USERNAME') {
+ $this->database->update(['redis_username' => $env->real_value]);
+ }
+ } else {
+ if ($env->key === 'REDIS_PASSWORD') {
+ $env->update(['value' => $this->database->redis_password]);
+ } elseif ($env->key === 'REDIS_USERNAME') {
+ $env->update(['value' => $this->database->redis_username]);
+ }
+ $environment_variables->push("$env->key=$env->real_value");
+ }
}
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
- $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
- }
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}
+ private function buildStartCommand(): string
+ {
+ $hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf);
+ $redisConfPath = '/usr/local/etc/redis/redis.conf';
+
+ if ($hasRedisConf) {
+ $confContent = $this->database->redis_conf;
+ $hasRequirePass = str_contains($confContent, 'requirepass');
+
+ if ($hasRequirePass) {
+ $command = "redis-server $redisConfPath";
+ } else {
+ $command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}";
+ }
+ } else {
+ $command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
+ }
+
+ return $command;
+ }
+
private function add_custom_redis()
{
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index 66a32e811..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,26 +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(
- ["docker rm -f {$database->uuid}"],
- $server
- );
+
+ $this->stopContainer($database, $database->uuid, 300);
+ if (! $isDeleteOperation) {
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ }
+
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
- // TODO: make notification for services
- // $database->environment->project->team->notify(new StatusChanged($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/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php
index 984225435..0a166d24a 100644
--- a/app/Actions/Database/StopDatabaseProxy.php
+++ b/app/Actions/Database/StopDatabaseProxy.php
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
+use App\Events\DatabaseProxyStopped;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -21,12 +22,16 @@ class StopDatabaseProxy
{
$server = data_get($database, 'destination.server');
$uuid = $database->uuid;
- if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$uuid = $database->service->uuid;
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
+
$database->is_public = false;
$database->save();
+
+ DatabaseProxyStopped::dispatch();
+
}
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 9b32e89f3..a08056837 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -3,15 +3,13 @@
namespace App\Actions\Docker;
use App\Actions\Database\StartDatabaseProxy;
-use App\Actions\Proxy\CheckProxy;
-use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
-use App\Notifications\Container\ContainerStopped;
use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
@@ -20,16 +18,19 @@ class GetContainersStatus
public $applications;
+ public ?Collection $containers;
+
+ public ?Collection $containerReplicates;
+
public $server;
- public function handle(Server $server)
+ public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
{
- // if (isDev()) {
- // $server = Server::find(0);
- // }
+ $this->containers = $containers;
+ $this->containerReplicates = $containerReplicates;
$this->server = $server;
if (! $this->server->isFunctional()) {
- return 'Server is not ready.';
+ return 'Server is not functional.';
}
$this->applications = $this->server->applications();
$skip_these_applications = collect([]);
@@ -45,343 +46,18 @@ class GetContainersStatus
$this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) {
return ! $skip_these_applications->pluck('id')->contains($value->id);
});
- $this->old_way();
- // if ($this->server->isSwarm()) {
- // $this->old_way();
- // } else {
- // if (!$this->server->is_metrics_enabled) {
- // $this->old_way();
- // return;
- // }
- // $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false);
- // $sentinel_found = json_decode($sentinel_found, true);
- // $status = data_get($sentinel_found, '0.State.Status', 'exited');
- // if ($status === 'running') {
- // ray('Checking with Sentinel');
- // $this->sentinel();
- // } else {
- // ray('Checking the Old way');
- // $this->old_way();
- // }
- // }
- }
-
- private function sentinel()
- {
- try {
- $containers = $this->server->getContainers();
- if ($containers->count() === 0) {
- return;
- }
- $databases = $this->server->databases();
- $services = $this->server->services()->get();
- $previews = $this->server->previews();
- $foundApplications = [];
- $foundApplicationPreviews = [];
- $foundDatabases = [];
- $foundServices = [];
-
- foreach ($containers as $container) {
- $labels = Arr::undot(data_get($container, 'labels'));
- $containerStatus = data_get($container, 'state');
- $containerHealth = data_get($container, 'health_status', 'unhealthy');
- $containerStatus = "$containerStatus ($containerHealth)";
- $applicationId = data_get($labels, 'coolify.applicationId');
- if ($applicationId) {
- $pullRequestId = data_get($labels, 'coolify.pullRequestId');
- if ($pullRequestId) {
- if (str($applicationId)->contains('-')) {
- $applicationId = str($applicationId)->before('-');
- }
- $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
- if ($preview) {
- $foundApplicationPreviews[] = $preview->id;
- $statusFromDb = $preview->status;
- if ($statusFromDb !== $containerStatus) {
- $preview->update(['status' => $containerStatus]);
- }
- } else {
- //Notify user that this container should not be there.
- }
- } else {
- $application = $this->applications->where('id', $applicationId)->first();
- if ($application) {
- $foundApplications[] = $application->id;
- $statusFromDb = $application->status;
- if ($statusFromDb !== $containerStatus) {
- $application->update(['status' => $containerStatus]);
- }
- } else {
- //Notify user that this container should not be there.
- }
- }
- } else {
- $uuid = data_get($labels, 'com.docker.compose.service');
- $type = data_get($labels, 'coolify.type');
- if ($uuid) {
- if ($type === 'service') {
- $database_id = data_get($labels, 'coolify.service.subId');
- if ($database_id) {
- $service_db = ServiceDatabase::where('id', $database_id)->first();
- if ($service_db) {
- $uuid = $service_db->service->uuid;
- $isPublic = data_get($service_db, 'is_public');
- if ($isPublic) {
- $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
- if ($this->server->isSwarm()) {
- // TODO: fix this with sentinel
- return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
- } else {
- return data_get($value, 'name') === "$uuid-proxy";
- }
- })->first();
- if (! $foundTcpProxy) {
- StartDatabaseProxy::run($service_db);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
- }
- }
- }
- }
- } else {
- $database = $databases->where('uuid', $uuid)->first();
- if ($database) {
- $isPublic = data_get($database, 'is_public');
- $foundDatabases[] = $database->id;
- $statusFromDb = $database->status;
- if ($statusFromDb !== $containerStatus) {
- $database->update(['status' => $containerStatus]);
- }
- if ($isPublic) {
- $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
- if ($this->server->isSwarm()) {
- // TODO: fix this with sentinel
- return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
- } else {
- return data_get($value, 'name') === "$uuid-proxy";
- }
- })->first();
- if (! $foundTcpProxy) {
- StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
- }
- }
- } else {
- // Notify user that this container should not be there.
- }
- }
- }
- if (data_get($container, 'name') === 'coolify-db') {
- $foundDatabases[] = 0;
- }
- }
- $serviceLabelId = data_get($labels, 'coolify.serviceId');
- if ($serviceLabelId) {
- $subType = data_get($labels, 'coolify.service.subType');
- $subId = data_get($labels, 'coolify.service.subId');
- $service = $services->where('id', $serviceLabelId)->first();
- if (! $service) {
- continue;
- }
- if ($subType === 'application') {
- $service = $service->applications()->where('id', $subId)->first();
- } else {
- $service = $service->databases()->where('id', $subId)->first();
- }
- if ($service) {
- $foundServices[] = "$service->id-$service->name";
- $statusFromDb = $service->status;
- if ($statusFromDb !== $containerStatus) {
- // ray('Updating status: ' . $containerStatus);
- $service->update(['status' => $containerStatus]);
- }
- }
- }
- }
- $exitedServices = collect([]);
- foreach ($services as $service) {
- $apps = $service->applications()->get();
- $dbs = $service->databases()->get();
- foreach ($apps as $app) {
- if (in_array("$app->id-$app->name", $foundServices)) {
- continue;
- } else {
- $exitedServices->push($app);
- }
- }
- foreach ($dbs as $db) {
- if (in_array("$db->id-$db->name", $foundServices)) {
- continue;
- } else {
- $exitedServices->push($db);
- }
- }
- }
- $exitedServices = $exitedServices->unique('id');
- foreach ($exitedServices as $exitedService) {
- if (str($exitedService->status)->startsWith('exited')) {
- continue;
- }
- $name = data_get($exitedService, 'name');
- $fqdn = data_get($exitedService, 'fqdn');
- if ($name) {
- if ($fqdn) {
- $containerName = "$name, available at $fqdn";
- } else {
- $containerName = $name;
- }
- } else {
- if ($fqdn) {
- $containerName = $fqdn;
- } else {
- $containerName = null;
- }
- }
- $projectUuid = data_get($service, 'environment.project.uuid');
- $serviceUuid = data_get($service, 'uuid');
- $environmentName = data_get($service, 'environment.name');
-
- if ($projectUuid && $serviceUuid && $environmentName) {
- $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
- } else {
- $url = null;
- }
- // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
- $exitedService->update(['status' => 'exited']);
- }
-
- $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
- foreach ($notRunningApplications as $applicationId) {
- $application = $this->applications->where('id', $applicationId)->first();
- if (str($application->status)->startsWith('exited')) {
- continue;
- }
- $application->update(['status' => 'exited']);
-
- $name = data_get($application, 'name');
- $fqdn = data_get($application, 'fqdn');
-
- $containerName = $name ? "$name ($fqdn)" : $fqdn;
-
- $projectUuid = data_get($application, 'environment.project.uuid');
- $applicationUuid = data_get($application, 'uuid');
- $environment = data_get($application, 'environment.name');
-
- if ($projectUuid && $applicationUuid && $environment) {
- $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
- } else {
- $url = null;
- }
-
- // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
- }
- $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
- foreach ($notRunningApplicationPreviews as $previewId) {
- $preview = $previews->where('id', $previewId)->first();
- if (str($preview->status)->startsWith('exited')) {
- continue;
- }
- $preview->update(['status' => 'exited']);
-
- $name = data_get($preview, 'name');
- $fqdn = data_get($preview, 'fqdn');
-
- $containerName = $name ? "$name ($fqdn)" : $fqdn;
-
- $projectUuid = data_get($preview, 'application.environment.project.uuid');
- $environmentName = data_get($preview, 'application.environment.name');
- $applicationUuid = data_get($preview, 'application.uuid');
-
- if ($projectUuid && $applicationUuid && $environmentName) {
- $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
- } else {
- $url = null;
- }
-
- // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
- }
- $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
- foreach ($notRunningDatabases as $database) {
- $database = $databases->where('id', $database)->first();
- if (str($database->status)->startsWith('exited')) {
- continue;
- }
- $database->update(['status' => 'exited']);
-
- $name = data_get($database, 'name');
- $fqdn = data_get($database, 'fqdn');
-
- $containerName = $name;
-
- $projectUuid = data_get($database, 'environment.project.uuid');
- $environmentName = data_get($database, 'environment.name');
- $databaseUuid = data_get($database, 'uuid');
-
- if ($projectUuid && $databaseUuid && $environmentName) {
- $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
- } else {
- $url = null;
- }
- // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
- }
-
- // Check if proxy is running
- $this->server->proxyType();
- $foundProxyContainer = $containers->filter(function ($value, $key) {
- if ($this->server->isSwarm()) {
- // TODO: fix this with sentinel
- return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
- } else {
- return data_get($value, 'name') === 'coolify-proxy';
- }
- })->first();
- if (! $foundProxyContainer) {
- try {
- $shouldStart = CheckProxy::run($this->server);
- if ($shouldStart) {
- StartProxy::run($this->server, false);
- $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
- }
- } catch (\Throwable $e) {
- ray($e);
- }
- } else {
- $this->server->proxy->status = data_get($foundProxyContainer, 'state');
- $this->server->save();
- $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
- instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
- }
- } catch (\Exception $e) {
- // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
- ray($e->getMessage());
-
- return handleError($e);
+ if ($this->containers === null) {
+ ['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
}
- }
- private function old_way()
- {
- if ($this->server->isSwarm()) {
- $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
- $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
- } else {
- // Precheck for containers
- $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, false);
- $containerReplicates = null;
- }
- if (is_null($containers)) {
+ if (is_null($this->containers)) {
return;
}
- $containers = format_docker_command_output_to_json($containers);
- if ($containerReplicates) {
- $containerReplicates = format_docker_command_output_to_json($containerReplicates);
- foreach ($containerReplicates as $containerReplica) {
+ if ($this->containerReplicates) {
+ foreach ($this->containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
- $containers = $containers->map(function ($container) use ($name, $containerReplica) {
+ $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {
$replicas = data_get($containerReplica, 'Replicas');
$running = str($replicas)->explode('/')[0];
@@ -407,7 +83,7 @@ class GetContainersStatus
$foundDatabases = [];
$foundServices = [];
- foreach ($containers as $container) {
+ foreach ($this->containers as $container) {
if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name');
@@ -431,6 +107,8 @@ class GetContainersStatus
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
+ } else {
+ $preview->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -442,6 +120,8 @@ class GetContainersStatus
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
+ } else {
+ $application->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -461,7 +141,7 @@ class GetContainersStatus
if ($uuid) {
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
- $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
+ $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
@@ -484,9 +164,12 @@ class GetContainersStatus
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
+ } else {
+ $database->update(['last_online_at' => now()]);
}
+
if ($isPublic) {
- $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
+ $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
@@ -495,7 +178,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else {
@@ -526,6 +209,8 @@ class GetContainersStatus
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
+ } else {
+ $service->update(['last_online_at' => now()]);
}
}
}
@@ -549,7 +234,7 @@ class GetContainersStatus
}
}
}
- $exitedServices = $exitedServices->unique('id');
+ $exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
@@ -656,31 +341,5 @@ class GetContainersStatus
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
-
- // Check if proxy is running
- $this->server->proxyType();
- $foundProxyContainer = $containers->filter(function ($value, $key) {
- if ($this->server->isSwarm()) {
- return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
- } else {
- return data_get($value, 'Name') === '/coolify-proxy';
- }
- })->first();
- if (! $foundProxyContainer) {
- try {
- $shouldStart = CheckProxy::run($this->server);
- if ($shouldStart) {
- StartProxy::run($this->server, false);
- $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
- }
- } catch (\Throwable $e) {
- ray($e);
- }
- } else {
- $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
- $this->server->save();
- $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
- instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
- }
}
}
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index f8882d12a..ea2befd3a 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -2,17 +2,15 @@
namespace App\Actions\Fortify;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\Password;
use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers
{
- use PasswordValidationRules;
-
/**
* Validate and create a newly registered user.
*
@@ -20,7 +18,7 @@ class CreateNewUser implements CreatesNewUsers
*/
public function create(array $input): User
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403);
}
@@ -33,7 +31,7 @@ class CreateNewUser implements CreatesNewUsers
'max:255',
Rule::unique(User::class),
],
- 'password' => $this->passwordRules(),
+ 'password' => ['required', Password::defaults(), 'confirmed'],
])->validate();
if (User::count() == 0) {
@@ -42,19 +40,19 @@ class CreateNewUser implements CreatesNewUsers
$user = User::create([
'id' => 0,
'name' => $input['name'],
- 'email' => $input['email'],
+ 'email' => strtolower($input['email']),
'password' => Hash::make($input['password']),
]);
$team = $user->teams()->first();
// Disable registration after first user is created
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$settings->is_registration_enabled = false;
$settings->save();
} else {
$user = User::create([
'name' => $input['name'],
- 'email' => $input['email'],
+ 'email' => strtolower($input['email']),
'password' => Hash::make($input['password']),
]);
$team = $user->teams()->first();
diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php
deleted file mode 100644
index 92fcc7532..000000000
--- a/app/Actions/Fortify/PasswordValidationRules.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
- protected function passwordRules(): array
- {
- return ['required', 'string', new Password, 'confirmed'];
- }
-}
diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php
index 7a57c5037..d3727a52c 100644
--- a/app/Actions/Fortify/ResetUserPassword.php
+++ b/app/Actions/Fortify/ResetUserPassword.php
@@ -5,12 +5,11 @@ namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
+use Illuminate\Validation\Rules\Password;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
- use PasswordValidationRules;
-
/**
* Validate and reset the user's forgotten password.
*
@@ -19,7 +18,7 @@ class ResetUserPassword implements ResetsUserPasswords
public function reset(User $user, array $input): void
{
Validator::make($input, [
- 'password' => $this->passwordRules(),
+ 'password' => ['required', Password::defaults(), 'confirmed'],
])->validate();
$user->forceFill([
diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php
index 700563905..0c51ec56d 100644
--- a/app/Actions/Fortify/UpdateUserPassword.php
+++ b/app/Actions/Fortify/UpdateUserPassword.php
@@ -5,12 +5,11 @@ namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
+use Illuminate\Validation\Rules\Password;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
- use PasswordValidationRules;
-
/**
* Validate and update the user's password.
*
@@ -20,7 +19,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
- 'password' => $this->passwordRules(),
+ 'password' => ['required', Password::defaults(), 'confirmed'],
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
index dcb4058c0..26a1ff7bf 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,
@@ -26,8 +25,6 @@ class CheckResaleLicense
// }
$base_url = config('coolify.license_url');
$instance_id = config('app.id');
-
- ray("Checking license key against $base_url/lemon/validate");
$data = Http::withHeaders([
'Accept' => 'application/json',
])->get("$base_url/lemon/validate", [
@@ -35,7 +32,6 @@ class CheckResaleLicense
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
- ray('Valid & active license key');
$settings->update([
'is_resale_license_active' => true,
]);
@@ -49,7 +45,6 @@ class CheckResaleLicense
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'activated') === true) {
- ray('Activated license key');
$settings->update([
'is_resale_license_active' => true,
]);
@@ -61,7 +56,6 @@ class CheckResaleLicense
}
throw new \Exception('Cannot activate license key.');
} catch (\Throwable $e) {
- ray($e);
$settings->update([
'resale_license' => null,
'is_resale_license_active' => false,
diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php
index 35374ba43..bdeafd061 100644
--- a/app/Actions/Proxy/CheckConfiguration.php
+++ b/app/Actions/Proxy/CheckConfiguration.php
@@ -3,7 +3,6 @@
namespace App\Actions\Proxy;
use App\Models\Server;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckConfiguration
@@ -22,9 +21,8 @@ class CheckConfiguration
"cat $proxy_path/docker-compose.yml",
];
$proxy_configuration = instant_remote_process($payload, $server, false);
-
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
- $proxy_configuration = Str::of(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..51303d87a 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -2,14 +2,18 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Models\Server;
+use Illuminate\Support\Facades\Log;
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 +30,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 +66,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) {
+ Log::error('Error checking proxy: '.$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/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php
index 4c413ca36..f2de2b3f5 100644
--- a/app/Actions/Proxy/SaveConfiguration.php
+++ b/app/Actions/Proxy/SaveConfiguration.php
@@ -3,7 +3,6 @@
namespace App\Actions\Proxy;
use App\Models\Server;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveConfiguration
@@ -18,7 +17,7 @@ class SaveConfiguration
$proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
- $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
+ $server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return instant_remote_process([
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 710b5cdd8..7c93720cb 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -4,7 +4,6 @@ namespace App\Actions\Proxy;
use App\Events\ProxyStarted;
use App\Models\Server;
-use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
@@ -12,66 +11,62 @@ class StartProxy
{
use AsAction;
- public function handle(Server $server, bool $async = true): string|Activity
+ public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
{
- try {
- $proxyType = $server->proxyType();
- if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) {
- return 'OK';
- }
- $commands = collect([]);
- $proxy_path = $server->proxyPath();
- $configuration = CheckConfiguration::run($server);
- if (! $configuration) {
- throw new \Exception('Configuration is not synced');
- }
- SaveConfiguration::run($server, $configuration);
- $docker_compose_yml_base64 = base64_encode($configuration);
- $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
+ $proxyType = $server->proxyType();
+ if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
+ return 'OK';
+ }
+ $commands = collect([]);
+ $proxy_path = $server->proxyPath();
+ $configuration = CheckConfiguration::run($server);
+ if (! $configuration) {
+ throw new \Exception('Configuration is not synced');
+ }
+ 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->save();
+ if ($server->isSwarm()) {
+ $commands = $commands->merge([
+ "mkdir -p $proxy_path/dynamic",
+ "cd $proxy_path",
+ "echo 'Creating required Docker Compose file.'",
+ "echo 'Starting coolify-proxy.'",
+ 'docker stack deploy -c docker-compose.yml coolify-proxy',
+ "echo 'Successfully started coolify-proxy.'",
+ ]);
+ } else {
+ $caddfile = 'import /dynamic/*.caddy';
+ $commands = $commands->merge([
+ "mkdir -p $proxy_path/dynamic",
+ "cd $proxy_path",
+ "echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
+ "echo 'Creating required Docker Compose file.'",
+ "echo 'Pulling docker image.'",
+ 'docker compose pull',
+ '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 'Successfully started coolify-proxy.'",
+ ]);
+ $commands = $commands->merge(connectProxyToNetworks($server));
+ }
+
+ if ($async) {
+ return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
+ } else {
+ instant_remote_process($commands, $server);
+ $server->proxy->set('status', 'running');
+ $server->proxy->set('type', $proxyType);
$server->save();
- if ($server->isSwarm()) {
- $commands = $commands->merge([
- "mkdir -p $proxy_path/dynamic",
- "cd $proxy_path",
- "echo 'Creating required Docker Compose file.'",
- "echo 'Starting coolify-proxy.'",
- 'docker stack deploy -c docker-compose.yml coolify-proxy',
- "echo 'Proxy started successfully.'",
- ]);
- } else {
- $caddfile = 'import /dynamic/*.caddy';
- $commands = $commands->merge([
- "mkdir -p $proxy_path/dynamic",
- "cd $proxy_path",
- "echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
- "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',
- "echo 'Starting coolify-proxy.'",
- 'docker compose up -d --remove-orphans',
- "echo 'Proxy started successfully.'",
- ]);
- $commands = $commands->merge(connectProxyToNetworks($server));
- }
+ ProxyStarted::dispatch($server);
- if ($async) {
- $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
-
- return $activity;
- } else {
- instant_remote_process($commands, $server);
- $server->proxy->set('status', 'running');
- $server->proxy->set('type', $proxyType);
- $server->save();
- ProxyStarted::dispatch($server);
-
- return 'OK';
- }
- } catch (\Throwable $e) {
- ray($e);
- throw $e;
+ return 'OK';
}
}
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 1261e6830..dc6ac12bf 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -9,16 +9,31 @@ class CleanupDocker
{
use AsAction;
- public function handle(Server $server, bool $force = true)
+ public function handle(Server $server)
{
- 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..fc04e67a4 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;
@@ -39,9 +40,12 @@ 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',
]);
diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php
new file mode 100644
index 000000000..15c892e75
--- /dev/null
+++ b/app/Actions/Server/DeleteServer.php
@@ -0,0 +1,17 @@
+forceDelete();
+ }
+}
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index f671f2d2a..ba6c23ffc 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -12,12 +12,11 @@ class InstallDocker
public function handle(Server $server)
{
+ $dockerVersion = config('constants.docker_install_version');
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation .');
}
- ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
- $dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {
diff --git a/app/Actions/Server/ResourcesCheck.php b/app/Actions/Server/ResourcesCheck.php
new file mode 100644
index 000000000..e6b90ba38
--- /dev/null
+++ b/app/Actions/Server/ResourcesCheck.php
@@ -0,0 +1,41 @@
+subSeconds($seconds))->update(['status' => 'exited']);
+ ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+}
diff --git a/app/Actions/Server/RestartContainer.php b/app/Actions/Server/RestartContainer.php
new file mode 100644
index 000000000..63361d8b7
--- /dev/null
+++ b/app/Actions/Server/RestartContainer.php
@@ -0,0 +1,16 @@
+restartContainer($containerName);
+ }
+}
diff --git a/app/Actions/Server/RunCommand.php b/app/Actions/Server/RunCommand.php
new file mode 100644
index 000000000..254c78587
--- /dev/null
+++ b/app/Actions/Server/RunCommand.php
@@ -0,0 +1,17 @@
+value);
+ }
+}
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
new file mode 100644
index 000000000..1dae03fd9
--- /dev/null
+++ b/app/Actions/Server/ServerCheck.php
@@ -0,0 +1,269 @@
+server = $server;
+ try {
+ if ($this->server->isFunctional() === false) {
+ return 'Server is not functional.';
+ }
+
+ if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
+
+ if (isset($data)) {
+ $data = collect($data);
+
+ $this->server->sentinelHeartbeat();
+
+ $this->containers = collect(data_get($data, 'containers'));
+
+ $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
+ ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
+
+ $containerReplicates = null;
+ $this->isSentinel = true;
+
+ } else {
+ ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
+ // ServerStorageCheckJob::dispatch($this->server);
+ }
+
+ if (is_null($this->containers)) {
+ return 'No containers found.';
+ }
+
+ if (isset($containerReplicates)) {
+ foreach ($containerReplicates as $containerReplica) {
+ $name = data_get($containerReplica, 'Name');
+ $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) {
+ if (data_get($container, 'Spec.Name') === $name) {
+ $replicas = data_get($containerReplica, 'Replicas');
+ $running = str($replicas)->explode('/')[0];
+ $total = str($replicas)->explode('/')[1];
+ if ($running === $total) {
+ data_set($container, 'State.Status', 'running');
+ data_set($container, 'State.Health.Status', 'healthy');
+ } else {
+ data_set($container, 'State.Status', 'starting');
+ data_set($container, 'State.Health.Status', 'unhealthy');
+ }
+ }
+
+ return $container;
+ });
+ }
+ }
+ $this->checkContainers();
+
+ if ($this->server->isSentinelEnabled() && $this->isSentinel === false) {
+ CheckAndStartSentinelJob::dispatch($this->server);
+ }
+
+ if ($this->server->isLogDrainEnabled()) {
+ $this->checkLogDrainContainer();
+ }
+
+ if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
+ $foundProxyContainer = $this->containers->filter(function ($value, $key) {
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
+ } else {
+ return data_get($value, 'Name') === '/coolify-proxy';
+ }
+ })->first();
+ if (! $foundProxyContainer) {
+ try {
+ $shouldStart = CheckProxy::run($this->server);
+ if ($shouldStart) {
+ StartProxy::run($this->server, false);
+ $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
+ }
+ } catch (\Throwable $e) {
+ }
+ } else {
+ $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
+ $this->server->save();
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
+ instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+
+ private function checkLogDrainContainer()
+ {
+ $foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
+ return data_get($value, 'Name') === '/coolify-log-drain';
+ })->first();
+ if ($foundLogDrainContainer) {
+ $status = data_get($foundLogDrainContainer, 'State.Status');
+ if ($status !== 'running') {
+ StartLogDrain::dispatch($this->server)->onQueue('high');
+ }
+ } else {
+ StartLogDrain::dispatch($this->server)->onQueue('high');
+ }
+ }
+
+ private function checkContainers()
+ {
+ foreach ($this->containers as $container) {
+ if ($this->isSentinel) {
+ $labels = Arr::undot(data_get($container, 'labels'));
+ } else {
+ if ($this->server->isSwarm()) {
+ $labels = Arr::undot(data_get($container, 'Spec.Labels'));
+ } else {
+ $labels = Arr::undot(data_get($container, 'Config.Labels'));
+ }
+
+ }
+ $managed = data_get($labels, 'coolify.managed');
+ if (! $managed) {
+ continue;
+ }
+ $uuid = data_get($labels, 'coolify.name');
+ if (! $uuid) {
+ $uuid = data_get($labels, 'com.docker.compose.service');
+ }
+
+ if ($this->isSentinel) {
+ $containerStatus = data_get($container, 'state');
+ $containerHealth = data_get($container, 'health_status');
+ } else {
+ $containerStatus = data_get($container, 'State.Status');
+ $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
+ }
+ $containerStatus = "$containerStatus ($containerHealth)";
+
+ $applicationId = data_get($labels, 'coolify.applicationId');
+ $serviceId = data_get($labels, 'coolify.serviceId');
+ $databaseId = data_get($labels, 'coolify.databaseId');
+ $pullRequestId = data_get($labels, 'coolify.pullRequestId');
+
+ if ($applicationId) {
+ // Application
+ if ($pullRequestId != 0) {
+ if (str($applicationId)->contains('-')) {
+ $applicationId = str($applicationId)->before('-');
+ }
+ $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
+ if ($preview) {
+ $preview->update(['status' => $containerStatus]);
+ }
+ } else {
+ $application = Application::where('id', $applicationId)->first();
+ if ($application) {
+ $application->update([
+ 'status' => $containerStatus,
+ 'last_online_at' => now(),
+ ]);
+ }
+ }
+ } elseif (isset($serviceId)) {
+ // Service
+ $subType = data_get($labels, 'coolify.service.subType');
+ $subId = data_get($labels, 'coolify.service.subId');
+ $service = Service::where('id', $serviceId)->first();
+ if (! $service) {
+ continue;
+ }
+ if ($subType === 'application') {
+ $service = ServiceApplication::where('id', $subId)->first();
+ } else {
+ $service = ServiceDatabase::where('id', $subId)->first();
+ }
+ if ($service) {
+ $service->update([
+ 'status' => $containerStatus,
+ 'last_online_at' => now(),
+ ]);
+ if ($subType === 'database') {
+ $isPublic = data_get($service, 'is_public');
+ if ($isPublic) {
+ $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
+ if ($this->isSentinel) {
+ return data_get($value, 'name') === $uuid.'-proxy';
+ } else {
+
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
+ } else {
+ return data_get($value, 'Name') === "/$uuid-proxy";
+ }
+ }
+ })->first();
+ if (! $foundTcpProxy) {
+ StartDatabaseProxy::run($service);
+ }
+ }
+ }
+ }
+ } else {
+ // Database
+ if (is_null($this->databases)) {
+ $this->databases = $this->server->databases();
+ }
+ $database = $this->databases->where('uuid', $uuid)->first();
+ if ($database) {
+ $database->update([
+ 'status' => $containerStatus,
+ 'last_online_at' => now(),
+ ]);
+
+ $isPublic = data_get($database, 'is_public');
+ if ($isPublic) {
+ $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
+ if ($this->isSentinel) {
+ return data_get($value, 'name') === $uuid.'-proxy';
+ } else {
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
+ } else {
+
+ return data_get($value, 'Name') === "/$uuid-proxy";
+ }
+ }
+ })->first();
+ if (! $foundTcpProxy) {
+ StartDatabaseProxy::run($database);
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/StartLogDrain.php
similarity index 87%
rename from app/Actions/Server/InstallLogDrain.php
rename to app/Actions/Server/StartLogDrain.php
index 6f74e020b..0e8036cd9 100644
--- a/app/Actions/Server/InstallLogDrain.php
+++ b/app/Actions/Server/StartLogDrain.php
@@ -5,7 +5,7 @@ namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
-class InstallLogDrain
+class StartLogDrain
{
use AsAction;
@@ -13,23 +13,22 @@ class InstallLogDrain
{
if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic';
+ StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight';
+ StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom';
+ StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom';
+ StopLogDrain::run($server);
} else {
$type = 'none';
}
try {
if ($type === 'none') {
- $command = [
- "echo 'Stopping old Fluent Bit'",
- 'docker rm -f coolify-log-drain || true',
- ];
-
- return instant_remote_process($command, $server);
+ return 'No log drain is enabled.';
} elseif ($type === 'newrelic') {
if (! $server->settings->is_logdrain_newrelic_enabled) {
throw new \Exception('New Relic log drain is not enabled.');
@@ -52,7 +51,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 *
@@ -103,7 +106,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 *
@@ -148,6 +155,8 @@ services:
- ./parsers.conf:/parsers.conf
ports:
- 127.0.0.1:24224:24224
+ labels:
+ - coolify.managed=true
restart: unless-stopped
');
$readme = base64_encode('# New Relic Log Drain
@@ -199,10 +208,8 @@ Files:
throw new \Exception('Unknown log drain type.');
}
$restart_command = [
- "echo 'Stopping old Fluent Bit'",
- "cd $config_path && docker compose down --remove-orphans || true",
"echo 'Starting Fluent Bit'",
- "cd $config_path && docker compose up -d --remove-orphans",
+ "cd $config_path && docker compose up -d",
];
$command = array_merge($command, $add_envs_command, $restart_command);
diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php
index eea429c79..587ac4a8d 100644
--- a/app/Actions/Server/StartSentinel.php
+++ b/app/Actions/Server/StartSentinel.php
@@ -9,15 +9,57 @@ class StartSentinel
{
use AsAction;
- public function handle(Server $server, $version = 'latest', bool $restart = false)
+ public function handle(Server $server, bool $restart = false, ?string $latestVersion = null)
{
- if ($restart) {
- instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
+ if ($server->isSwarm() || $server->isBuildServer()) {
+ return;
}
+ if ($restart) {
+ StopSentinel::run($server);
+ }
+ $version = $latestVersion ?? get_latest_sentinel_version();
+ $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days');
+ $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
+ $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
+ $token = data_get($server, 'settings.sentinel_token');
+ $endpoint = data_get($server, 'settings.sentinel_custom_url');
+ $debug = data_get($server, 'settings.is_sentinel_debug_enabled');
+ $mountDir = '/data/coolify/sentinel';
+ $image = "ghcr.io/coollabsio/sentinel:$version";
+ if (! $endpoint) {
+ throw new \Exception('You should set FQDN in Instance Settings.');
+ }
+ $environments = [
+ 'TOKEN' => $token,
+ 'DEBUG' => $debug ? 'true' : 'false',
+ 'PUSH_ENDPOINT' => $endpoint,
+ 'PUSH_INTERVAL_SECONDS' => $pushInterval,
+ 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
+ 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate,
+ 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory,
+ ];
+ $labels = [
+ 'coolify.managed' => 'true',
+ ];
+ if (isDev()) {
+ // data_set($environments, 'DEBUG', 'true');
+ // $image = 'sentinel';
+ $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
+ }
+ $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
+ $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels));
+ $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image";
+
instant_remote_process([
- "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
- 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
- 'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
- ], $server, false);
+ 'docker rm -f coolify-sentinel || true',
+ "mkdir -p $mountDir",
+ $dockerCommand,
+ "chown -R 9999:root $mountDir",
+ "chmod -R 700 $mountDir",
+ ], $server);
+
+ $server->settings->is_sentinel_enabled = true;
+ $server->settings->save();
+ $server->sentinelHeartbeat();
}
}
diff --git a/app/Actions/Server/StopLogDrain.php b/app/Actions/Server/StopLogDrain.php
new file mode 100644
index 000000000..96c2466de
--- /dev/null
+++ b/app/Actions/Server/StopLogDrain.php
@@ -0,0 +1,20 @@
+sentinelHeartbeat(isReset: true);
+ }
+}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 0e97d2aed..d57a4fe46 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -2,8 +2,9 @@
namespace App\Actions\Server;
-use App\Models\InstanceSettings;
+use App\Jobs\PullHelperImageJob;
use App\Models\Server;
+use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -18,46 +19,44 @@ class UpdateCoolify
public function handle($manual_update = false)
{
- try {
- $settings = InstanceSettings::get();
- ray('Running InstanceAutoUpdateJob');
- $this->server = Server::find(0);
- if (! $this->server) {
+ if (isDev()) {
+ Sleep::for(10)->seconds();
+
+ return;
+ }
+ $settings = instanceSettings();
+ $this->server = Server::find(0);
+ if (! $this->server) {
+ return;
+ }
+ CleanupDocker::dispatch($this->server)->onQueue('high');
+ $this->latestVersion = get_latest_version_of_coolify();
+ $this->currentVersion = config('version');
+ if (! $manual_update) {
+ if (! $settings->is_auto_update_enabled) {
return;
}
- CleanupDocker::run($this->server, false);
- $this->latestVersion = get_latest_version_of_coolify();
- $this->currentVersion = config('version');
- if (! $manual_update) {
- if (! $settings->is_auto_update_enabled) {
- return;
- }
- if ($this->latestVersion === $this->currentVersion) {
- return;
- }
- if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
- return;
- }
+ if ($this->latestVersion === $this->currentVersion) {
+ return;
+ }
+ if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
+ return;
}
- $this->update();
- } catch (\Throwable $e) {
- throw $e;
}
+ $this->update();
+ $settings->new_version_available = false;
+ $settings->save();
}
private function update()
{
- if (isDev()) {
- remote_process([
- 'sleep 10',
- ], $this->server);
+ PullHelperImageJob::dispatch($this->server);
+
+ instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
- return;
- }
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/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
new file mode 100644
index 000000000..d0a4cd6be
--- /dev/null
+++ b/app/Actions/Server/ValidateServer.php
@@ -0,0 +1,67 @@
+update([
+ 'validation_logs' => null,
+ ]);
+ ['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection();
+ if (! $this->uptime) {
+ $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help. Error: '.$error.'
';
+ $server->update([
+ 'validation_logs' => $this->error,
+ ]);
+ throw new \Exception($this->error);
+ }
+ $this->supported_os_type = $server->validateOS();
+ if (! $this->supported_os_type) {
+ $this->error = 'Server OS type is not supported. Please install Docker manually before continuing: documentation .';
+ $server->update([
+ 'validation_logs' => $this->error,
+ ]);
+ throw new \Exception($this->error);
+ }
+
+ $this->docker_installed = $server->validateDockerEngine();
+ $this->docker_compose_installed = $server->validateDockerCompose();
+ if (! $this->docker_installed || ! $this->docker_compose_installed) {
+ $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation .';
+ $server->update([
+ 'validation_logs' => $this->error,
+ ]);
+ throw new \Exception($this->error);
+ }
+ $this->docker_version = $server->validateDockerEngineVersion();
+
+ if ($this->docker_version) {
+ return 'OK';
+ } else {
+ $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation .';
+ $server->update([
+ 'validation_logs' => $this->error,
+ ]);
+ throw new \Exception($this->error);
+ }
+ }
+}
diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php
index 194cf4db9..9b87454da 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -2,18 +2,20 @@
namespace App\Actions\Service;
+use App\Actions\Server\CleanupDocker;
use App\Models\Service;
+use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
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 +35,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 !== null && $result !== 0) {
+ Log::error('Error deleting volumes: '.$result);
+ }
+ }
+ }
}
+
+ 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 +68,11 @@ class DeleteService
$task->delete();
}
$service->tags()->detach();
+ $service->forceDelete();
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
}
}
}
diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php
new file mode 100644
index 000000000..1b6a5c32c
--- /dev/null
+++ b/app/Actions/Service/RestartService.php
@@ -0,0 +1,18 @@
+name);
$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,11 +30,10 @@ 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');
- return $activity;
+ return 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 4c0042ebd..046d94ced 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,34 +10,25 @@ 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(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
- $application->update(['status' => 'exited']);
- }
- $dbs = $service->databases()->get();
- foreach ($dbs as $db) {
- instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
- $db->update(['status' => 'exited']);
- }
- instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
- instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
- // TODO: make notification for databases
- // $service->environment->project->team->notify(new StatusChanged($service));
- } catch (\Exception $e) {
- echo $e->getMessage();
- ray($e->getMessage());
+ $containersToStop = $service->getContainersToStop();
+ $service->stopContainers($containersToStop, $server);
+
+ if (! $isDeleteOperation) {
+ $service->delete_connected_networks($service->uuid);
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ }
+ } catch (\Exception $e) {
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/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php
index 1e177ca62..a0adc8b36 100644
--- a/app/Console/Commands/CleanupDatabase.php
+++ b/app/Console/Commands/CleanupDatabase.php
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\DB;
class CleanupDatabase extends Command
{
- protected $signature = 'cleanup:database {--yes}';
+ protected $signature = 'cleanup:database {--yes} {--keep-days=}';
protected $description = 'Cleanup database';
@@ -18,7 +18,12 @@ class CleanupDatabase extends Command
} else {
echo "Running database cleanup in dry-run mode...\n";
}
- $keep_days = 60;
+ if (isCloud()) {
+ // Later on we can increase this to 180 days or dynamically set
+ $keep_days = $this->option('keep-days') ?? 60;
+ } else {
+ $keep_days = $this->option('keep-days') ?? 60;
+ }
echo "Keep days: $keep_days\n";
// Cleanup failed jobs table
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1));
@@ -59,6 +64,5 @@ class CleanupDatabase extends Command
if ($this->option('yes')) {
$webhooks->delete();
}
-
}
}
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..5fc2b4e61
--- /dev/null
+++ b/app/Console/Commands/CleanupRedis.php
@@ -0,0 +1,30 @@
+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..9d36ce9b8 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;
@@ -25,14 +30,33 @@ class CleanupStuckedResources extends Command
public function handle()
{
- ray('Running cleanup stucked resources.');
echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources();
}
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 +66,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 +188,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/CleanupUnreachableServers.php b/app/Console/Commands/CleanupUnreachableServers.php
index 328628039..df0c6b81b 100644
--- a/app/Console/Commands/CleanupUnreachableServers.php
+++ b/app/Console/Commands/CleanupUnreachableServers.php
@@ -18,7 +18,7 @@ class CleanupUnreachableServers extends Command
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
- send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
+ // send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([
'ip' => '1.2.3.4',
]);
diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php
new file mode 100644
index 000000000..6e237e84b
--- /dev/null
+++ b/app/Console/Commands/CloudCheckSubscription.php
@@ -0,0 +1,49 @@
+get();
+ foreach ($activeSubscribers as $team) {
+ $stripeSubscriptionId = $team->subscription->stripe_subscription_id;
+ $stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
+ $stripeCustomerId = $team->subscription->stripe_customer_id;
+ if (! $stripeSubscriptionId) {
+ echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
+
+ continue;
+ }
+ $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
+ if ($subscription->status === 'active') {
+ continue;
+ }
+ echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
+ }
+ }
+}
diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php
new file mode 100644
index 000000000..8bb420ab8
--- /dev/null
+++ b/app/Console/Commands/CloudCleanupSubscriptions.php
@@ -0,0 +1,98 @@
+error('This command can only be run on cloud');
+
+ return;
+ }
+ $this->info('Cleaning up subcriptions teams');
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+
+ $teams = Team::all()->filter(function ($team) {
+ return $team->id !== 0;
+ })->sortBy('id');
+ foreach ($teams as $team) {
+ if ($team) {
+ $this->info("Checking team {$team->id}");
+ }
+ if (! data_get($team, 'subscription')) {
+ $this->disableServers($team);
+
+ continue;
+ }
+ // If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
+ if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
+ $this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
+
+ $team->subscription->update([
+ 'stripe_invoice_paid' => false,
+ 'stripe_trial_already_ended' => false,
+ 'stripe_subscription_id' => null,
+ ]);
+ $this->disableServers($team);
+
+ continue;
+ } else {
+ $subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
+ $status = data_get($subscription, 'status');
+ if ($status === 'active' || $status === 'past_due') {
+ $team->subscription->update([
+ 'stripe_invoice_paid' => true,
+ 'stripe_trial_already_ended' => false,
+ ]);
+
+ continue;
+ }
+ $this->info('Subscription status: '.$status);
+ $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
+ $confirm = $this->confirm('Do you want to cancel the subscription?', true);
+ if (! $confirm) {
+ $this->info("Skipping team {$team->id} {$team->name}");
+ } else {
+ $this->info("Cancelling subscription for team {$team->id} {$team->name}");
+ $team->subscription->update([
+ 'stripe_invoice_paid' => false,
+ 'stripe_trial_already_ended' => false,
+ 'stripe_subscription_id' => null,
+ ]);
+ $this->disableServers($team);
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ $this->error($e->getMessage());
+
+ return;
+ }
+ }
+
+ private function disableServers(Team $team)
+ {
+ foreach ($team->servers as $server) {
+ if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') {
+ $this->info("Disabling server {$server->id} {$server->name}");
+ $server->settings()->update([
+ 'is_usable' => false,
+ 'is_reachable' => false,
+ ]);
+ $server->update([
+ 'ip' => '1.2.3.4',
+ ]);
+ }
+ }
+ }
+}
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 80059bf00..f5f1233fe 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -9,17 +9,51 @@ use Illuminate\Support\Facades\Process;
class Dev extends Command
{
- protected $signature = 'dev:init';
+ protected $signature = 'dev {--init} {--generate-openapi}';
- protected $description = 'Init the app in dev mode';
+ protected $description = 'Helper commands for development.';
public function handle()
+ {
+ if ($this->option('init')) {
+ $this->init();
+
+ return;
+ }
+ if ($this->option('generate-openapi')) {
+ $this->generateOpenApi();
+
+ return;
+ }
+ }
+
+ public function generateOpenApi()
+ {
+ // Generate OpenAPI documentation
+ echo "Generating OpenAPI documentation.\n";
+ $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ $error = $process->errorOutput();
+ $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
+ $error = preg_replace('/^\h*\v+/m', '', $error);
+ echo $error;
+ echo $process->output();
+ }
+
+ public function init()
{
// Generate APP_KEY if not exists
+
if (empty(env('APP_KEY'))) {
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/Emails.php b/app/Console/Commands/Emails.php
index 8ad0d458f..cda4ca84f 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -15,7 +15,6 @@ use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
-use App\Notifications\Database\DailyBackup;
use App\Notifications\Test;
use Exception;
use Illuminate\Console\Command;
@@ -81,7 +80,7 @@ class Emails extends Command
}
set_transanctional_email_settings();
- $this->mail = new MailMessage();
+ $this->mail = new MailMessage;
$this->mail->subject('Test Email');
switch ($type) {
case 'updates':
@@ -107,7 +106,7 @@ class Emails extends Command
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
- $this->mail = new MailMessage();
+ $this->mail = new MailMessage;
$this->mail->subject('One-click Services, Docker Compose support');
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
'token' => encrypt($email),
@@ -118,31 +117,13 @@ class Emails extends Command
}
break;
case 'emails-test':
- $this->mail = (new Test())->toMail();
- $this->sendEmail();
- break;
- case 'database-backup-statuses-daily':
- $scheduled_backups = ScheduledDatabaseBackup::all();
- $databases = 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;
- $databases->put($database->name, [
- 'failed_count' => $failed->count(),
- ]);
- }
- $this->mail = (new DailyBackup($databases))->toMail();
+ $this->mail = (new Test)->toMail();
$this->sendEmail();
break;
case 'application-deployment-success-daily':
$applications = Application::all();
foreach ($applications as $application) {
$deployments = $application->get_last_days_deployments();
- ray($deployments);
if ($deployments->isEmpty()) {
continue;
}
@@ -224,7 +205,7 @@ class Emails extends Command
// $this->sendEmail();
// break;
case 'waitlist-invitation-link':
- $this->mail = new MailMessage();
+ $this->mail = new MailMessage;
$this->mail->view('emails.waitlist-invitation', [
'loginLink' => 'https://coolify.io',
]);
@@ -241,7 +222,7 @@ class Emails extends Command
break;
case 'realusers-before-trial':
- $this->mail = new MailMessage();
+ $this->mail = new MailMessage;
$this->mail->view('emails.before-trial-conversion');
$this->mail->subject('Trial period has been added for all subscription plans.');
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
@@ -287,7 +268,7 @@ class Emails extends Command
foreach ($admins as $admin) {
$this->info($admin);
}
- $this->mail = new MailMessage();
+ $this->mail = new MailMessage;
$this->mail->view('emails.server-lost-connection', [
'name' => $server->name,
]);
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 50c9fe29b..c802fb116 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -2,51 +2,74 @@
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\InstanceSettings;
+use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
+use App\Models\User;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class Init extends Command
{
- protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}';
+ protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
+ public $servers = null;
+
public function handle()
{
- $this->alive();
- get_public_ips();
- $full_cleanup = $this->option('full-cleanup');
- $cleanup_deployments = $this->option('cleanup-deployments');
- if ($cleanup_deployments) {
- echo "Running cleanup deployments.\n";
- $this->cleanup_in_progress_application_deployments();
+ if (isCloud() && ! $this->option('force-cloud')) {
+ echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
- if ($full_cleanup) {
- // Required for falsely deleted coolify db
- $this->restore_coolify_db_backup();
- $this->cleanup_in_progress_application_deployments();
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:queue');
- $this->call('cleanup:stucked-resources');
- if (! isCloud()) {
- try {
- $server = Server::find(0)->first();
- $server->setupDynamicProxyConfiguration();
- } catch (\Throwable $e) {
- echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
- }
- }
- $settings = InstanceSettings::get();
+ $this->servers = Server::all();
+ 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_user_emails();
+ //
+ $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]);
@@ -54,53 +77,134 @@ class Init extends Command
$settings->update(['is_auto_update_enabled' => false]);
}
}
-
- return;
}
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:stucked-resources');
+ }
+
+ // 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) {
+ // $server->settings->update(['is_metrics_enabled' => false]);
+ // }
+ // if ($server->isFunctional()) {
+ // StopSentinel::dispatch($server)->onQueue('high');
+ // }
+ // }
+ // }
+ // }
+
+ private function update_user_emails()
+ {
+ try {
+ User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)]));
+ } catch (\Throwable $e) {
+ echo "Error in updating user emails: {$e->getMessage()}\n";
+ }
+ }
+
+ private function update_traefik_labels()
+ {
+ try {
+ Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']);
+ } catch (\Throwable $e) {
+ echo "Error in updating traefik labels: {$e->getMessage()}\n";
+ }
+ }
+
+ private function cleanup_unnecessary_dynamic_proxy_configuration()
+ {
+ 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";
+ }
+ }
+ }
+
+ private function cleanup_unused_network_from_coolify_proxy()
+ {
+ foreach ($this->servers as $server) {
+ if (! $server->isFunctional()) {
+ continue;
+ }
+ if (! $server->isProxyShouldRun()) {
+ continue;
+ }
+ try {
+ ['networks' => $networks, 'allNetworks' => $allNetworks] = collectDockerNetworksByServer($server);
+ $removeNetworks = $allNetworks->diff($networks);
+ $commands = collect();
+ foreach ($removeNetworks as $network) {
+ $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
+ if (empty($out)) {
+ $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ } else {
+ $data = collect(json_decode($out, true));
+ if ($data->count() === 1) {
+ // If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
+ $isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
+ if ($isCoolifyProxyItself) {
+ $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ }
+ }
+ }
+ }
+ if ($commands->isNotEmpty()) {
+ echo "Cleaning up unused networks from coolify proxy\n";
+ remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false);
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning up unused networks from coolify proxy: {$e->getMessage()}\n";
+ }
+ }
}
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::class,
+ 'team_id' => 0,
+ ]);
+ }
}
- }
- } catch (\Throwable $e) {
- echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
- }
- }
-
- private function cleanup_stucked_helper_containers()
- {
- $servers = Server::all();
- foreach ($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";
@@ -114,34 +218,16 @@ 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
-
try {
if (isCloud()) {
return;
}
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
foreach ($queued_inprogress_deployments as $deployment) {
- ray($deployment->id, $deployment->status);
echo "Cleaning up deployment: {$deployment->id}\n";
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save();
@@ -150,4 +236,17 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n";
}
}
+
+ private function replace_slash_in_environment_name()
+ {
+ 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/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php
index 81333b868..f0131b7b2 100644
--- a/app/Console/Commands/NotifyDemo.php
+++ b/app/Console/Commands/NotifyDemo.php
@@ -36,8 +36,6 @@ class NotifyDemo extends Command
return;
}
-
- ray($channel);
}
private function showHelp()
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
new file mode 100644
index 000000000..e248aa2c0
--- /dev/null
+++ b/app/Console/Commands/OpenApi.php
@@ -0,0 +1,25 @@
+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/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php
index b5a74166a..1e5d5808c 100644
--- a/app/Console/Commands/ServicesDelete.php
+++ b/app/Console/Commands/ServicesDelete.php
@@ -96,7 +96,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
break;
}
- DeleteResourceJob::dispatch($toDelete);
+ DeleteResourceJob::dispatch($toDelete)->onQueue('high');
}
}
}
@@ -122,7 +122,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
return;
}
- DeleteResourceJob::dispatch($toDelete);
+ DeleteResourceJob::dispatch($toDelete)->onQueue('high');
}
}
}
@@ -148,7 +148,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
return;
}
- DeleteResourceJob::dispatch($toDelete);
+ DeleteResourceJob::dispatch($toDelete)->onQueue('high');
}
}
}
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index de64afefa..1559e5f6d 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -3,128 +3,82 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
+use Illuminate\Support\Arr;
use Symfony\Component\Yaml\Yaml;
class ServicesGenerate extends Command
{
/**
- * The name and signature of the console command.
- *
- * @var string
+ * {@inheritdoc}
*/
protected $signature = 'services:generate';
/**
- * The console command description.
- *
- * @var string
+ * {@inheritdoc}
*/
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
- /**
- * Execute the console command.
- */
- public function handle()
+ public function handle(): int
{
- $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
- $files = array_filter($files, function ($file) {
- return strpos($file, '.yaml') !== false;
- });
- $serviceTemplatesJson = [];
- foreach ($files as $file) {
- $parsed = $this->process_file($file);
- if ($parsed) {
- $name = data_get($parsed, 'name');
- $parsed = data_forget($parsed, 'name');
- $serviceTemplatesJson[$name] = $parsed;
- }
- }
- $serviceTemplatesJson = json_encode($serviceTemplatesJson);
- file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
+ $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml')))
+ ->mapWithKeys(function ($file): array {
+ $file = basename($file);
+ $parsed = $this->processFile($file);
+
+ return $parsed === false ? [] : [
+ Arr::pull($parsed, 'name') => $parsed,
+ ];
+ })->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+
+ file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
+
+ return self::SUCCESS;
}
- private function process_file($file)
+ private function processFile(string $file): false|array
{
- $serviceName = str($file)->before('.yaml')->value();
$content = file_get_contents(base_path("templates/compose/$file"));
- // $this->info($content);
- $ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
- if ($ignore->count() > 0) {
- $ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value();
- } else {
- $ignore = false;
- }
- if ($ignore) {
+
+ $data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array {
+ preg_match('/^#(?.*):(?.*)$/U', $line, $m);
+
+ return $m ? [trim($m['key']) => trim($m['value'])] : [];
+ });
+
+ if (str($data->get('ignore'))->toBoolean()) {
$this->info("Ignoring $file");
- return;
+ return false;
}
+
$this->info("Processing $file");
- $documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
- if ($documentation->count() > 0) {
- $documentation = str($documentation[0])->after('# documentation:')->trim()->value();
- $documentation = str($documentation)->append('?utm_source=coolify.io');
- } else {
- $documentation = 'https://coolify.io/docs';
- }
- $slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values();
- if ($slogan->count() > 0) {
- $slogan = str($slogan[0])->after('# slogan:')->trim()->value();
- } else {
- $slogan = str($file)->headline()->value();
- }
- $logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values();
- if ($logo->count() > 0) {
- $logo = str($logo[0])->after('# logo:')->trim()->value();
- } else {
- $logo = 'svgs/unknown.svg';
- }
- $minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
- if ($minversion->count() > 0) {
- $minversion = str($minversion[0])->after('# minversion:')->trim()->value();
- } else {
- $minversion = '0.0.0';
- }
- $env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
- if ($env_file->count() > 0) {
- $env_file = str($env_file[0])->after('# env_file:')->trim()->value();
- } else {
- $env_file = null;
- }
+ $documentation = $data->get('documentation');
+ $documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs';
- $tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values();
- if ($tags->count() > 0) {
- $tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) {
- return str($tag)->trim()->lower()->value();
- })->values();
- } else {
- $tags = null;
- }
- $port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values();
- if ($port->count() > 0) {
- $port = str($port[0])->after('# port:')->trim()->value();
- } else {
- $port = null;
- }
$json = Yaml::parse($content);
- $yaml = base64_encode(Yaml::dump($json, 10, 2));
+ $compose = base64_encode(Yaml::dump($json, 10, 2));
+
+ $tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter();
+ $tags = $tags->isEmpty() ? null : $tags->all();
+
$payload = [
- 'name' => $serviceName,
+ 'name' => pathinfo($file, PATHINFO_FILENAME),
'documentation' => $documentation,
- 'slogan' => $slogan,
- 'compose' => $yaml,
+ 'slogan' => $data->get('slogan', str($file)->headline()),
+ 'compose' => $compose,
'tags' => $tags,
- 'logo' => $logo,
- 'minversion' => $minversion,
+ 'logo' => $data->get('logo', 'svgs/coolify.png'),
+ 'minversion' => $data->get('minversion', '0.0.0'),
];
- if ($port) {
+
+ if ($port = $data->get('port')) {
$payload['port'] = $port;
}
- if ($env_file) {
- $env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
- $env_file_base64 = base64_encode($env_file_content);
- $payload['envs'] = $env_file_base64;
+
+ if ($envFile = $data->get('env_file')) {
+ $envFileContent = file_get_contents(base_path("templates/compose/$envFile"));
+ $payload['envs'] = base64_encode($envFileContent);
}
return $payload;
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/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php
index 88ff21d46..2e330068c 100644
--- a/app/Console/Commands/WaitlistInvite.php
+++ b/app/Console/Commands/WaitlistInvite.php
@@ -82,7 +82,7 @@ class WaitlistInvite extends Command
if (! $already_registered) {
$this->password = Str::password();
User::create([
- 'name' => Str::of($this->next_patient->email)->before('@'),
+ 'name' => str($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
'force_password_reset' => true,
@@ -103,7 +103,7 @@ class WaitlistInvite extends Command
{
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->view('emails.waitlist-invitation', [
'loginLink' => $loginLink,
]);
diff --git a/app/Console/Commands/Weird.php b/app/Console/Commands/Weird.php
new file mode 100644
index 000000000..e471a5f96
--- /dev/null
+++ b/app/Console/Commands/Weird.php
@@ -0,0 +1,58 @@
+error('This command can only be run in development mode');
+
+ return;
+ }
+ $run = $this->option('run');
+ if ($run) {
+ $servers = Server::all();
+ foreach ($servers as $server) {
+ ServerCheck::dispatch($server);
+ }
+
+ return;
+ }
+ $number = $this->option('number');
+ for ($i = 0; $i < $number; $i++) {
+ $uuid = Str::uuid();
+ $server = Server::create([
+ 'name' => 'localhost-'.$uuid,
+ 'description' => 'This is a test docker container in development mode',
+ 'ip' => 'coolify-testing-host',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::NONE->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+ $server->settings->update([
+ 'is_usable' => true,
+ 'is_reachable' => true,
+ ]);
+ }
+ } catch (\Exception $e) {
+ $this->error($e->getMessage());
+ }
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index e2698d90e..3fb4de60b 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -2,135 +2,195 @@
namespace App\Console;
-use App\Jobs\CheckLogDrainContainerJob;
+use App\Jobs\CheckAndStartSentinelJob;
+use App\Jobs\CheckForUpdatesJob;
+use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob;
-use App\Jobs\ContainerStatusJob;
+use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
-use App\Jobs\PullCoolifyImageJob;
-use App\Jobs\PullHelperImageJob;
-use App\Jobs\PullSentinelImageJob;
+use App\Jobs\DockerCleanupJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
-use App\Jobs\ServerStatusJob;
+use App\Jobs\ServerCheckJob;
+use App\Jobs\ServerCleanupMux;
+use App\Jobs\ServerStorageCheckJob;
+use App\Jobs\UpdateCoolifyJob;
+use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel
{
- private $all_servers;
+ private $allServers;
+
+ private InstanceSettings $settings;
+
+ private string $updateCheckFrequency;
+
+ private string $instanceTimezone;
protected function schedule(Schedule $schedule): void
{
- $this->all_servers = Server::all();
+ $this->allServers = Server::where('ip', '!=', '1.2.3.4');
+
+ $this->settings = instanceSettings();
+ $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
+ $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
+
+ $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
+
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer();
+ $schedule->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
+
// Server Jobs
- $this->check_scheduled_backups($schedule);
- $this->check_resources($schedule);
- $this->check_scheduled_backups($schedule);
- $this->check_scheduled_tasks($schedule);
+ $this->checkResources($schedule);
+
+ $this->checkScheduledBackups($schedule);
+ $this->checkScheduledTasks($schedule);
+
$schedule->command('uploads:clear')->everyTwoMinutes();
+
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily();
- $schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer();
+ $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
+ $schedule->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
- // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
+ $this->scheduleUpdates($schedule);
// Server Jobs
- $this->check_scheduled_backups($schedule);
- $this->check_resources($schedule);
- $this->pull_images($schedule);
- $this->check_scheduled_tasks($schedule);
+ $this->checkResources($schedule);
+
+ $this->pullImages($schedule);
+
+ $this->checkScheduledBackups($schedule);
+ $this->checkScheduledTasks($schedule);
$schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
}
}
- private function pull_images($schedule)
+ private function pullImages($schedule): void
{
- $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
+ $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) {
- if (config('coolify.is_sentinel_enabled')) {
- $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
+ if ($server->isSentinelEnabled()) {
+ $schedule->job(function () use ($server) {
+ CheckAndStartSentinelJob::dispatch($server);
+ })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
}
- $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();
+ }
+ $schedule->job(new CheckHelperImageJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
+ ->onOneServer();
+ }
+
+ private function scheduleUpdates($schedule): void
+ {
+ $schedule->job(new CheckForUpdatesJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
+ ->onOneServer();
+
+ if ($this->settings->is_auto_update_enabled) {
+ $autoUpdateFrequency = $this->settings->auto_update_frequency;
+ $schedule->job(new UpdateCoolifyJob)
+ ->cron($autoUpdateFrequency)
+ ->timezone($this->instanceTimezone)
+ ->onOneServer();
}
}
- private function check_resources($schedule)
+ private function checkResources($schedule): void
{
if (isCloud()) {
- $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
+ $servers = $this->allServers->whereHas('team.subscription')->get();
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
- $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else {
- $servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
- $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
- }
- foreach ($containerServers as $server) {
- $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
- if ($server->isLogDrainEnabled()) {
- $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
- }
+ $servers = $this->allServers->get();
}
+
foreach ($servers as $server) {
- $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
+ $serverTimezone = $server->settings->server_timezone;
+
+ // Sentinel check
+ $lastSentinelUpdate = $server->sentinel_updated_at;
+ if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
+ // Check container status every minute if Sentinel does not activated
+ $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
+ // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
+
+ // Check storage usage every 10 minutes if Sentinel does not activated
+ $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
+ }
+ 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();
+ }
+
+ // Cleanup multiplexed connections every hour
+ $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer();
+
+ // Temporary solution until we have better memory management for Sentinel
+ if ($server->isSentinelEnabled()) {
+ $schedule->job(function () use ($server) {
+ $server->restartContainer('coolify-sentinel');
+ })->daily()->onOneServer();
+ }
}
}
- private function check_scheduled_backups($schedule)
+ private function checkScheduledBackups($schedule): void
{
- $scheduled_backups = ScheduledDatabaseBackup::all();
+ $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) {
return;
}
foreach ($scheduled_backups as $scheduled_backup) {
- if (! $scheduled_backup->enabled) {
- continue;
- }
if (is_null(data_get($scheduled_backup, 'database'))) {
- ray('database not found');
$scheduled_backup->delete();
continue;
}
+ $server = $scheduled_backup->server();
+
+ if (is_null($server)) {
+ continue;
+ }
+
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($this->instanceTimezone)->onOneServer();
}
}
- private function check_scheduled_tasks($schedule)
+ private function checkScheduledTasks($schedule): void
{
- $scheduled_tasks = ScheduledTask::all();
+ $scheduled_tasks = ScheduledTask::where('enabled', true)->get();
if ($scheduled_tasks->isEmpty()) {
return;
}
foreach ($scheduled_tasks as $scheduled_task) {
- if ($scheduled_task->enabled === false) {
- continue;
- }
$service = $scheduled_task->service;
$application = $scheduled_task->application;
if (! $application && ! $service) {
- ray('application/service attached to scheduled task does not exist');
$scheduled_task->delete();
continue;
@@ -145,12 +205,18 @@ class Kernel extends ConsoleKernel
continue;
}
}
+
+ $server = $scheduled_task->server();
+ if (! $server) {
+ continue;
+ }
+
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($this->instanceTimezone)->onOneServer();
}
}
diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php
index b96efa622..d95944b15 100644
--- a/app/Data/ServerMetadata.php
+++ b/app/Data/ServerMetadata.php
@@ -11,6 +11,5 @@ class ServerMetadata extends Data
public function __construct(
public ?ProxyTypes $type,
public ?ProxyStatus $status
- ) {
- }
+ ) {}
}
diff --git a/app/Enums/ActivityTypes.php b/app/Enums/ActivityTypes.php
index e2536a7f0..2d23cd98b 100644
--- a/app/Enums/ActivityTypes.php
+++ b/app/Enums/ActivityTypes.php
@@ -5,4 +5,5 @@ namespace App\Enums;
enum ActivityTypes: string
{
case INLINE = 'inline';
+ case COMMAND = 'command';
}
diff --git a/app/Enums/BuildPackTypes.php b/app/Enums/BuildPackTypes.php
new file mode 100644
index 000000000..cb51db6d6
--- /dev/null
+++ b/app/Enums/BuildPackTypes.php
@@ -0,0 +1,11 @@
+ 1,
+ self::ADMIN => 2,
+ self::OWNER => 3,
+ };
+ }
+
+ public function lt(Role|string $role): bool
+ {
+ if (is_string($role)) {
+ $role = Role::from($role);
+ }
+
+ return $this->rank() < $role->rank();
+ }
+
+ public function gt(Role|string $role): bool
+ {
+ if (is_string($role)) {
+ $role = Role::from($role);
+ }
+
+ return $this->rank() > $role->rank();
+ }
+}
diff --git a/app/Enums/StaticImageTypes.php b/app/Enums/StaticImageTypes.php
new file mode 100644
index 000000000..5f5304bf8
--- /dev/null
+++ b/app/Enums/StaticImageTypes.php
@@ -0,0 +1,8 @@
+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/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php
new file mode 100644
index 000000000..b457dc6a0
--- /dev/null
+++ b/app/Events/DatabaseProxyStopped.php
@@ -0,0 +1,35 @@
+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/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php
index 190983c80..913b21bc2 100644
--- a/app/Events/DatabaseStatusChanged.php
+++ b/app/Events/DatabaseStatusChanged.php
@@ -7,28 +7,34 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Auth;
class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
- public $userId;
+ public $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
- $userId = auth()->user()->id ?? null;
+ $userId = Auth::id() ?? null;
}
if (is_null($userId)) {
- throw new \Exception('User id is null');
+ return false;
}
+
$this->userId = $userId;
}
- public function broadcastOn(): array
+ public function broadcastOn(): ?array
{
- return [
- new PrivateChannel("user.{$this->userId}"),
- ];
+ if (! is_null($this->userId)) {
+ return [
+ new PrivateChannel("user.{$this->userId}"),
+ ];
+ }
+
+ return null;
}
}
diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php
new file mode 100644
index 000000000..57004cf4c
--- /dev/null
+++ b/app/Events/FileStorageChanged.php
@@ -0,0 +1,31 @@
+teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Events/ProxyStarted.php b/app/Events/ProxyStarted.php
index ed62eccb1..64d562e0a 100644
--- a/app/Events/ProxyStarted.php
+++ b/app/Events/ProxyStarted.php
@@ -10,8 +10,5 @@ class ProxyStarted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
- public function __construct(public $data)
- {
-
- }
+ public function __construct(public $data) {}
}
diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php
new file mode 100644
index 000000000..c8b5547f6
--- /dev/null
+++ b/app/Events/ScheduledTaskDone.php
@@ -0,0 +1,34 @@
+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/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php
index e3e24a248..3950022e1 100644
--- a/app/Events/ServiceStatusChanged.php
+++ b/app/Events/ServiceStatusChanged.php
@@ -7,28 +7,33 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Auth;
class ServiceStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
- public $userId;
+ public ?string $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
- $userId = auth()->user()->id ?? null;
+ $userId = Auth::id() ?? null;
}
if (is_null($userId)) {
- throw new \Exception('User id is null');
+ return false;
}
$this->userId = $userId;
}
- public function broadcastOn(): array
+ public function broadcastOn(): ?array
{
- return [
- new PrivateChannel("user.{$this->userId}"),
- ];
+ if (! is_null($this->userId)) {
+ return [
+ new PrivateChannel("user.{$this->userId}"),
+ ];
+ }
+
+ return null;
}
}
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 254a8df7a..8c89bb07f 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -50,7 +50,7 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 401);
}
- return redirect()->guest($exception->redirectTo() ?? route('login'));
+ return redirect()->guest($exception->redirectTo($request) ?? route('login'));
}
/**
@@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) {
return;
}
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
if ($this->settings->do_not_track) {
return;
}
@@ -84,7 +84,6 @@ class Handler extends ExceptionHandler
if (str($e->getMessage())->contains('No space left on device')) {
return;
}
- ray('reporting to sentry');
Integration::captureUnhandledException($e);
});
}
diff --git a/app/Exceptions/ProcessException.php b/app/Exceptions/ProcessException.php
index 728a0d81b..47eaa6fd8 100644
--- a/app/Exceptions/ProcessException.php
+++ b/app/Exceptions/ProcessException.php
@@ -4,6 +4,4 @@ namespace App\Exceptions;
use Exception;
-class ProcessException extends Exception
-{
-}
+class ProcessException extends Exception {}
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
new file mode 100644
index 000000000..500db3922
--- /dev/null
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -0,0 +1,2813 @@
+user()->currentAccessToken();
+ $application->makeHidden([
+ 'id',
+ ]);
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($application);
+ }
+ $application->makeHidden([
+ 'custom_labels',
+ 'dockerfile',
+ 'docker_compose',
+ 'docker_compose_raw',
+ 'manual_webhook_secret_bitbucket',
+ 'manual_webhook_secret_gitea',
+ 'manual_webhook_secret_github',
+ 'manual_webhook_secret_gitlab',
+ 'private_key_id',
+ 'value',
+ 'real_value',
+ ]);
+
+ return serializeApiResponse($application);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List all applications.',
+ path: '/applications',
+ operationId: 'list-applications',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all applications.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Application')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function applications(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $projects = Project::where('team_id', $teamId)->get();
+ $applications = collect();
+ $applications->push($projects->pluck('applications')->flatten());
+ $applications = $applications->flatten();
+ $applications = $applications->map(function ($application) {
+ return $this->removeSensitiveData($application);
+ });
+
+ return response()->json($applications);
+ }
+
+ #[OA\Post(
+ summary: 'Create (Public)',
+ description: 'Create new application based on a public git repository.',
+ path: '/applications/public',
+ operationId: 'create-public-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
+ 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
+ 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
+ 'install_command' => ['type' => 'string', 'description' => 'The install command.'],
+ 'build_command' => ['type' => 'string', 'description' => 'The build command.'],
+ 'start_command' => ['type' => 'string', 'description' => 'The start command.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
+ 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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']],
+ // 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
+ 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_public_application(Request $request)
+ {
+ return $this->create_application($request, 'public');
+ }
+
+ #[OA\Post(
+ summary: 'Create (Private - GH App)',
+ description: 'Create new application based on a private repository through a Github App.',
+ path: '/applications/private-github-app',
+ operationId: 'create-private-github-app-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
+ 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
+ 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
+ 'install_command' => ['type' => 'string', 'description' => 'The install command.'],
+ 'build_command' => ['type' => 'string', 'description' => 'The build command.'],
+ 'start_command' => ['type' => 'string', 'description' => 'The start command.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
+ 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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.'],
+ 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
+ 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_private_gh_app_application(Request $request)
+ {
+ return $this->create_application($request, 'private-gh-app');
+ }
+
+ #[OA\Post(
+ 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' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'],
+ 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
+ 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
+ 'install_command' => ['type' => 'string', 'description' => 'The install command.'],
+ 'build_command' => ['type' => 'string', 'description' => 'The build command.'],
+ 'start_command' => ['type' => 'string', 'description' => 'The start command.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
+ 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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.'],
+ 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
+ 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_private_deploy_key_application(Request $request)
+ {
+ return $this->create_application($request, 'private-deploy-key');
+ }
+
+ #[OA\Post(
+ summary: 'Create (Dockerfile)',
+ description: 'Create new application based on a simple Dockerfile.',
+ path: '/applications/dockerfile',
+ operationId: 'create-dockerfile-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'dockerfile'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
+ 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_dockerfile_application(Request $request)
+ {
+ return $this->create_application($request, 'dockerfile');
+ }
+
+ #[OA\Post(
+ summary: 'Create (Docker Image)',
+ description: 'Create new application based on a prebuilt docker image',
+ path: '/applications/dockerimage',
+ operationId: 'create-dockerimage-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_registry_image_name', 'ports_exposes'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_dockerimage_application(Request $request)
+ {
+ return $this->create_application($request, 'dockerimage');
+ }
+
+ #[OA\Post(
+ summary: 'Create (Docker Compose)',
+ description: 'Create new application based on a docker-compose file.',
+ path: '/applications/dockercompose',
+ operationId: 'create-dockercompose-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application object that needs to be created.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_compose_raw'],
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application created successfully.',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_dockercompose_application(Request $request)
+ {
+ return $this->create_application($request, 'dockercompose');
+ }
+
+ 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', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'project_uuid' => 'string|required',
+ 'environment_name' => 'string|required',
+ 'server_uuid' => 'string|required',
+ 'destination_uuid' => 'string',
+ ]);
+
+ $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);
+ }
+
+ $serverUuid = $request->server_uuid;
+ $fqdn = $request->domains;
+ $instantDeploy = $request->instant_deploy;
+ $githubAppUuid = $request->github_app_uuid;
+ $useBuildServer = $request->use_build_server;
+ $isStatic = $request->is_static;
+ $customNginxConfiguration = $request->custom_nginx_configuration;
+
+ if (! is_null($customNginxConfiguration)) {
+ if (! isBase64Encoded($customNginxConfiguration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($customNginxConfiguration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
+
+ $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+ $environment = $project->environments()->where('name', $request->environment_name)->first();
+ if (! $environment) {
+ return response()->json(['message' => 'Environment not found.'], 404);
+ }
+ $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $destinations = $server->destinations();
+ if ($destinations->count() == 0) {
+ return response()->json(['message' => 'Server has no destinations.'], 400);
+ }
+ if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
+ return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
+ }
+ $destination = $destinations->first();
+ if ($type === 'public') {
+ if (! $request->has('name')) {
+ $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
+ }
+ if ($request->build_pack === 'dockercompose') {
+ $request->offsetSet('ports_exposes', '80');
+ }
+ $validationRules = [
+ 'git_repository' => 'string|required',
+ 'git_branch' => 'string|required',
+ 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
+ 'docker_compose_location' => 'string',
+ 'docker_compose_raw' => 'string|nullable',
+ 'docker_compose_domains' => 'array|nullable',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+
+ $application = new Application;
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $application->fill($request->all());
+ $dockerComposeDomainsJson = collect();
+ if ($request->has('docker_compose_domains')) {
+ $dockerComposeDomains = collect($request->docker_compose_domains);
+ if ($dockerComposeDomains->count() > 0) {
+ $dockerComposeDomains->each(function ($domain, $key) use ($dockerComposeDomainsJson) {
+ $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
+ });
+ }
+ $request->offsetUnset('docker_compose_domains');
+ }
+ if ($dockerComposeDomainsJson->count() > 0) {
+ $application->docker_compose_domains = $dockerComposeDomainsJson;
+ }
+
+ $application->fqdn = $fqdn;
+ $application->destination_id = $destination->id;
+ $application->destination_type = $destination->getMorphClass();
+ $application->environment_id = $environment->id;
+ $application->save();
+ if (isset($isStatic)) {
+ $application->settings->is_static = $isStatic;
+ $application->settings->save();
+ }
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
+ $application->refresh();
+ if (! $application->settings->is_container_label_readonly_enabled) {
+ $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->save();
+ }
+ $application->isConfigurationChanged(true);
+
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ no_questions_asked: true,
+ is_api: true,
+ );
+ } else {
+ if ($application->build_pack === 'dockercompose') {
+ LoadComposeFile::dispatch($application);
+ }
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($application, 'uuid'),
+ 'domains' => data_get($application, 'domains'),
+ ]));
+ } elseif ($type === 'private-gh-app') {
+ if (! $request->has('name')) {
+ $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
+ }
+ if ($request->build_pack === 'dockercompose') {
+ $request->offsetSet('ports_exposes', '80');
+ }
+ $validationRules = [
+ 'git_repository' => 'string|required',
+ 'git_branch' => 'string|required',
+ 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
+ 'github_app_uuid' => 'string|required',
+ 'watch_paths' => 'string|nullable',
+ 'docker_compose_location' => 'string',
+ 'docker_compose_raw' => 'string|nullable',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+
+ $validator = customApiValidator($request->all(), $validationRules);
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first();
+ if (! $githubApp) {
+ return response()->json(['message' => 'Github App not found.'], 404);
+ }
+ $gitRepository = $request->git_repository;
+ if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) {
+ $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', '');
+ }
+ $application = new Application;
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $application->fill($request->all());
+
+ $dockerComposeDomainsJson = collect();
+ if ($request->has('docker_compose_domains')) {
+ $yaml = Yaml::parse($application->docker_compose_raw);
+ $services = data_get($yaml, 'services');
+ $dockerComposeDomains = collect($request->docker_compose_domains);
+ if ($dockerComposeDomains->count() > 0) {
+ $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
+ $name = data_get($domain, 'name');
+ if (data_get($services, $name)) {
+ $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
+ }
+ });
+ }
+ $request->offsetUnset('docker_compose_domains');
+ }
+ if ($dockerComposeDomainsJson->count() > 0) {
+ $application->docker_compose_domains = $dockerComposeDomainsJson;
+ }
+ $application->fqdn = $fqdn;
+ $application->git_repository = $gitRepository;
+ $application->destination_id = $destination->id;
+ $application->destination_type = $destination->getMorphClass();
+ $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) {
+ $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->save();
+ }
+ $application->isConfigurationChanged(true);
+
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ no_questions_asked: true,
+ is_api: true,
+ );
+ } else {
+ if ($application->build_pack === 'dockercompose') {
+ LoadComposeFile::dispatch($application);
+ }
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($application, 'uuid'),
+ 'domains' => data_get($application, 'domains'),
+ ]));
+ } elseif ($type === 'private-deploy-key') {
+ if (! $request->has('name')) {
+ $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
+ }
+ if ($request->build_pack === 'dockercompose') {
+ $request->offsetSet('ports_exposes', '80');
+ }
+
+ $validationRules = [
+ 'git_repository' => 'string|required',
+ 'git_branch' => 'string|required',
+ 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
+ 'private_key_uuid' => 'string|required',
+ 'watch_paths' => 'string|nullable',
+ 'docker_compose_location' => 'string',
+ 'docker_compose_raw' => 'string|nullable',
+ ];
+
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first();
+ if (! $privateKey) {
+ return response()->json(['message' => 'Private Key not found.'], 404);
+ }
+
+ $application = new Application;
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $application->fill($request->all());
+
+ $dockerComposeDomainsJson = collect();
+ if ($request->has('docker_compose_domains')) {
+ $yaml = Yaml::parse($application->docker_compose_raw);
+ $services = data_get($yaml, 'services');
+ $dockerComposeDomains = collect($request->docker_compose_domains);
+ if ($dockerComposeDomains->count() > 0) {
+ $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
+ $name = data_get($domain, 'name');
+ if (data_get($services, $name)) {
+ $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
+ }
+ });
+ }
+ $request->offsetUnset('docker_compose_domains');
+ }
+ if ($dockerComposeDomainsJson->count() > 0) {
+ $application->docker_compose_domains = $dockerComposeDomainsJson;
+ }
+ $application->fqdn = $fqdn;
+ $application->private_key_id = $privateKey->id;
+ $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) {
+ $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->save();
+ }
+ $application->isConfigurationChanged(true);
+
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ no_questions_asked: true,
+ is_api: true,
+ );
+ } else {
+ if ($application->build_pack === 'dockercompose') {
+ LoadComposeFile::dispatch($application);
+ }
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($application, 'uuid'),
+ 'domains' => data_get($application, 'domains'),
+ ]));
+ } elseif ($type === 'dockerfile') {
+ if (! $request->has('name')) {
+ $request->offsetSet('name', 'dockerfile-'.new Cuid2);
+ }
+
+ $validationRules = [
+ 'dockerfile' => 'string|required',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ if (! isBase64Encoded($request->dockerfile)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'dockerfile' => 'The dockerfile should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerFile = base64_decode($request->dockerfile);
+ if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'dockerfile' => 'The dockerfile should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerFile = base64_decode($request->dockerfile);
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $port = get_port_from_dockerfile($request->dockerfile);
+ if (! $port) {
+ $port = 80;
+ }
+
+ $application = new Application;
+ $application->fill($request->all());
+ $application->fqdn = $fqdn;
+ $application->ports_exposes = $port;
+ $application->build_pack = 'dockerfile';
+ $application->dockerfile = $dockerFile;
+ $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';
+ $application->save();
+ $application->refresh();
+ if (! $application->settings->is_container_label_readonly_enabled) {
+ $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->save();
+ }
+ $application->isConfigurationChanged(true);
+
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ no_questions_asked: true,
+ is_api: true,
+ );
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($application, 'uuid'),
+ 'domains' => data_get($application, 'domains'),
+ ]));
+ } elseif ($type === 'dockerimage') {
+ if (! $request->has('name')) {
+ $request->offsetSet('name', 'docker-image-'.new Cuid2);
+ }
+ $validationRules = [
+ 'docker_registry_image_name' => 'string|required',
+ 'docker_registry_image_tag' => 'string',
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ if (! $request->docker_registry_image_tag) {
+ $request->offsetSet('docker_registry_image_tag', 'latest');
+ }
+ $application = new Application;
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $application->fill($request->all());
+ $application->fqdn = $fqdn;
+ $application->build_pack = 'dockerimage';
+ $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';
+ $application->save();
+ $application->refresh();
+ if (! $application->settings->is_container_label_readonly_enabled) {
+ $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->save();
+ }
+ $application->isConfigurationChanged(true);
+
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ no_questions_asked: true,
+ is_api: true,
+ );
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($application, 'uuid'),
+ 'domains' => data_get($application, 'domains'),
+ ]));
+ } elseif ($type === 'dockercompose') {
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw'];
+
+ $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);
+ }
+ if (! $request->has('name')) {
+ $request->offsetSet('name', 'service'.new Cuid2);
+ }
+ $validationRules = [
+ 'docker_compose_raw' => 'string|required',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ if (! isBase64Encoded($request->docker_compose_raw)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerComposeRaw = base64_decode($request->docker_compose_raw);
+ if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerCompose = base64_decode($request->docker_compose_raw);
+ $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+
+ // $isValid = validateComposeFile($dockerComposeRaw, $server_id);
+ // if ($isValid !== 'OK') {
+ // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
+ // }
+
+ $service = new Service;
+ removeUnnecessaryFieldsFromRequest($request);
+ $service->fill($request->all());
+
+ $service->docker_compose_raw = $dockerComposeRaw;
+ $service->environment_id = $environment->id;
+ $service->server_id = $server->id;
+ $service->destination_id = $destination->id;
+ $service->destination_type = $destination->getMorphClass();
+ $service->save();
+
+ $service->name = "service-$service->uuid";
+ $service->parse(isNew: true);
+ if ($instantDeploy) {
+ StartService::dispatch($service)->onQueue('high');
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => data_get($service, 'uuid'),
+ 'domains' => data_get($service, 'domains'),
+ ]));
+ }
+
+ return response()->json(['message' => 'Invalid type.'], 400);
+ }
+
+ #[OA\Get(
+ summary: 'Get',
+ description: 'Get application by UUID.',
+ path: '/applications/{uuid}',
+ operationId: 'get-application-by-uuid',
+ 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',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get application by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ ref: '#/components/schemas/Application'
+ )
+ ),
+ ]),
+ 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 application_by_uuid(Request $request)
+ {
+ $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 response()->json($this->removeSensitiveData($application));
+ }
+
+ #[OA\Delete(
+ summary: 'Delete',
+ description: 'Delete application by UUID.',
+ path: '/applications/{uuid}',
+ operationId: 'delete-application-by-uuid',
+ 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',
+ )
+ ),
+ 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: 'Application deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Application 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_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ $cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 404);
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+
+ DeleteResourceJob::dispatch(
+ resource: $application,
+ 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)
+ )->onQueue('high');
+
+ return response()->json([
+ 'message' => 'Application deletion request queued.',
+ ]);
+ }
+
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update application by UUID.',
+ path: '/applications/{uuid}',
+ operationId: 'update-application-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ requestBody: new OA\RequestBody(
+ description: 'Application updated.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
+ 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
+ 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'description' => ['type' => 'string', 'description' => 'The application description.'],
+ 'domains' => ['type' => 'string', 'description' => 'The application domains.'],
+ 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
+ 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'install_command' => ['type' => 'string', 'description' => 'The install command.'],
+ 'build_command' => ['type' => 'string', 'description' => 'The build command.'],
+ 'start_command' => ['type' => 'string', 'description' => 'The start command.'],
+ 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
+ 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'],
+ '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.'],
+ 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
+ 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
+ '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.'],
+ ],
+ )),
+ ]),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Application updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['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 update_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ if ($request->collect()->count() == 0) {
+ return response()->json([
+ 'message' => 'Invalid request.',
+ ], 400);
+ }
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+ $server = $application->destination->server;
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
+
+ $validationRules = [
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'static_image' => 'string',
+ 'watch_paths' => 'string|nullable',
+ 'docker_compose_location' => 'string',
+ 'docker_compose_raw' => 'string|nullable',
+ 'docker_compose_domains' => 'array|nullable',
+ 'docker_compose_custom_start_command' => 'string|nullable',
+ 'docker_compose_custom_build_command' => 'string|nullable',
+ 'custom_nginx_configuration' => 'string|nullable',
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
+ // Validate ports_exposes
+ if ($request->has('ports_exposes')) {
+ $ports = explode(',', $request->ports_exposes);
+ foreach ($ports as $port) {
+ if (! is_numeric($port)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.',
+ ],
+ ], 422);
+ }
+ }
+ }
+ if ($request->has('custom_nginx_configuration')) {
+ if (! isBase64Encoded($request->custom_nginx_configuration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($request->custom_nginx_configuration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
+ $return = $this->validateDataApplications($request, $server);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $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);
+ }
+ $domains = $request->domains;
+ if ($request->has('domains') && $server->isProxyShouldRun()) {
+ $errors = [];
+ $fqdn = $request->domains;
+ $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $application->fqdn = $fqdn;
+ if (! $application->settings->is_container_label_readonly_enabled) {
+ $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
+ $application->custom_labels = base64_encode($customLabels);
+ }
+ $request->offsetUnset('domains');
+ }
+
+ $dockerComposeDomainsJson = collect();
+ if ($request->has('docker_compose_domains')) {
+ $yaml = Yaml::parse($application->docker_compose_raw);
+ $services = data_get($yaml, 'services');
+ $dockerComposeDomains = collect($request->docker_compose_domains);
+ if ($dockerComposeDomains->count() > 0) {
+ $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
+ $name = data_get($domain, 'name');
+ if (data_get($services, $name)) {
+ $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
+ }
+ });
+ }
+ $request->offsetUnset('docker_compose_domains');
+ }
+ $instantDeploy = $request->instant_deploy;
+ $isStatic = $request->is_static;
+ $useBuildServer = $request->use_build_server;
+
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
+
+ if (isset($isStatic)) {
+ $application->settings->is_static = $isStatic;
+ $application->settings->save();
+ }
+
+ removeUnnecessaryFieldsFromRequest($request);
+
+ $data = $request->all();
+ data_set($data, 'fqdn', $domains);
+ if ($dockerComposeDomainsJson->count() > 0) {
+ data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
+ }
+ $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,
+ ]);
+ }
+
+ #[OA\Get(
+ summary: 'List Envs',
+ description: 'List all envs by application UUID.',
+ path: '/applications/{uuid}/envs',
+ operationId: 'list-envs-by-application-uuid',
+ 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',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'All environment variables by application 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();
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+ $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id'));
+
+ $envs = $envs->map(function ($env) {
+ $env->makeHidden([
+ 'service_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',
+ ]);
+
+ return $this->removeSensitiveData($env);
+ });
+
+ return response()->json($envs);
+ }
+
+ #[OA\Patch(
+ summary: 'Update Env',
+ description: 'Update env by application UUID.',
+ path: '/applications/{uuid}/envs',
+ operationId: 'update-env-by-application-uuid',
+ 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(
+ 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)
+ {
+ $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal'];
+ $teamId = getTeamIdFromToken();
+
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+ $validator = customApiValidator($request->all(), [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_preview' => 'boolean',
+ 'is_build_time' => 'boolean',
+ 'is_literal' => 'boolean',
+ 'is_multiline' => 'boolean',
+ 'is_shown_once' => 'boolean',
+ ]);
+
+ $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);
+ }
+ $is_preview = $request->is_preview ?? false;
+ $is_build_time = $request->is_build_time ?? false;
+ $is_literal = $request->is_literal ?? false;
+ if ($is_preview) {
+ $env = $application->environment_variables_preview->where('key', $request->key)->first();
+ if ($env) {
+ $env->value = $request->value;
+ if ($env->is_build_time != $is_build_time) {
+ $env->is_build_time = $is_build_time;
+ }
+ if ($env->is_literal != $is_literal) {
+ $env->is_literal = $is_literal;
+ }
+ if ($env->is_preview != $is_preview) {
+ $env->is_preview = $is_preview;
+ }
+ if ($env->is_multiline != $request->is_multiline) {
+ $env->is_multiline = $request->is_multiline;
+ }
+ if ($env->is_shown_once != $request->is_shown_once) {
+ $env->is_shown_once = $request->is_shown_once;
+ }
+ $env->save();
+
+ return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
+ } else {
+ return response()->json([
+ 'message' => 'Environment variable not found.',
+ ], 404);
+ }
+ } else {
+ $env = $application->environment_variables->where('key', $request->key)->first();
+ if ($env) {
+ $env->value = $request->value;
+ if ($env->is_build_time != $is_build_time) {
+ $env->is_build_time = $is_build_time;
+ }
+ if ($env->is_literal != $is_literal) {
+ $env->is_literal = $is_literal;
+ }
+ if ($env->is_preview != $is_preview) {
+ $env->is_preview = $is_preview;
+ }
+ if ($env->is_multiline != $request->is_multiline) {
+ $env->is_multiline = $request->is_multiline;
+ }
+ if ($env->is_shown_once != $request->is_shown_once) {
+ $env->is_shown_once = $request->is_shown_once;
+ }
+ $env->save();
+
+ return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
+ } else {
+ return response()->json([
+ 'message' => 'Environment variable not found.',
+ ], 404);
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Something is not okay. Are you okay?',
+ ], 500);
+ }
+
+ #[OA\Patch(
+ summary: 'Update Envs (Bulk)',
+ description: 'Update multiple envs by application UUID.',
+ path: '/applications/{uuid}/envs/bulk',
+ operationId: 'update-envs-by-application-uuid',
+ 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(
+ 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();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+
+ $bulk_data = $request->get('data');
+ if (! $bulk_data) {
+ return response()->json([
+ 'message' => 'Bulk data is required.',
+ ], 400);
+ }
+ $bulk_data = collect($bulk_data)->map(function ($item) {
+ return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']);
+ });
+ foreach ($bulk_data as $item) {
+ $validator = customApiValidator($item, [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_preview' => 'boolean',
+ '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);
+ }
+ $is_preview = $item->get('is_preview') ?? false;
+ $is_build_time = $item->get('is_build_time') ?? false;
+ $is_literal = $item->get('is_literal') ?? false;
+ $is_multi_line = $item->get('is_multiline') ?? false;
+ $is_shown_once = $item->get('is_shown_once') ?? false;
+ if ($is_preview) {
+ $env = $application->environment_variables_preview->where('key', $item->get('key'))->first();
+ if ($env) {
+ $env->value = $item->get('value');
+ if ($env->is_build_time != $is_build_time) {
+ $env->is_build_time = $is_build_time;
+ }
+ if ($env->is_literal != $is_literal) {
+ $env->is_literal = $is_literal;
+ }
+ if ($env->is_multiline != $item->get('is_multiline')) {
+ $env->is_multiline = $item->get('is_multiline');
+ }
+ if ($env->is_shown_once != $item->get('is_shown_once')) {
+ $env->is_shown_once = $item->get('is_shown_once');
+ }
+ $env->save();
+ } else {
+ $env = $application->environment_variables()->create([
+ 'key' => $item->get('key'),
+ 'value' => $item->get('value'),
+ 'is_preview' => $is_preview,
+ 'is_build_time' => $is_build_time,
+ 'is_literal' => $is_literal,
+ 'is_multiline' => $is_multi_line,
+ 'is_shown_once' => $is_shown_once,
+ ]);
+ }
+ } else {
+ $env = $application->environment_variables->where('key', $item->get('key'))->first();
+ if ($env) {
+ $env->value = $item->get('value');
+ if ($env->is_build_time != $is_build_time) {
+ $env->is_build_time = $is_build_time;
+ }
+ if ($env->is_literal != $is_literal) {
+ $env->is_literal = $is_literal;
+ }
+ if ($env->is_multiline != $item->get('is_multiline')) {
+ $env->is_multiline = $item->get('is_multiline');
+ }
+ if ($env->is_shown_once != $item->get('is_shown_once')) {
+ $env->is_shown_once = $item->get('is_shown_once');
+ }
+ $env->save();
+ } else {
+ $env = $application->environment_variables()->create([
+ 'key' => $item->get('key'),
+ 'value' => $item->get('value'),
+ 'is_preview' => $is_preview,
+ 'is_build_time' => $is_build_time,
+ 'is_literal' => $is_literal,
+ 'is_multiline' => $is_multi_line,
+ 'is_shown_once' => $is_shown_once,
+ ]);
+ }
+ }
+ }
+
+ return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
+ }
+
+ #[OA\Post(
+ summary: 'Create Env',
+ description: 'Create env by application UUID.',
+ path: '/applications/{uuid}/envs',
+ operationId: 'create-env-by-application-uuid',
+ 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: '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)
+ {
+ $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal'];
+ $teamId = getTeamIdFromToken();
+
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found',
+ ], 404);
+ }
+ $validator = customApiValidator($request->all(), [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_preview' => 'boolean',
+ 'is_build_time' => 'boolean',
+ 'is_literal' => 'boolean',
+ 'is_multiline' => 'boolean',
+ 'is_shown_once' => 'boolean',
+ ]);
+
+ $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);
+ }
+ $is_preview = $request->is_preview ?? false;
+ if ($is_preview) {
+ $env = $application->environment_variables_preview->where('key', $request->key)->first();
+ if ($env) {
+ return response()->json([
+ 'message' => 'Environment variable already exists. Use PATCH request to update it.',
+ ], 409);
+ } else {
+ $env = $application->environment_variables()->create([
+ 'key' => $request->key,
+ 'value' => $request->value,
+ 'is_preview' => $request->is_preview ?? false,
+ 'is_build_time' => $request->is_build_time ?? false,
+ 'is_literal' => $request->is_literal ?? false,
+ 'is_multiline' => $request->is_multiline ?? false,
+ 'is_shown_once' => $request->is_shown_once ?? false,
+ ]);
+
+ return response()->json([
+ 'uuid' => $env->uuid,
+ ])->setStatusCode(201);
+ }
+ } else {
+ $env = $application->environment_variables->where('key', $request->key)->first();
+ if ($env) {
+ return response()->json([
+ 'message' => 'Environment variable already exists. Use PATCH request to update it.',
+ ], 409);
+ } else {
+ $env = $application->environment_variables()->create([
+ 'key' => $request->key,
+ 'value' => $request->value,
+ 'is_preview' => $request->is_preview ?? false,
+ 'is_build_time' => $request->is_build_time ?? false,
+ 'is_literal' => $request->is_literal ?? false,
+ 'is_multiline' => $request->is_multiline ?? false,
+ 'is_shown_once' => $request->is_shown_once ?? false,
+ ]);
+
+ return response()->json([
+ 'uuid' => $env->uuid,
+ ])->setStatusCode(201);
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Something went wrong.',
+ ], 500);
+ }
+
+ #[OA\Delete(
+ summary: 'Delete Env',
+ description: 'Delete env by UUID.',
+ path: '/applications/{uuid}/envs/{env_uuid}',
+ operationId: 'delete-env-by-application-uuid',
+ 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',
+ )
+ ),
+ 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();
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+
+ if (! $application) {
+ return response()->json([
+ 'message' => 'Application not found.',
+ ], 404);
+ }
+ $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first();
+ if (! $found_env) {
+ return response()->json([
+ 'message' => 'Environment variable not found.',
+ ], 404);
+ }
+ $found_env->forceDelete();
+
+ return response()->json([
+ 'message' => 'Environment variable deleted.',
+ ]);
+ }
+
+ #[OA\Get(
+ summary: 'Start',
+ description: 'Start application. `Post` request is also accepted.',
+ path: '/applications/{uuid}/start',
+ operationId: 'start-application-by-uuid',
+ 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',
+ )
+ ),
+ new OA\Parameter(
+ name: 'force',
+ in: 'query',
+ description: 'Force rebuild.',
+ schema: new OA\Schema(
+ type: 'boolean',
+ default: false,
+ )
+ ),
+ new OA\Parameter(
+ name: 'instant_deploy',
+ in: 'query',
+ description: 'Instant deploy (skip queuing).',
+ schema: new OA\Schema(
+ type: 'boolean',
+ default: false,
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Start application.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
+ 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
+ ])
+ ),
+ ]),
+ 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 action_deploy(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $force = $request->query->get('force') ?? false;
+ $instant_deploy = $request->query->get('instant_deploy') ?? false;
+ $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);
+ }
+
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: $force,
+ is_api: true,
+ no_questions_asked: $instant_deploy
+ );
+
+ return response()->json(
+ [
+ 'message' => 'Deployment request queued.',
+ 'deployment_uuid' => $deployment_uuid->toString(),
+ ],
+ 200
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Stop',
+ description: 'Stop application. `Post` request is also accepted.',
+ path: '/applications/{uuid}/stop',
+ operationId: 'stop-application-by-uuid',
+ 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',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Stop application.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Application stopping request queued.'],
+ ]
+ )
+ ),
+ ]),
+ 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 action_stop(Request $request)
+ {
+ $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);
+ }
+ StopApplication::dispatch($application)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Application stopping request queued.',
+ ],
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Restart',
+ description: 'Restart application. `Post` request is also accepted.',
+ path: '/applications/{uuid}/restart',
+ operationId: 'restart-application-by-uuid',
+ 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',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Restart application.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Restart request queued.'],
+ 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
+ ]
+ )
+ ),
+ ]),
+
+ 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 action_restart(Request $request)
+ {
+ $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);
+ }
+
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ restart_only: true,
+ is_api: true,
+ );
+
+ return response()->json(
+ [
+ 'message' => 'Restart request queued.',
+ 'deployment_uuid' => $deployment_uuid->toString(),
+ ],
+ );
+ }
+
+ #[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();
+
+ // Validate ports_mappings
+ if ($request->has('ports_mappings')) {
+ $ports = [];
+ foreach (explode(',', $request->ports_mappings) as $portMapping) {
+ $port = explode(':', $portMapping);
+ if (in_array($port[0], $ports)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'ports_mappings' => 'The first number before : should be unique between mappings.',
+ ],
+ ], 422);
+ }
+ $ports[] = $port[0];
+ }
+ }
+ // Validate custom_labels
+ if ($request->has('custom_labels')) {
+ if (! isBase64Encoded($request->custom_labels)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customLabels = base64_decode($request->custom_labels);
+ if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_labels' => 'The custom_labels should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
+ if ($request->has('domains') && $server->isProxyShouldRun()) {
+ $uuid = $request->uuid;
+ $fqdn = $request->domains;
+ $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
+
+ return str($domain)->trim()->lower();
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
new file mode 100644
index 000000000..eaa542a83
--- /dev/null
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -0,0 +1,1827 @@
+user()->currentAccessToken();
+ $database->makeHidden([
+ 'id',
+ 'laravel_through_key',
+ ]);
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($database);
+ }
+
+ $database->makeHidden([
+ 'internal_db_url',
+ 'external_db_url',
+ 'postgres_password',
+ 'dragonfly_password',
+ 'redis_password',
+ 'mongo_initdb_root_password',
+ 'keydb_password',
+ 'clickhouse_admin_password',
+ ]);
+
+ return serializeApiResponse($database);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List all databases.',
+ path: '/databases',
+ operationId: 'list-databases',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all databases',
+ content: new OA\JsonContent(
+ type: 'string',
+ example: 'Content is very complex. Will be implemented later.',
+ ),
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function databases(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $projects = Project::where('team_id', $teamId)->get();
+ $databases = collect();
+ foreach ($projects as $project) {
+ $databases = $databases->merge($project->databases());
+ }
+ $databases = $databases->map(function ($database) {
+ return $this->removeSensitiveData($database);
+ });
+
+ return response()->json($databases);
+ }
+
+ #[OA\Get(
+ summary: 'Get',
+ description: 'Get database by UUID.',
+ path: '/databases/{uuid}',
+ operationId: 'get-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all databases',
+ content: new OA\JsonContent(
+ type: 'string',
+ example: 'Content is very complex. Will be implemented later.',
+ ),
+ ),
+ 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 database_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 404);
+ }
+ $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+
+ return response()->json($this->removeSensitiveData($database));
+ }
+
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update database by UUID.',
+ path: '/databases/{uuid}',
+ operationId: 'update-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'],
+ 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'],
+ 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'],
+ 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'],
+ 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'],
+ 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'],
+ 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'],
+ 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'],
+ 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'],
+ 'redis_password' => ['type' => 'string', 'description' => 'Redis password'],
+ 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'],
+ 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'],
+ 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'],
+ 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'],
+ 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'],
+ 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'],
+ 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'],
+ 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'],
+ 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
+ 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
+ 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
+ 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
+ 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
+ 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
+ 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database 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_by_uuid(Request $request)
+ {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'image' => 'string',
+ 'is_public' => 'boolean',
+ 'public_port' => 'numeric|nullable',
+ 'limits_memory' => 'string',
+ 'limits_memory_swap' => 'string',
+ 'limits_memory_swappiness' => 'numeric',
+ 'limits_memory_reservation' => 'string',
+ 'limits_cpus' => 'string',
+ 'limits_cpuset' => 'string|nullable',
+ 'limits_cpu_shares' => 'numeric',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ $uuid = $request->uuid;
+ removeUnnecessaryFieldsFromRequest($request);
+ $database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+ if ($request->is_public && $request->public_port) {
+ if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) {
+ return response()->json(['message' => 'Public port already used by another database.'], 400);
+ }
+ }
+ switch ($database->type()) {
+ case 'standalone-postgresql':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'postgres_user' => 'string',
+ 'postgres_password' => 'string',
+ 'postgres_db' => 'string',
+ 'postgres_initdb_args' => 'string',
+ 'postgres_host_auth_method' => 'string',
+ 'postgres_conf' => 'string',
+ ]);
+ if ($request->has('postgres_conf')) {
+ if (! isBase64Encoded($request->postgres_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'postgres_conf' => 'The postgres_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $postgresConf = base64_decode($request->postgres_conf);
+ if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'postgres_conf' => 'The postgres_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('postgres_conf', $postgresConf);
+ }
+ break;
+ case 'standalone-clickhouse':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
+ $validator = customApiValidator($request->all(), [
+ 'clickhouse_admin_user' => 'string',
+ 'clickhouse_admin_password' => 'string',
+ ]);
+ break;
+ case 'standalone-dragonfly':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
+ $validator = customApiValidator($request->all(), [
+ 'dragonfly_password' => 'string',
+ ]);
+ break;
+ case 'standalone-redis':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'redis_password' => 'string',
+ 'redis_conf' => 'string',
+ ]);
+ if ($request->has('redis_conf')) {
+ if (! isBase64Encoded($request->redis_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'redis_conf' => 'The redis_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $redisConf = base64_decode($request->redis_conf);
+ if (mb_detect_encoding($redisConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'redis_conf' => 'The redis_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('redis_conf', $redisConf);
+ }
+ break;
+ case 'standalone-keydb':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'keydb_password' => 'string',
+ 'keydb_conf' => 'string',
+ ]);
+ if ($request->has('keydb_conf')) {
+ if (! isBase64Encoded($request->keydb_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'keydb_conf' => 'The keydb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $keydbConf = base64_decode($request->keydb_conf);
+ if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'keydb_conf' => 'The keydb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('keydb_conf', $keydbConf);
+ }
+ break;
+ case 'standalone-mariadb':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
+ $validator = customApiValidator($request->all(), [
+ 'mariadb_conf' => 'string',
+ 'mariadb_root_password' => 'string',
+ 'mariadb_user' => 'string',
+ 'mariadb_password' => 'string',
+ 'mariadb_database' => 'string',
+ ]);
+ if ($request->has('mariadb_conf')) {
+ if (! isBase64Encoded($request->mariadb_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mariadbConf = base64_decode($request->mariadb_conf);
+ if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mariadb_conf', $mariadbConf);
+ }
+ break;
+ case 'standalone-mongodb':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $validator = customApiValidator($request->all(), [
+ 'mongo_conf' => 'string',
+ 'mongo_initdb_root_username' => 'string',
+ 'mongo_initdb_root_password' => 'string',
+ 'mongo_initdb_init_database' => 'string',
+ ]);
+ if ($request->has('mongo_conf')) {
+ if (! isBase64Encoded($request->mongo_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mongo_conf' => 'The mongo_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mongoConf = base64_decode($request->mongo_conf);
+ if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mongo_conf' => 'The mongo_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mongo_conf', $mongoConf);
+ }
+
+ break;
+ case 'standalone-mysql':
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'mysql_root_password' => 'string',
+ 'mysql_user' => 'string',
+ 'mysql_database' => 'string',
+ 'mysql_conf' => 'string',
+ ]);
+ if ($request->has('mysql_conf')) {
+ if (! isBase64Encoded($request->mysql_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mysql_conf' => 'The mysql_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mysqlConf = base64_decode($request->mysql_conf);
+ if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mysql_conf' => 'The mysql_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mysql_conf', $mysqlConf);
+ }
+ break;
+ }
+ $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);
+ }
+ $whatToDoWithDatabaseProxy = null;
+ if ($request->is_public === false && $database->is_public === true) {
+ $whatToDoWithDatabaseProxy = 'stop';
+ }
+ if ($request->is_public === true && $request->public_port && $database->is_public === false) {
+ $whatToDoWithDatabaseProxy = 'start';
+ }
+
+ $database->update($request->all());
+
+ if ($whatToDoWithDatabaseProxy === 'start') {
+ StartDatabaseProxy::dispatch($database)->onQueue('high');
+ } elseif ($whatToDoWithDatabaseProxy === 'stop') {
+ StopDatabaseProxy::dispatch($database)->onQueue('high');
+ }
+
+ return response()->json([
+ 'message' => 'Database updated.',
+ ]);
+ }
+
+ #[OA\Post(
+ summary: 'Create (PostgreSQL)',
+ description: 'Create a new PostgreSQL database.',
+ path: '/databases/postgresql',
+ operationId: 'create-database-postgresql',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'],
+ 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'],
+ 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'],
+ 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'],
+ 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'],
+ 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_postgresql(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::POSTGRESQL);
+ }
+
+ #[OA\Post(
+ summary: 'Create (Clickhouse)',
+ description: 'Create a new Clickhouse database.',
+ path: '/databases/clickhouse',
+ operationId: 'create-database-clickhouse',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'],
+ 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_clickhouse(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::CLICKHOUSE);
+ }
+
+ #[OA\Post(
+ summary: 'Create (DragonFly)',
+ description: 'Create a new DragonFly database.',
+ path: '/databases/dragonfly',
+ operationId: 'create-database-dragonfly',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_dragonfly(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::DRAGONFLY);
+ }
+
+ #[OA\Post(
+ summary: 'Create (Redis)',
+ description: 'Create a new Redis database.',
+ path: '/databases/redis',
+ operationId: 'create-database-redis',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'redis_password' => ['type' => 'string', 'description' => 'Redis password'],
+ 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_redis(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::REDIS);
+ }
+
+ #[OA\Post(
+ summary: 'Create (KeyDB)',
+ description: 'Create a new KeyDB database.',
+ path: '/databases/keydb',
+ operationId: 'create-database-keydb',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'],
+ 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_keydb(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::KEYDB);
+ }
+
+ #[OA\Post(
+ summary: 'Create (MariaDB)',
+ description: 'Create a new MariaDB database.',
+ path: '/databases/mariadb',
+ operationId: 'create-database-mariadb',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'],
+ 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'],
+ 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'],
+ 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'],
+ 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_mariadb(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::MARIADB);
+ }
+
+ #[OA\Post(
+ summary: 'Create (MySQL)',
+ description: 'Create a new MySQL database.',
+ path: '/databases/mysql',
+ operationId: 'create-database-mysql',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
+ 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
+ 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_mysql(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::MYSQL);
+ }
+
+ #[OA\Post(
+ summary: 'Create (MongoDB)',
+ description: 'Create a new MongoDB database.',
+ path: '/databases/mongodb',
+ operationId: 'create-database-mongodb',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+
+ requestBody: new OA\RequestBody(
+ description: 'Database data',
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name'],
+ properties: [
+ 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
+ 'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'],
+ 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'],
+ 'name' => ['type' => 'string', 'description' => 'Name of the database'],
+ 'description' => ['type' => 'string', 'description' => 'Description of the database'],
+ 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
+ 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
+ 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
+ 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
+ ],
+ ),
+ )
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Database updated',
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_database_mongodb(Request $request)
+ {
+ return $this->create_database($request, NewDatabaseTypes::MONGODB);
+ }
+
+ public function create_database(Request $request, NewDatabaseTypes $type)
+ {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+
+ $extraFields = array_diff(array_keys($request->all()), $allowedFields);
+ if (! empty($extraFields)) {
+ $errors = collect([]);
+ if (! empty($extraFields)) {
+ foreach ($extraFields as $field) {
+ $errors->add($field, 'This field is not allowed.');
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ $serverUuid = $request->server_uuid;
+ $instantDeploy = $request->instant_deploy ?? false;
+ if ($request->is_public && ! $request->public_port) {
+ $request->offsetSet('is_public', false);
+ }
+ $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+ $environment = $project->environments()->where('name', $request->environment_name)->first();
+ if (! $environment) {
+ return response()->json(['message' => 'Environment not found.'], 404);
+ }
+ $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $destinations = $server->destinations();
+ if ($destinations->count() == 0) {
+ return response()->json(['message' => 'Server has no destinations.'], 400);
+ }
+ if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
+ return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
+ }
+ $destination = $destinations->first();
+ if ($request->has('public_port') && $request->is_public) {
+ if (isPublicPortAlreadyUsed($server, $request->public_port)) {
+ return response()->json(['message' => 'Public port already used by another database.'], 400);
+ }
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'image' => 'string',
+ 'project_uuid' => 'string|required',
+ 'environment_name' => 'string|required',
+ 'server_uuid' => 'string|required',
+ 'destination_uuid' => 'string',
+ 'is_public' => 'boolean',
+ 'public_port' => 'numeric|nullable',
+ 'limits_memory' => 'string',
+ 'limits_memory_swap' => 'string',
+ 'limits_memory_swappiness' => 'numeric',
+ 'limits_memory_reservation' => 'string',
+ 'limits_cpus' => 'string',
+ 'limits_cpuset' => 'string|nullable',
+ 'limits_cpu_shares' => 'numeric',
+ 'instant_deploy' => 'boolean',
+ ]);
+ if ($validator->failed()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+ if ($request->public_port) {
+ if ($request->public_port < 1024 || $request->public_port > 65535) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'public_port' => 'The public port should be between 1024 and 65535.',
+ ],
+ ], 422);
+ }
+ }
+ if ($type === NewDatabaseTypes::POSTGRESQL) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'postgres_user' => 'string',
+ 'postgres_password' => 'string',
+ 'postgres_db' => 'string',
+ 'postgres_initdb_args' => 'string',
+ 'postgres_host_auth_method' => 'string',
+ 'postgres_conf' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('postgres_conf')) {
+ if (! isBase64Encoded($request->postgres_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'postgres_conf' => 'The postgres_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $postgresConf = base64_decode($request->postgres_conf);
+ if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'postgres_conf' => 'The postgres_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('postgres_conf', $postgresConf);
+ }
+ $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::MARIADB) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
+ $validator = customApiValidator($request->all(), [
+ 'clickhouse_admin_user' => 'string',
+ 'clickhouse_admin_password' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('mariadb_conf')) {
+ if (! isBase64Encoded($request->mariadb_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mariadbConf = base64_decode($request->mariadb_conf);
+ if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mariadb_conf', $mariadbConf);
+ }
+ $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::MYSQL) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'mysql_root_password' => 'string',
+ 'mysql_user' => 'string',
+ 'mysql_database' => 'string',
+ 'mysql_conf' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('mysql_conf')) {
+ if (! isBase64Encoded($request->mysql_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mysql_conf' => 'The mysql_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mysqlConf = base64_decode($request->mysql_conf);
+ if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mysql_conf' => 'The mysql_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mysql_conf', $mysqlConf);
+ }
+ $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::REDIS) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'redis_password' => 'string',
+ 'redis_conf' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('redis_conf')) {
+ if (! isBase64Encoded($request->redis_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'redis_conf' => 'The redis_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $redisConf = base64_decode($request->redis_conf);
+ if (mb_detect_encoding($redisConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'redis_conf' => 'The redis_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('redis_conf', $redisConf);
+ }
+ $database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::DRAGONFLY) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
+ $validator = customApiValidator($request->all(), [
+ 'dragonfly_password' => 'string',
+ ]);
+
+ $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);
+ }
+
+ removeUnnecessaryFieldsFromRequest($request);
+ $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => $database->uuid,
+ ]))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::KEYDB) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
+ $validator = customApiValidator($request->all(), [
+ 'keydb_password' => 'string',
+ 'keydb_conf' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('keydb_conf')) {
+ if (! isBase64Encoded($request->keydb_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'keydb_conf' => 'The keydb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $keydbConf = base64_decode($request->keydb_conf);
+ if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'keydb_conf' => 'The keydb_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('keydb_conf', $keydbConf);
+ }
+ $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
+ $validator = customApiValidator($request->all(), [
+ 'clickhouse_admin_user' => 'string',
+ 'clickhouse_admin_password' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ } elseif ($type === NewDatabaseTypes::MONGODB) {
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $validator = customApiValidator($request->all(), [
+ 'mongo_conf' => 'string',
+ 'mongo_initdb_root_username' => 'string',
+ 'mongo_initdb_root_password' => 'string',
+ 'mongo_initdb_init_database' => 'string',
+ ]);
+ $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);
+ }
+ removeUnnecessaryFieldsFromRequest($request);
+ if ($request->has('mongo_conf')) {
+ if (! isBase64Encoded($request->mongo_conf)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mongo_conf' => 'The mongo_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $mongoConf = base64_decode($request->mongo_conf);
+ if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'mongo_conf' => 'The mongo_conf should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $request->offsetSet('mongo_conf', $mongoConf);
+ }
+ $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
+ if ($instantDeploy) {
+ StartDatabase::dispatch($database)->onQueue('high');
+ }
+
+ $database->refresh();
+ $payload = [
+ 'uuid' => $database->uuid,
+ 'internal_db_url' => $database->internal_db_url,
+ ];
+ if ($database->is_public && $database->public_port) {
+ $payload['external_db_url'] = $database->external_db_url;
+ }
+
+ return response()->json(serializeApiResponse($payload))->setStatusCode(201);
+ }
+
+ return response()->json(['message' => 'Invalid database type requested.'], 400);
+ }
+
+ #[OA\Delete(
+ summary: 'Delete',
+ description: 'Delete database by UUID.',
+ path: '/databases/{uuid}',
+ operationId: 'delete-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ 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: 'Database deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Database 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_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ $cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 404);
+ }
+ $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+
+ DeleteResourceJob::dispatch(
+ resource: $database,
+ 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)
+ )->onQueue('high');
+
+ return response()->json([
+ 'message' => 'Database deletion request queued.',
+ ]);
+ }
+
+ #[OA\Get(
+ summary: 'Start',
+ description: 'Start database. `Post` request is also accepted.',
+ path: '/databases/{uuid}/start',
+ operationId: 'start-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Start database.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
+ ])
+ ),
+ ]),
+ 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 action_deploy(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+ if (str($database->status)->contains('running')) {
+ return response()->json(['message' => 'Database is already running.'], 400);
+ }
+ StartDatabase::dispatch($database)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Database starting request queued.',
+ ],
+ 200
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Stop',
+ description: 'Stop database. `Post` request is also accepted.',
+ path: '/databases/{uuid}/stop',
+ operationId: 'stop-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Stop database.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
+ ])
+ ),
+ ]),
+ 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 action_stop(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+ if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) {
+ return response()->json(['message' => 'Database is already stopped.'], 400);
+ }
+ StopDatabase::dispatch($database)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Database stopping request queued.',
+ ],
+ 200
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Restart',
+ description: 'Restart database. `Post` request is also accepted.',
+ path: '/databases/{uuid}/restart',
+ operationId: 'restart-database-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Restart database.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
+ ])
+ ),
+ ]),
+ 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 action_restart(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+ RestartDatabase::dispatch($database)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Database restarting request queued.',
+ ],
+ 200
+ );
+ }
+}
diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php
deleted file mode 100644
index d2abe2e31..000000000
--- a/app/Http/Controllers/Api/Deploy.php
+++ /dev/null
@@ -1,216 +0,0 @@
-get();
- $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([
- 'id',
- 'application_id',
- 'application_name',
- 'deployment_url',
- 'pull_request_id',
- 'server_name',
- 'server_id',
- 'status',
- ])->sortBy('id')->toArray();
-
- return response()->json($deployments_per_server, 200);
- }
-
- public function deploy(Request $request)
- {
- $teamId = get_team_id_from_token();
- $uuids = $request->query->get('uuid');
- $tags = $request->query->get('tag');
- $force = $request->query->get('force') ?? false;
-
- if ($uuids && $tags) {
- return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
- }
- if (is_null($teamId)) {
- return invalid_token();
- }
- if ($tags) {
- return $this->by_tags($tags, $teamId, $force);
- } elseif ($uuids) {
- return $this->by_uuids($uuids, $teamId, $force);
- }
-
- return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
- }
-
- private function by_uuids(string $uuid, int $teamId, bool $force = false)
- {
- $uuids = explode(',', $uuid);
- $uuids = collect(array_filter($uuids));
-
- if (count($uuids) === 0) {
- return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
- }
- $deployments = collect();
- $payload = collect();
- foreach ($uuids as $uuid) {
- $resource = getResourceByUuid($uuid, $teamId);
- if ($resource) {
- ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
- if ($deployment_uuid) {
- $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
- } else {
- $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
- }
- }
- }
- if ($deployments->count() > 0) {
- $payload->put('deployments', $deployments->toArray());
-
- return response()->json($payload->toArray(), 200);
- }
-
- return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
- }
-
- public function by_tags(string $tags, int $team_id, bool $force = false)
- {
- $tags = explode(',', $tags);
- $tags = collect(array_filter($tags));
-
- if (count($tags) === 0) {
- return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
- }
- $message = collect([]);
- $deployments = collect();
- $payload = collect();
- foreach ($tags as $tag) {
- $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
- if (! $found_tag) {
- // $message->push("Tag {$tag} not found.");
- continue;
- }
- $applications = $found_tag->applications()->get();
- $services = $found_tag->services()->get();
- if ($applications->count() === 0 && $services->count() === 0) {
- $message->push("No resources found for tag {$tag}.");
-
- continue;
- }
- foreach ($applications as $resource) {
- ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
- if ($deployment_uuid) {
- $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
- }
- $message = $message->merge($return_message);
- }
- foreach ($services as $resource) {
- ['message' => $return_message] = $this->deploy_resource($resource, $force);
- $message = $message->merge($return_message);
- }
- }
- ray($message);
- if ($message->count() > 0) {
- $payload->put('message', $message->toArray());
- if ($deployments->count() > 0) {
- $payload->put('details', $deployments->toArray());
- }
-
- return response()->json($payload->toArray(), 200);
- }
-
- return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
- }
-
- public function deploy_resource($resource, bool $force = false): array
- {
- $message = null;
- $deployment_uuid = null;
- if (gettype($resource) !== 'object') {
- return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
- }
- $type = $resource?->getMorphClass();
- if ($type === 'App\Models\Application') {
- $deployment_uuid = new Cuid2(7);
- queue_application_deployment(
- application: $resource,
- deployment_uuid: $deployment_uuid,
- force_rebuild: $force,
- );
- $message = "Application {$resource->name} deployment queued.";
- } elseif ($type === 'App\Models\StandalonePostgresql') {
- StartPostgresql::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneRedis') {
- StartRedis::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneKeydb') {
- StartKeydb::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneDragonfly') {
- StartDragonfly::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneClickhouse') {
- StartClickhouse::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneMongodb') {
- StartMongodb::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneMysql') {
- StartMysql::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\StandaloneMariadb') {
- StartMariadb::run($resource);
- $resource->update([
- 'started_at' => now(),
- ]);
- $message = "Database {$resource->name} started.";
- } elseif ($type === 'App\Models\Service') {
- StartService::run($resource);
- $message = "Service {$resource->name} started. It could take a while, be patient.";
- }
-
- return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
- }
-}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
new file mode 100644
index 000000000..59b199d87
--- /dev/null
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -0,0 +1,320 @@
+user()->currentAccessToken();
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($deployment);
+ }
+
+ $deployment->makeHidden([
+ 'logs',
+ ]);
+
+ return serializeApiResponse($deployment);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List currently running deployments',
+ path: '/deployments',
+ operationId: 'list-deployments',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Deployments'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all currently running deployments.',
+ content: [
+
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'),
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function deployments(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $servers = Server::whereTeamId($teamId)->get();
+ $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id');
+ $deployments_per_server = $deployments_per_server->map(function ($deployment) {
+ return $this->removeSensitiveData($deployment);
+ });
+
+ return response()->json($deployments_per_server);
+ }
+
+ #[OA\Get(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get deployment by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ ref: '#/components/schemas/ApplicationDeploymentQueue',
+ )
+ ),
+ ]),
+ 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 deployment_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first();
+ if (! $deployment) {
+ return response()->json(['message' => 'Deployment not found.'], 404);
+ }
+
+ return response()->json($this->removeSensitiveData($deployment));
+ }
+
+ #[OA\Get(
+ summary: 'Deploy',
+ description: 'Deploy by tag or uuid. `Post` request also accepted.',
+ path: '/deploy',
+ operationId: 'deploy-by-tag-or-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Deployments'],
+ parameters: [
+ new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
+ ],
+
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get deployment(s) UUID\'s',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'deployments' => new OA\Property(
+ property: 'deployments',
+ type: 'array',
+ items: new OA\Items(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string'],
+ 'resource_uuid' => ['type' => 'string'],
+ 'deployment_uuid' => ['type' => 'string'],
+ ]
+ ),
+ ),
+ ],
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+
+ ]
+ )]
+ public function deploy(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ $uuids = $request->query->get('uuid');
+ $tags = $request->query->get('tag');
+ $force = $request->query->get('force') ?? false;
+
+ if ($uuids && $tags) {
+ return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
+ }
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if ($tags) {
+ return $this->by_tags($tags, $teamId, $force);
+ } elseif ($uuids) {
+ return $this->by_uuids($uuids, $teamId, $force);
+ }
+
+ return response()->json(['message' => 'You must provide uuid or tag.'], 400);
+ }
+
+ private function by_uuids(string $uuid, int $teamId, bool $force = false)
+ {
+ $uuids = explode(',', $uuid);
+ $uuids = collect(array_filter($uuids));
+
+ if (count($uuids) === 0) {
+ return response()->json(['message' => 'No UUIDs provided.'], 400);
+ }
+ $deployments = collect();
+ $payload = collect();
+ foreach ($uuids as $uuid) {
+ $resource = getResourceByUuid($uuid, $teamId);
+ if ($resource) {
+ ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
+ if ($deployment_uuid) {
+ $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
+ } else {
+ $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
+ }
+ }
+ }
+ if ($deployments->count() > 0) {
+ $payload->put('deployments', $deployments->toArray());
+
+ return response()->json(serializeApiResponse($payload->toArray()));
+ }
+
+ return response()->json(['message' => 'No resources found.'], 404);
+ }
+
+ public function by_tags(string $tags, int $team_id, bool $force = false)
+ {
+ $tags = explode(',', $tags);
+ $tags = collect(array_filter($tags));
+
+ if (count($tags) === 0) {
+ return response()->json(['message' => 'No TAGs provided.'], 400);
+ }
+ $message = collect([]);
+ $deployments = collect();
+ $payload = collect();
+ foreach ($tags as $tag) {
+ $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
+ if (! $found_tag) {
+ // $message->push("Tag {$tag} not found.");
+ continue;
+ }
+ $applications = $found_tag->applications()->get();
+ $services = $found_tag->services()->get();
+ if ($applications->count() === 0 && $services->count() === 0) {
+ $message->push("No resources found for tag {$tag}.");
+
+ continue;
+ }
+ foreach ($applications as $resource) {
+ ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
+ if ($deployment_uuid) {
+ $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
+ }
+ $message = $message->merge($return_message);
+ }
+ foreach ($services as $resource) {
+ ['message' => $return_message] = $this->deploy_resource($resource, $force);
+ $message = $message->merge($return_message);
+ }
+ }
+ if ($message->count() > 0) {
+ $payload->put('message', $message->toArray());
+ if ($deployments->count() > 0) {
+ $payload->put('details', $deployments->toArray());
+ }
+
+ return response()->json(serializeApiResponse($payload->toArray()));
+ }
+
+ return response()->json(['message' => 'No resources found with this tag.'], 404);
+ }
+
+ public function deploy_resource($resource, bool $force = false): array
+ {
+ $message = null;
+ $deployment_uuid = null;
+ if (gettype($resource) !== 'object') {
+ return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
+ }
+ switch ($resource?->getMorphClass()) {
+ case \App\Models\Application::class:
+ $deployment_uuid = new Cuid2;
+ queue_application_deployment(
+ application: $resource,
+ deployment_uuid: $deployment_uuid,
+ force_rebuild: $force,
+ );
+ $message = "Application {$resource->name} deployment queued.";
+ break;
+ case \App\Models\Service::class:
+ StartService::run($resource);
+ $message = "Service {$resource->name} started. It could take a while, be patient.";
+ break;
+ default:
+ // Database resource
+ StartDatabase::dispatch($resource)->onQueue('high');
+ $resource->update([
+ 'started_at' => now(),
+ ]);
+ $message = "Database {$resource->name} started.";
+ break;
+ }
+
+ return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
+ }
+}
diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php
deleted file mode 100644
index c27ddf620..000000000
--- a/app/Http/Controllers/Api/Domains.php
+++ /dev/null
@@ -1,104 +0,0 @@
-get();
- $domains = collect();
- $applications = $projects->pluck('applications')->flatten();
- $settings = InstanceSettings::get();
- if ($applications->count() > 0) {
- foreach ($applications as $application) {
- $ip = $application->destination->server->ip;
- $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
- return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
- });
- if ($ip === 'host.docker.internal') {
- if ($settings->public_ipv4) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $settings->public_ipv4,
- ]);
- }
- if ($settings->public_ipv6) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $settings->public_ipv6,
- ]);
- }
- if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $ip,
- ]);
- }
- } else {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $ip,
- ]);
- }
- }
- }
- $services = $projects->pluck('services')->flatten();
- if ($services->count() > 0) {
- foreach ($services as $service) {
- $service_applications = $service->applications;
- if ($service_applications->count() > 0) {
- foreach ($service_applications as $application) {
- $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
- return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
- });
- if ($ip === 'host.docker.internal') {
- if ($settings->public_ipv4) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $settings->public_ipv4,
- ]);
- }
- if ($settings->public_ipv6) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $settings->public_ipv6,
- ]);
- }
- if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $ip,
- ]);
- }
- } else {
- $domains->push([
- 'domain' => $fqdn,
- 'ip' => $ip,
- ]);
- }
- }
- }
- }
- }
- $domains = $domains->groupBy('ip')->map(function ($domain) {
- return $domain->pluck('domain')->flatten();
- })->map(function ($domain, $ip) {
- return [
- 'ip' => $ip,
- 'domains' => $domain,
- ];
- })->values();
-
- return response()->json($domains);
- }
-}
diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php
new file mode 100644
index 000000000..60337a76c
--- /dev/null
+++ b/app/Http/Controllers/Api/OpenApi.php
@@ -0,0 +1,51 @@
+ []],
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Returns the version of the application',
+ content: new OA\JsonContent(
+ type: 'string',
+ example: 'v4.0.0',
+ )),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function version(Request $request)
+ {
+ return response(config('version'));
+ }
+
+ #[OA\Get(
+ summary: 'Enable API',
+ description: 'Enable API (only with root permissions).',
+ path: '/enable',
+ operationId: 'enable-api',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Enable API.',
+ content: new OA\JsonContent(
+ type: 'object',
+ properties: [
+ new OA\Property(property: 'message', type: 'string', example: 'API enabled.'),
+ ]
+ )),
+ new OA\Response(
+ response: 403,
+ description: 'You are not allowed to enable the API.',
+ content: new OA\JsonContent(
+ type: 'object',
+ properties: [
+ new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'),
+ ]
+ )),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function enable_api(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if ($teamId !== '0') {
+ return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
+ }
+ $settings = instanceSettings();
+ $settings->update(['is_api_enabled' => true]);
+
+ return response()->json(['message' => 'API enabled.'], 200);
+ }
+
+ #[OA\Get(
+ summary: 'Disable API',
+ description: 'Disable API (only with root permissions).',
+ path: '/disable',
+ operationId: 'disable-api',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Disable API.',
+ content: new OA\JsonContent(
+ type: 'object',
+ properties: [
+ new OA\Property(property: 'message', type: 'string', example: 'API disabled.'),
+ ]
+ )),
+ new OA\Response(
+ response: 403,
+ description: 'You are not allowed to disable the API.',
+ content: new OA\JsonContent(
+ type: 'object',
+ properties: [
+ new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'),
+ ]
+ )),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function disable_api(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if ($teamId !== '0') {
+ return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
+ }
+ $settings = instanceSettings();
+ $settings->update(['is_api_enabled' => false]);
+
+ return response()->json(['message' => 'API disabled.'], 200);
+ }
+
+ public function feedback(Request $request)
+ {
+ $content = $request->input('content');
+ $webhook_url = config('coolify.feedback_discord_webhook');
+ if ($webhook_url) {
+ Http::post($webhook_url, [
+ 'content' => $content,
+ ]);
+ }
+
+ return response()->json(['message' => 'Feedback sent.'], 200);
+ }
+
+ #[OA\Get(
+ summary: 'Healthcheck',
+ description: 'Healthcheck endpoint.',
+ path: '/health',
+ operationId: 'healthcheck',
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Healthcheck endpoint.',
+ content: new OA\JsonContent(
+ type: 'string',
+ example: 'OK',
+ )),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function healthcheck(Request $request)
+ {
+ return 'OK';
+ }
+}
diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php
deleted file mode 100644
index baaf1eacb..000000000
--- a/app/Http/Controllers/Api/Project.php
+++ /dev/null
@@ -1,44 +0,0 @@
-select('id', 'name', 'uuid')->get();
-
- return response()->json($projects);
- }
-
- public function project_by_uuid(Request $request)
- {
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
-
- return response()->json($project);
- }
-
- public function environment_details(Request $request)
- {
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
- $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
-
- return response()->json($environment);
- }
-}
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
new file mode 100644
index 000000000..1d89c82ed
--- /dev/null
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -0,0 +1,433 @@
+ []],
+ ],
+ tags: ['Projects'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all projects.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Project')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function projects(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get();
+
+ return response()->json(serializeApiResponse($projects),
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Get',
+ description: 'Get project by UUID.',
+ path: '/projects/{uuid}',
+ operationId: 'get-project-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Projects'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Project details',
+ content: new OA\JsonContent(ref: '#/components/schemas/Project')),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ description: 'Project not found.',
+ ),
+ ]
+ )]
+ public function project_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+
+ return response()->json(
+ serializeApiResponse($project),
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Environment',
+ description: 'Get environment by name.',
+ path: '/projects/{uuid}/{environment_name}',
+ operationId: 'get-environment-by-name',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Projects'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Environment details',
+ content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
+ 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 environment_details(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ 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);
+ }
+ $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
+
+ return response()->json(serializeApiResponse($environment));
+ }
+
+ #[OA\Post(
+ summary: 'Create',
+ description: 'Create Project.',
+ path: '/projects',
+ operationId: 'create-project',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Projects'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Project created.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'name' => ['type' => 'string', 'description' => 'The name of the project.'],
+ 'description' => ['type' => 'string', 'description' => 'The description of the project.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Project created.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the project.'],
+ ]
+ )
+ ),
+ ]),
+ 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_project(Request $request)
+ {
+ $allowedFields = ['name', 'description'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255|required',
+ 'description' => 'string|nullable',
+ ]);
+
+ $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);
+ }
+
+ $project = Project::create([
+ 'name' => $request->name,
+ 'description' => $request->description,
+ 'team_id' => $teamId,
+ ]);
+
+ return response()->json([
+ 'uuid' => $project->uuid,
+ ])->setStatusCode(201);
+ }
+
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update Project.',
+ path: '/projects/{uuid}',
+ operationId: 'update-project-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Projects'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Project updated.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'name' => ['type' => 'string', 'description' => 'The name of the project.'],
+ 'description' => ['type' => 'string', 'description' => 'The description of the project.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Project updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'example' => 'og888os'],
+ 'name' => ['type' => 'string', 'example' => 'Project Name'],
+ 'description' => ['type' => 'string', 'example' => 'Project Description'],
+ ]
+ )
+ ),
+ ]),
+ 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_project(Request $request)
+ {
+ $allowedFields = ['name', 'description'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255|nullable',
+ 'description' => 'string|nullable',
+ ]);
+
+ $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);
+ }
+ $uuid = $request->uuid;
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 422);
+ }
+
+ $project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+
+ $project->update($request->only($allowedFields));
+
+ return response()->json([
+ 'uuid' => $project->uuid,
+ 'name' => $project->name,
+ 'description' => $project->description,
+ ])->setStatusCode(201);
+ }
+
+ #[OA\Delete(
+ summary: 'Delete',
+ description: 'Delete project by UUID.',
+ path: '/projects/{uuid}',
+ operationId: 'delete-project-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Projects'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the application.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Project deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Project 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_project(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 422);
+ }
+ $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+ if (! $project->isEmpty()) {
+ return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
+ }
+
+ $project->delete();
+
+ return response()->json(['message' => 'Project deleted.']);
+ }
+}
diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/Resources.php
deleted file mode 100644
index 0d538b62e..000000000
--- a/app/Http/Controllers/Api/Resources.php
+++ /dev/null
@@ -1,39 +0,0 @@
-get();
- $resources = collect();
- $resources->push($projects->pluck('applications')->flatten());
- $resources->push($projects->pluck('services')->flatten());
- foreach (collect(DATABASE_TYPES) as $db) {
- $resources->push($projects->pluck(str($db)->plural(2))->flatten());
- }
- $resources = $resources->flatten();
- $resources = $resources->map(function ($resource) {
- $payload = $resource->toArray();
- if ($resource->getMorphClass() === 'App\Models\Service') {
- $payload['status'] = $resource->status();
- } else {
- $payload['status'] = $resource->status;
- }
- $payload['type'] = $resource->type();
-
- return $payload;
- });
-
- return response()->json($resources);
- }
-}
diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php
new file mode 100644
index 000000000..4180cef9a
--- /dev/null
+++ b/app/Http/Controllers/Api/ResourcesController.php
@@ -0,0 +1,68 @@
+ []],
+ ],
+ tags: ['Resources'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all resources',
+ content: new OA\JsonContent(
+ type: 'string',
+ example: 'Content is very complex. Will be implemented later.',
+ ),
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function resources(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $projects = Project::where('team_id', $teamId)->get();
+ $resources = collect();
+ $resources->push($projects->pluck('applications')->flatten());
+ $resources->push($projects->pluck('services')->flatten());
+ foreach (collect(DATABASE_TYPES) as $db) {
+ $resources->push($projects->pluck(str($db)->plural(2))->flatten());
+ }
+ $resources = $resources->flatten();
+ $resources = $resources->map(function ($resource) {
+ $payload = $resource->toArray();
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
+ $payload['status'] = $resource->status();
+ } else {
+ $payload['status'] = $resource->status;
+ }
+ $payload['type'] = $resource->type();
+
+ return $payload;
+ });
+
+ return response()->json(serializeApiResponse($resources));
+ }
+}
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
new file mode 100644
index 000000000..b7190ab1e
--- /dev/null
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -0,0 +1,370 @@
+user()->currentAccessToken();
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($team);
+ }
+ $team->makeHidden([
+ 'private_key',
+ ]);
+
+ return serializeApiResponse($team);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List all private keys.',
+ path: '/security/keys',
+ operationId: 'list-private-keys',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Private Keys'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all private keys.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/PrivateKey')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function keys(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $keys = PrivateKey::where('team_id', $teamId)->get();
+
+ return response()->json($this->removeSensitiveData($keys));
+ }
+
+ #[OA\Get(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all private keys.',
+ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ description: 'Private Key not found.',
+ ),
+ ]
+ )]
+ public function key_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
+
+ if (is_null($key)) {
+ return response()->json([
+ 'message' => 'Private Key not found.',
+ ], 404);
+ }
+
+ return response()->json($this->removeSensitiveData($key));
+ }
+
+ #[OA\Post(
+ summary: 'Create',
+ description: 'Create a new private key.',
+ path: '/security/keys',
+ operationId: 'create-private-key',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Private Keys'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ content: [
+ 'application/json' => new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['private_key'],
+ properties: [
+ 'name' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ 'private_key' => ['type' => 'string'],
+ ],
+ additionalProperties: false,
+ )
+ ),
+ ]
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'The created private key\'s UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_key(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|max:255',
+ 'private_key' => 'required|string',
+ ]);
+
+ if ($validator->fails()) {
+ $errors = $validator->errors();
+
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (! $request->name) {
+ $request->offsetSet('name', generate_random_name());
+ }
+ if (! $request->description) {
+ $request->offsetSet('description', 'Created by Coolify via API');
+ }
+ $key = PrivateKey::create([
+ 'team_id' => $teamId,
+ 'name' => $request->name,
+ 'description' => $request->description,
+ 'private_key' => $request->private_key,
+ ]);
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => $key->uuid,
+ ]))->setStatusCode(201);
+ }
+
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update a private key.',
+ path: '/security/keys',
+ operationId: 'update-private-key',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Private Keys'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ content: [
+ 'application/json' => new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['private_key'],
+ properties: [
+ 'name' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ 'private_key' => ['type' => 'string'],
+ ],
+ additionalProperties: false,
+ )
+ ),
+ ]
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'The updated private key\'s UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function update_key(Request $request)
+ {
+ $allowedFields = ['name', 'description', 'private_key'];
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|max:255',
+ 'private_key' => 'required|string',
+ ]);
+
+ $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);
+ }
+ $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
+ if (is_null($foundKey)) {
+ return response()->json([
+ 'message' => 'Private Key not found.',
+ ], 404);
+ }
+ $foundKey->update($request->all());
+
+ return response()->json(serializeApiResponse([
+ 'uuid' => $foundKey->uuid,
+ ]))->setStatusCode(201);
+ }
+
+ #[OA\Delete(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Private Key deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Private Key deleted.'],
+ ]
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ description: 'Private Key not found.',
+ ),
+ ]
+ )]
+ public function delete_key(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 422);
+ }
+
+ $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
+ if (is_null($key)) {
+ return response()->json(['message' => 'Private Key not found.'], 404);
+ }
+ $key->forceDelete();
+
+ return response()->json([
+ 'message' => 'Private Key deleted.',
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php
deleted file mode 100644
index 9f88a3b28..000000000
--- a/app/Http/Controllers/Api/Server.php
+++ /dev/null
@@ -1,62 +0,0 @@
-select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
- $server['is_reachable'] = $server->settings->is_reachable;
- $server['is_usable'] = $server->settings->is_usable;
-
- return $server;
- });
-
- return response()->json($servers);
- }
-
- public function server_by_uuid(Request $request)
- {
- $with_resources = $request->query('resources');
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
- if (is_null($server)) {
- return response()->json(['error' => 'Server not found.'], 404);
- }
- if ($with_resources) {
- $server['resources'] = $server->definedResources()->map(function ($resource) {
- $payload = [
- 'id' => $resource->id,
- 'uuid' => $resource->uuid,
- 'name' => $resource->name,
- 'type' => $resource->type(),
- 'created_at' => $resource->created_at,
- 'updated_at' => $resource->updated_at,
- ];
- if ($resource->type() === 'service') {
- $payload['status'] = $resource->status();
- } else {
- $payload['status'] = $resource->status;
- }
-
- return $payload;
- });
- } else {
- $server->load(['settings']);
- }
-
- return response()->json($server);
- }
-}
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
new file mode 100644
index 000000000..024ef35fa
--- /dev/null
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -0,0 +1,794 @@
+user()->currentAccessToken();
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($settings);
+ }
+ $settings = $settings->makeHidden([
+ 'sentinel_token',
+ ]);
+
+ return serializeApiResponse($settings);
+ }
+
+ private function removeSensitiveData($server)
+ {
+ $token = auth()->user()->currentAccessToken();
+ $server->makeHidden([
+ 'id',
+ ]);
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($server);
+ }
+
+ return serializeApiResponse($server);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List all servers.',
+ path: '/servers',
+ operationId: 'list-servers',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Servers'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all servers.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Server')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function servers(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port', 'description')->get()->load(['settings'])->map(function ($server) {
+ $server['is_reachable'] = $server->settings->is_reachable;
+ $server['is_usable'] = $server->settings->is_usable;
+
+ return $server;
+ });
+ $servers = $servers->map(function ($server) {
+ $settings = $this->removeSensitiveDataFromSettings($server->settings);
+ $server = $this->removeSensitiveData($server);
+ data_set($server, 'settings', $settings);
+
+ return $server;
+ });
+
+ return response()->json($servers);
+ }
+
+ #[OA\Get(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get server by UUID',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ ref: '#/components/schemas/Server'
+ )
+ ),
+ ]),
+ 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 server_by_uuid(Request $request)
+ {
+ $with_resources = $request->query('resources');
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
+ if (is_null($server)) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ if ($with_resources) {
+ $server['resources'] = $server->definedResources()->map(function ($resource) {
+ $payload = [
+ 'id' => $resource->id,
+ 'uuid' => $resource->uuid,
+ 'name' => $resource->name,
+ 'type' => $resource->type(),
+ 'created_at' => $resource->created_at,
+ 'updated_at' => $resource->updated_at,
+ ];
+ if ($resource->type() === 'service') {
+ $payload['status'] = $resource->status();
+ } else {
+ $payload['status'] = $resource->status;
+ }
+
+ return $payload;
+ });
+ } else {
+ $server->load(['settings']);
+ }
+
+ $settings = $this->removeSensitiveDataFromSettings($server->settings);
+ $server = $this->removeSensitiveData($server);
+ data_set($server, 'settings', $settings);
+
+ return response()->json(serializeApiResponse($server));
+ }
+
+ #[OA\Get(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get resources by server',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'uuid' => ['type' => 'string'],
+ 'name' => ['type' => 'string'],
+ 'type' => ['type' => 'string'],
+ 'created_at' => ['type' => 'string'],
+ 'updated_at' => ['type' => 'string'],
+ 'status' => ['type' => 'string'],
+ ]
+ )
+ )),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function resources_by_server(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
+ if (is_null($server)) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $server['resources'] = $server->definedResources()->map(function ($resource) {
+ $payload = [
+ 'id' => $resource->id,
+ 'uuid' => $resource->uuid,
+ 'name' => $resource->name,
+ 'type' => $resource->type(),
+ 'created_at' => $resource->created_at,
+ 'updated_at' => $resource->updated_at,
+ ];
+ if ($resource->type() === 'service') {
+ $payload['status'] = $resource->status();
+ } else {
+ $payload['status'] = $resource->status;
+ }
+
+ return $payload;
+ });
+ $server = $this->removeSensitiveData($server);
+
+ return response()->json(serializeApiResponse(data_get($server, 'resources')));
+ }
+
+ #[OA\Get(
+ 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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get domains by server',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(
+ type: 'object',
+ properties: [
+ 'ip' => ['type' => 'string'],
+ 'domains' => ['type' => 'array', 'items' => ['type' => 'string']],
+ ]
+ )
+ )),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function domains_by_server(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->get('uuid');
+ if ($uuid) {
+ $domains = Application::getDomainsByUuid($uuid);
+
+ return response()->json(serializeApiResponse($domains));
+ }
+ $projects = Project::where('team_id', $teamId)->get();
+ $domains = collect();
+ $applications = $projects->pluck('applications')->flatten();
+ $settings = instanceSettings();
+ if ($applications->count() > 0) {
+ foreach ($applications as $application) {
+ $ip = $application->destination->server->ip;
+ $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
+ $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
+
+ return str(str($f[0])->explode(':')[0]);
+ })->filter(function (Stringable $fqdn) {
+ return $fqdn->isNotEmpty();
+ });
+
+ if ($ip === 'host.docker.internal') {
+ if ($settings->public_ipv4) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $settings->public_ipv4,
+ ]);
+ }
+ if ($settings->public_ipv6) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $settings->public_ipv6,
+ ]);
+ }
+ if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $ip,
+ ]);
+ }
+ } else {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $ip,
+ ]);
+ }
+ }
+ }
+ $services = $projects->pluck('services')->flatten();
+ if ($services->count() > 0) {
+ foreach ($services as $service) {
+ $service_applications = $service->applications;
+ if ($service_applications->count() > 0) {
+ foreach ($service_applications as $application) {
+ $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
+ $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
+
+ return str(str($f[0])->explode(':')[0]);
+ })->filter(function (Stringable $fqdn) {
+ return $fqdn->isNotEmpty();
+ });
+ if ($ip === 'host.docker.internal') {
+ if ($settings->public_ipv4) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $settings->public_ipv4,
+ ]);
+ }
+ if ($settings->public_ipv6) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $settings->public_ipv6,
+ ]);
+ }
+ if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $ip,
+ ]);
+ }
+ } else {
+ $domains->push([
+ 'domain' => $fqdn,
+ 'ip' => $ip,
+ ]);
+ }
+ }
+ }
+ }
+ }
+ $domains = $domains->groupBy('ip')->map(function ($domain) {
+ return $domain->pluck('domain')->flatten();
+ })->map(function ($domain, $ip) {
+ return [
+ 'ip' => $ip,
+ 'domains' => $domain,
+ ];
+ })->values();
+
+ return response()->json(serializeApiResponse($domains));
+ }
+
+ #[OA\Post(
+ summary: 'Create',
+ description: 'Create Server.',
+ path: '/servers',
+ operationId: 'create-server',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Servers'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Server created.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'name' => ['type' => 'string', 'example' => 'My Server', 'description' => 'The name of the server.'],
+ 'description' => ['type' => 'string', 'example' => 'My Server Description', 'description' => 'The description of the server.'],
+ 'ip' => ['type' => 'string', 'example' => '127.0.0.1', 'description' => 'The IP of the server.'],
+ 'port' => ['type' => 'integer', 'example' => 22, 'description' => 'The port of the server.'],
+ 'user' => ['type' => 'string', 'example' => 'root', 'description' => 'The user of the server.'],
+ 'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
+ 'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
+ 'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Server created.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the server.'],
+ ]
+ )
+ ),
+ ]),
+ 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_server(Request $request)
+ {
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'ip' => 'string|required',
+ 'port' => 'integer|nullable',
+ 'private_key_uuid' => 'string|required',
+ 'user' => 'string|nullable',
+ 'is_build_server' => 'boolean|nullable',
+ 'instant_validate' => 'boolean|nullable',
+ ]);
+
+ $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);
+ }
+ if (! $request->name) {
+ $request->offsetSet('name', generate_random_name());
+ }
+ if (! $request->user) {
+ $request->offsetSet('user', 'root');
+ }
+ if (is_null($request->port)) {
+ $request->offsetSet('port', 22);
+ }
+ if (is_null($request->is_build_server)) {
+ $request->offsetSet('is_build_server', false);
+ }
+ if (is_null($request->instant_validate)) {
+ $request->offsetSet('instant_validate', false);
+ }
+ $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
+ if (! $privateKey) {
+ return response()->json(['message' => 'Private key not found.'], 404);
+ }
+ $allServers = ModelsServer::whereIp($request->ip)->get();
+ if ($allServers->count() > 0) {
+ return response()->json(['message' => 'Server with this IP already exists.'], 400);
+ }
+
+ $server = ModelsServer::create([
+ 'name' => $request->name,
+ 'description' => $request->description,
+ 'ip' => $request->ip,
+ 'port' => $request->port,
+ 'user' => $request->user,
+ 'private_key_id' => $privateKey->id,
+ 'team_id' => $teamId,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+ $server->settings()->update([
+ 'is_build_server' => $request->is_build_server,
+ ]);
+ if ($request->instant_validate) {
+ ValidateServer::dispatch($server)->onQueue('high');
+ }
+
+ return response()->json([
+ 'uuid' => $server->uuid,
+ ])->setStatusCode(201);
+ }
+
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update Server.',
+ path: '/servers/{uuid}',
+ operationId: 'update-server-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Servers'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Server updated.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'name' => ['type' => 'string', 'description' => 'The name of the server.'],
+ 'description' => ['type' => 'string', 'description' => 'The description of the server.'],
+ 'ip' => ['type' => 'string', 'description' => 'The IP of the server.'],
+ 'port' => ['type' => 'integer', 'description' => 'The port of the server.'],
+ 'user' => ['type' => 'string', 'description' => 'The user of the server.'],
+ 'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
+ 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
+ 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Server updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Server')
+ )
+ ),
+ ]),
+ 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_server(Request $request)
+ {
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'name' => 'string|max:255|nullable',
+ 'description' => 'string|nullable',
+ 'ip' => 'string|nullable',
+ 'port' => 'integer|nullable',
+ 'private_key_uuid' => 'string|nullable',
+ 'user' => 'string|nullable',
+ 'is_build_server' => 'boolean|nullable',
+ 'instant_validate' => 'boolean|nullable',
+ ]);
+
+ $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);
+ }
+ $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
+ if ($request->is_build_server) {
+ $server->settings()->update([
+ 'is_build_server' => $request->is_build_server,
+ ]);
+ }
+ if ($request->instant_validate) {
+ ValidateServer::dispatch($server)->onQueue('high');
+ }
+
+ return response()->json(serializeApiResponse($server))->setStatusCode(201);
+ }
+
+ #[OA\Delete(
+ summary: 'Delete',
+ description: 'Delete server by UUID.',
+ path: '/servers/{uuid}',
+ operationId: 'delete-server-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the server.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Server deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Server 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_server(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ if (! $request->uuid) {
+ return response()->json(['message' => 'Uuid is required.'], 422);
+ }
+ $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
+
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ if ($server->definedResources()->count() > 0) {
+ return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
+ }
+ $server->delete();
+ DeleteServer::dispatch($server);
+
+ return response()->json(['message' => 'Server deleted.']);
+ }
+
+ #[OA\Get(
+ summary: 'Validate',
+ description: 'Validate server by UUID.',
+ path: '/servers/{uuid}/validate',
+ operationId: 'validate-server-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Server validation started.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Validation started.'],
+ ]
+ )
+ ),
+ ]),
+ 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 validate_server(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ if (! $request->uuid) {
+ return response()->json(['message' => 'Uuid is required.'], 422);
+ }
+ $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
+
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ ValidateServer::dispatch($server)->onQueue('high');
+
+ return response()->json(['message' => 'Validation started.']);
+ }
+}
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
new file mode 100644
index 000000000..bdb5612ad
--- /dev/null
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -0,0 +1,1241 @@
+user()->currentAccessToken();
+ $service->makeHidden([
+ 'id',
+ ]);
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($service);
+ }
+
+ $service->makeHidden([
+ 'docker_compose_raw',
+ 'docker_compose',
+ ]);
+
+ return serializeApiResponse($service);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'List all services.',
+ path: '/services',
+ operationId: 'list-services',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get all services',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Service')
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function services(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $projects = Project::where('team_id', $teamId)->get();
+ $services = collect();
+ foreach ($projects as $project) {
+ $services->push($project->services()->get());
+ }
+ foreach ($services as $service) {
+ $service = $this->removeSensitiveData($service);
+ }
+
+ return response()->json($services->flatten());
+ }
+
+ #[OA\Post(
+ summary: 'Create',
+ description: 'Create a one-click service',
+ path: '/services',
+ operationId: 'create-service',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ requestBody: new OA\RequestBody(
+ required: true,
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name', 'type'],
+ properties: [
+ 'type' => [
+ 'description' => 'The one-click service type',
+ 'type' => 'string',
+ 'enum' => [
+ 'activepieces',
+ 'appsmith',
+ 'appwrite',
+ 'authentik',
+ 'babybuddy',
+ 'budge',
+ 'changedetection',
+ 'chatwoot',
+ 'classicpress-with-mariadb',
+ 'classicpress-with-mysql',
+ 'classicpress-without-database',
+ 'cloudflared',
+ 'code-server',
+ 'dashboard',
+ 'directus',
+ 'directus-with-postgresql',
+ 'docker-registry',
+ 'docuseal',
+ 'docuseal-with-postgres',
+ 'dokuwiki',
+ 'duplicati',
+ 'emby',
+ 'embystat',
+ 'fider',
+ 'filebrowser',
+ 'firefly',
+ 'formbricks',
+ 'ghost',
+ 'gitea',
+ 'gitea-with-mariadb',
+ 'gitea-with-mysql',
+ 'gitea-with-postgresql',
+ 'glance',
+ 'glances',
+ 'glitchtip',
+ 'grafana',
+ 'grafana-with-postgresql',
+ 'grocy',
+ 'heimdall',
+ 'homepage',
+ 'jellyfin',
+ 'kuzzle',
+ 'listmonk',
+ 'logto',
+ 'mediawiki',
+ 'meilisearch',
+ 'metabase',
+ 'metube',
+ 'minio',
+ 'moodle',
+ 'n8n',
+ 'n8n-with-postgresql',
+ 'next-image-transformation',
+ 'nextcloud',
+ 'nocodb',
+ 'odoo',
+ 'openblocks',
+ 'pairdrop',
+ 'penpot',
+ 'phpmyadmin',
+ 'pocketbase',
+ 'posthog',
+ 'reactive-resume',
+ 'rocketchat',
+ 'shlink',
+ 'slash',
+ 'snapdrop',
+ 'statusnook',
+ 'stirling-pdf',
+ 'supabase',
+ 'syncthing',
+ 'tolgee',
+ 'trigger',
+ 'trigger-with-external-database',
+ 'twenty',
+ 'umami',
+ 'unleash-with-postgresql',
+ 'unleash-without-database',
+ 'uptime-kuma',
+ 'vaultwarden',
+ 'vikunja',
+ 'weblate',
+ 'whoogle',
+ 'wordpress-with-mariadb',
+ 'wordpress-with-mysql',
+ 'wordpress-without-database',
+ ],
+ ],
+ 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
+ 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'Environment name.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
+ 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Create a service.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
+ 'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function create_service(Request $request)
+ {
+ $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy'];
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'type' => 'string|required',
+ 'project_uuid' => 'string|required',
+ 'environment_name' => 'string|required',
+ 'server_uuid' => 'string|required',
+ 'destination_uuid' => 'string',
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'instant_deploy' => 'boolean',
+ ]);
+
+ $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);
+ }
+ $serverUuid = $request->server_uuid;
+ $instantDeploy = $request->instant_deploy ?? false;
+ if ($request->is_public && ! $request->public_port) {
+ $request->offsetSet('is_public', false);
+ }
+ $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+ $environment = $project->environments()->where('name', $request->environment_name)->first();
+ if (! $environment) {
+ return response()->json(['message' => 'Environment not found.'], 404);
+ }
+ $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $destinations = $server->destinations();
+ if ($destinations->count() == 0) {
+ return response()->json(['message' => 'Server has no destinations.'], 400);
+ }
+ if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
+ return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
+ }
+ $destination = $destinations->first();
+ $services = get_service_templates();
+ $serviceKeys = $services->keys();
+ if ($serviceKeys->contains($request->type)) {
+ $oneClickServiceName = $request->type;
+ $oneClickService = data_get($services, "$oneClickServiceName.compose");
+ $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
+ if ($oneClickDotEnvs) {
+ $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
+ return ! empty($value);
+ });
+ }
+ if ($oneClickService) {
+ $service_payload = [
+ 'name' => "$oneClickServiceName-".str()->random(10),
+ 'docker_compose_raw' => base64_decode($oneClickService),
+ 'environment_id' => $environment->id,
+ 'service_type' => $oneClickServiceName,
+ 'server_id' => $server->id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination->getMorphClass(),
+ ];
+ if ($oneClickServiceName === 'cloudflared') {
+ data_set($service_payload, 'connect_to_docker_network', true);
+ }
+ $service = Service::create($service_payload);
+ $service->name = "$oneClickServiceName-".$service->uuid;
+ $service->save();
+ if ($oneClickDotEnvs?->count() > 0) {
+ $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);
+ }
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $generatedValue,
+ 'service_id' => $service->id,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ });
+ }
+ $service->parse(isNew: true);
+ if ($instantDeploy) {
+ StartService::dispatch($service)->onQueue('high');
+ }
+ $domains = $service->applications()->get()->pluck('fqdn')->sort();
+ $domains = $domains->map(function ($domain) {
+ return str($domain)->beforeLast(':')->value();
+ });
+
+ return response()->json([
+ 'uuid' => $service->uuid,
+ 'domains' => $domains,
+ ]);
+ }
+
+ return response()->json(['message' => 'Service not found.'], 404);
+ } else {
+ return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
+ }
+
+ return response()->json(['message' => 'Invalid service type.'], 400);
+ }
+
+ #[OA\Get(
+ summary: 'Get',
+ description: 'Get service by UUID.',
+ path: '/services/{uuid}',
+ operationId: 'get-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')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get a service by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ ref: '#/components/schemas/Service'
+ )
+ ),
+ ]
+ ),
+ 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 service_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 404);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $service = $service->load(['applications', 'databases']);
+
+ return response()->json($this->removeSensitiveData($service));
+ }
+
+ #[OA\Delete(
+ 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',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Service deletion request queued.'],
+ ],
+ )
+ ),
+ ]
+ ),
+ 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_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ if (! $request->uuid) {
+ return response()->json(['message' => 'UUID is required.'], 404);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ 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)
+ )->onQueue('high');
+
+ 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',
+ ]);
+
+ return $this->removeSensitiveData($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' => []],
+ ],
+ 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: 'Start service.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
+ ]
+ )
+ ),
+ ]
+ ),
+ 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 action_deploy(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+ if (str($service->status())->contains('running')) {
+ return response()->json(['message' => 'Service is already running.'], 400);
+ }
+ StartService::dispatch($service)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Service starting request queued.',
+ ],
+ 200
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Stop',
+ description: 'Stop service. `Post` request is also accepted.',
+ path: '/services/{uuid}/stop',
+ operationId: 'stop-service-by-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: 'Stop service.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
+ ]
+ )
+ ),
+ ]
+ ),
+ 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 action_stop(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+ if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
+ return response()->json(['message' => 'Service is already stopped.'], 400);
+ }
+ StopService::dispatch($service)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Service stopping request queued.',
+ ],
+ 200
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Restart',
+ description: 'Restart service. `Post` request is also accepted.',
+ path: '/services/{uuid}/restart',
+ operationId: 'restart-service-by-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: 'Restart service.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
+ ]
+ )
+ ),
+ ]
+ ),
+ 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 action_restart(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+ RestartService::dispatch($service)->onQueue('high');
+
+ return response()->json(
+ [
+ 'message' => 'Service restarting request queued.',
+ ],
+ 200
+ );
+ }
+}
diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php
deleted file mode 100644
index c895f2c1b..000000000
--- a/app/Http/Controllers/Api/Team.php
+++ /dev/null
@@ -1,74 +0,0 @@
-user()->teams;
-
- return response()->json($teams);
- }
-
- public function team_by_id(Request $request)
- {
- $id = $request->id;
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $teams = auth()->user()->teams;
- $team = $teams->where('id', $id)->first();
- if (is_null($team)) {
- return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404);
- }
-
- return response()->json($team);
- }
-
- public function members_by_id(Request $request)
- {
- $id = $request->id;
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $teams = auth()->user()->teams;
- $team = $teams->where('id', $id)->first();
- if (is_null($team)) {
- return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404);
- }
-
- return response()->json($team->members);
- }
-
- public function current_team(Request $request)
- {
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $team = auth()->user()->currentTeam();
-
- return response()->json($team);
- }
-
- public function current_team_members(Request $request)
- {
- $teamId = get_team_id_from_token();
- if (is_null($teamId)) {
- return invalid_token();
- }
- $team = auth()->user()->currentTeam();
-
- return response()->json($team->members);
- }
-}
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
new file mode 100644
index 000000000..3f951c6f7
--- /dev/null
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -0,0 +1,275 @@
+user()->currentAccessToken();
+ $team->makeHidden([
+ 'custom_server_limit',
+ 'pivot',
+ ]);
+ if ($token->can('view:sensitive')) {
+ return serializeApiResponse($team);
+ }
+ $team->makeHidden([
+ 'smtp_username',
+ 'smtp_password',
+ 'resend_api_key',
+ 'telegram_token',
+ ]);
+
+ return serializeApiResponse($team);
+ }
+
+ #[OA\Get(
+ summary: 'List',
+ description: 'Get all teams.',
+ path: '/teams',
+ operationId: 'list-teams',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Teams'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'List of teams.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Team')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function teams(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $teams = auth()->user()->teams->sortBy('id');
+ $teams = $teams->map(function ($team) {
+ return $this->removeSensitiveData($team);
+ });
+
+ return response()->json(
+ $teams,
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Get',
+ description: 'Get team by TeamId.',
+ path: '/teams/{id}',
+ operationId: 'get-team-by-id',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Teams'],
+ parameters: [
+ new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'List of teams.',
+ content: new OA\JsonContent(ref: '#/components/schemas/Team')
+ ),
+ 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 team_by_id(Request $request)
+ {
+ $id = $request->id;
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $teams = auth()->user()->teams;
+ $team = $teams->where('id', $id)->first();
+ if (is_null($team)) {
+ return response()->json(['message' => 'Team not found.'], 404);
+ }
+ $team = $this->removeSensitiveData($team);
+
+ return response()->json(
+ serializeApiResponse($team),
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Members',
+ description: 'Get members by TeamId.',
+ path: '/teams/{id}/members',
+ operationId: 'get-members-by-team-id',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Teams'],
+ parameters: [
+ new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'List of members.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/User')
+ )
+ ),
+ ]),
+ 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 members_by_id(Request $request)
+ {
+ $id = $request->id;
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $teams = auth()->user()->teams;
+ $team = $teams->where('id', $id)->first();
+ if (is_null($team)) {
+ return response()->json(['message' => 'Team not found.'], 404);
+ }
+ $members = $team->members;
+ $members->makeHidden([
+ 'pivot',
+ ]);
+
+ return response()->json(
+ serializeApiResponse($members),
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Authenticated Team',
+ description: 'Get currently authenticated team.',
+ path: '/teams/current',
+ operationId: 'get-current-team',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Teams'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Current Team.',
+ content: new OA\JsonContent(ref: '#/components/schemas/Team')),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function current_team(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $team = auth()->user()->currentTeam();
+
+ return response()->json(
+ $this->removeSensitiveData($team),
+ );
+ }
+
+ #[OA\Get(
+ summary: 'Authenticated Team Members',
+ description: 'Get currently authenticated team members.',
+ path: '/teams/current/members',
+ operationId: 'get-current-team-members',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Teams'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Currently authenticated team members.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/User')
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function current_team_members(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $team = auth()->user()->currentTeam();
+ $team->members->makeHidden([
+ 'pivot',
+ ]);
+
+ return response()->json(
+ serializeApiResponse($team->members),
+ );
+ }
+}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 3363d8164..9f1e4eeb8 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -81,8 +81,8 @@ class Controller extends BaseController
$token = request()->get('token');
if ($token) {
$decrypted = Crypt::decryptString($token);
- $email = Str::of($decrypted)->before('@@@');
- $password = Str::of($decrypted)->after('@@@');
+ $email = str($decrypted)->before('@@@');
+ $password = str($decrypted)->after('@@@');
$user = User::whereEmail($email)->first();
if (! $user) {
return redirect()->route('login');
@@ -110,59 +110,54 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
- public function accept_invitation()
+ public function acceptInvitation()
{
- try {
- $resetPassword = request()->query('reset-password');
- $invitationUuid = request()->route('uuid');
- $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
- $user = User::whereEmail($invitation->email)->firstOrFail();
- $invitationValid = $invitation->isValid();
- if ($invitationValid) {
- if ($resetPassword) {
- $user->update([
- 'password' => Hash::make($invitationUuid),
- 'force_password_reset' => true,
- ]);
- }
- if ($user->teams()->where('team_id', $invitation->team->id)->exists()) {
- $invitation->delete();
+ $resetPassword = request()->query('reset-password');
+ $invitationUuid = request()->route('uuid');
- return redirect()->route('team.index');
- }
- $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
+ $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
+ $user = User::whereEmail($invitation->email)->firstOrFail();
+
+ if (Auth::id() !== $user->id) {
+ abort(400, 'You are not allowed to accept this invitation.');
+ }
+ $invitationValid = $invitation->isValid();
+
+ if ($invitationValid) {
+ if ($resetPassword) {
+ $user->update([
+ 'password' => Hash::make($invitationUuid),
+ 'force_password_reset' => true,
+ ]);
+ }
+ if ($user->teams()->where('team_id', $invitation->team->id)->exists()) {
$invitation->delete();
- if (auth()->user()?->id !== $user->id) {
- return redirect()->route('login');
- }
- refreshSession($invitation->team);
return redirect()->route('team.index');
- } else {
- abort(401);
}
- } catch (\Throwable $e) {
- ray($e->getMessage());
- throw $e;
+ $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
+ $invitation->delete();
+
+ refreshSession($invitation->team);
+
+ return redirect()->route('team.index');
+ } else {
+ abort(400, 'Invitation expired.');
}
}
public function revoke_invitation()
{
- try {
- $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
- $user = User::whereEmail($invitation->email)->firstOrFail();
- if (is_null(auth()->user())) {
- return redirect()->route('login');
- }
- if (auth()->user()->id !== $user->id) {
- abort(401);
- }
- $invitation->delete();
-
- return redirect()->route('team.index');
- } catch (\Throwable $e) {
- throw $e;
+ $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
+ $user = User::whereEmail($invitation->email)->firstOrFail();
+ if (is_null(Auth::user())) {
+ return redirect()->route('login');
}
+ if (Auth::id() !== $user->id) {
+ abort(401);
+ }
+ $invitation->delete();
+
+ return redirect()->route('team.index');
}
}
diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php
index 5b17fe926..3a3f18c9c 100644
--- a/app/Http/Controllers/OauthController.php
+++ b/app/Http/Controllers/OauthController.php
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
+use Symfony\Component\HttpKernel\Exception\HttpException;
class OauthController extends Controller
{
@@ -20,6 +21,11 @@ class OauthController extends Controller
$oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first();
if (! $user) {
+ $settings = instanceSettings();
+ if (! $settings->is_registration_enabled) {
+ abort(403, 'Registration is disabled');
+ }
+
$user = User::create([
'name' => $oauthUser->name,
'email' => $oauthUser->email,
@@ -29,9 +35,9 @@ class OauthController extends Controller
return redirect('/');
} catch (\Exception $e) {
- ray($e->getMessage());
+ $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback';
- return redirect()->route('login')->withErrors([__('auth.failed.callback')]);
+ return redirect()->route('login')->withErrors([__($errorCode)]);
}
}
}
diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php
index 8e52fda32..4d34a1000 100644
--- a/app/Http/Controllers/UploadController.php
+++ b/app/Http/Controllers/UploadController.php
@@ -5,7 +5,6 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Controller as BaseController;
-use Illuminate\Support\Facades\Storage;
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
@@ -21,7 +20,7 @@ class UploadController extends BaseController
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
if ($receiver->isUploaded() === false) {
- throw new UploadMissingFileException();
+ throw new UploadMissingFileException;
}
$save = $receiver->receive();
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index b9035b755..8c74f95e5 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -16,7 +16,6 @@ class Bitbucket extends Controller
{
try {
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
@@ -55,7 +54,6 @@ class Bitbucket extends Controller
'message' => 'Nothing to do. No branch found in the request.',
]);
}
- ray('Manual webhook bitbucket push event with branch: '.$branch);
}
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
$branch = data_get($payload, 'pullrequest.destination.branch.name');
@@ -85,7 +83,6 @@ class Bitbucket extends Controller
'status' => 'failed',
'message' => 'Invalid signature.',
]);
- ray('Invalid signature');
continue;
}
@@ -96,14 +93,12 @@ class Bitbucket extends Controller
'status' => 'failed',
'message' => 'Server is not functional.',
]);
- ray('Server is not functional: '.$application->destination->server->name);
continue;
}
if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) {
- ray('Deploying '.$application->name.' with branch '.$branch);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -126,16 +121,26 @@ class Bitbucket extends Controller
}
if ($x_bitbucket_event === 'pullrequest:created') {
if ($application->isPRDeployable()) {
- ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'bitbucket',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'bitbucket',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'bitbucket',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
@@ -160,7 +165,6 @@ class Bitbucket extends Controller
}
}
if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
- ray('Pull request rejected');
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
@@ -180,12 +184,9 @@ class Bitbucket extends Controller
}
}
}
- ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
- ray($e);
-
return handleError($e);
}
}
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index 388481949..cc53f2034 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -19,15 +19,12 @@ class Gitea extends Controller
$return_payloads = collect([]);
$x_gitea_delivery = request()->header('X-Gitea-Delivery');
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$files = Storage::disk('webhooks-during-maintenance')->files();
$gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) {
return Str::contains($file, $x_gitea_delivery);
})->first();
if ($gitea_delivery_found) {
- ray('Webhook already found');
-
return;
}
$data = [
@@ -67,8 +64,6 @@ class Gitea extends Controller
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
- ray($changed_files);
- ray('Manual Webhook Gitea Push Event with branch: '.$branch);
}
if ($x_gitea_event === 'pull_request') {
$action = data_get($payload, 'action');
@@ -77,7 +72,6 @@ class Gitea extends Controller
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
- ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
}
if (! $branch) {
return response('Nothing to do. No branch found in the request.');
@@ -99,7 +93,6 @@ class Gitea extends Controller
$webhook_secret = data_get($application, 'manual_webhook_secret_gitea');
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
- ray('Invalid signature');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
@@ -122,8 +115,7 @@ class Gitea extends Controller
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
- ray('Deploying '.$application->name.' with branch '.$branch);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -162,15 +154,26 @@ class Gitea extends Controller
if ($x_gitea_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'gitea',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'gitea',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'gitea',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
@@ -216,12 +219,9 @@ class Gitea extends Controller
}
}
}
- ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 403438193..3683adaa8 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -25,15 +25,12 @@ class Github extends Controller
$return_payloads = collect([]);
$x_github_delivery = request()->header('X-GitHub-Delivery');
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$files = Storage::disk('webhooks-during-maintenance')->files();
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
return Str::contains($file, $x_github_delivery);
})->first();
if ($github_delivery_found) {
- ray('Webhook already found');
-
return;
}
$data = [
@@ -73,7 +70,6 @@ class Github extends Controller
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
- ray('Manual Webhook GitHub Push Event with branch: '.$branch);
}
if ($x_github_event === 'pull_request') {
$action = data_get($payload, 'action');
@@ -82,7 +78,6 @@ class Github extends Controller
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
- ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
}
if (! $branch) {
return response('Nothing to do. No branch found in the request.');
@@ -104,7 +99,6 @@ class Github extends Controller
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
- ray('Invalid signature');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
@@ -127,8 +121,7 @@ class Github extends Controller
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
- ray('Deploying '.$application->name.' with branch '.$branch);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -167,15 +160,26 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'github',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'github',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'github',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
@@ -221,12 +225,9 @@ class Github extends Controller
}
}
}
- ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
@@ -238,15 +239,12 @@ class Github extends Controller
$id = null;
$x_github_delivery = $request->header('X-GitHub-Delivery');
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$files = Storage::disk('webhooks-during-maintenance')->files();
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
return Str::contains($file, $x_github_delivery);
})->first();
if ($github_delivery_found) {
- ray('Webhook already found');
-
return;
}
$data = [
@@ -302,7 +300,6 @@ class Github extends Controller
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
- ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch);
}
if ($x_github_event === 'pull_request') {
$action = data_get($payload, 'action');
@@ -311,7 +308,6 @@ class Github extends Controller
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
- ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
}
if (! $id || ! $branch) {
return response('Nothing to do. No id or branch found.');
@@ -329,7 +325,6 @@ class Github extends Controller
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
-
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (! $isFunctional) {
@@ -346,8 +341,7 @@ class Github extends Controller
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
- ray('Deploying '.$application->name.' with branch '.$branch);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -386,7 +380,7 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
ApplicationPreview::create([
@@ -421,8 +415,13 @@ class Github extends Controller
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
- $container_name = generateApplicationContainerName($application, $pull_request_id);
- instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
+ $containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
+ if ($containers->isNotEmpty()) {
+ $containers->each(function ($container) use ($application) {
+ $container_name = data_get($container, 'Names');
+ instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
+ });
+ }
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();
@@ -445,8 +444,6 @@ class Github extends Controller
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
@@ -490,7 +487,6 @@ class Github extends Controller
try {
$installation_id = $request->get('installation_id');
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index a3d7712eb..d8dcc0c3b 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -17,7 +17,6 @@ class Gitlab extends Controller
{
try {
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
@@ -34,6 +33,7 @@ class Gitlab extends Controller
return;
}
+
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
@@ -49,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads);
}
+ if (empty($x_gitlab_token)) {
+ $return_payloads->push([
+ 'status' => 'failed',
+ 'message' => 'Invalid signature.',
+ ]);
+
+ return response($return_payloads);
+ }
+
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
@@ -67,7 +76,6 @@ class Gitlab extends Controller
$removed_files = data_get($payload, 'commits.*.removed');
$modified_files = data_get($payload, 'commits.*.modified');
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
- ray('Manual Webhook GitLab Push Event with branch: '.$branch);
}
if ($x_gitlab_event === 'merge_request') {
$action = data_get($payload, 'object_attributes.action');
@@ -84,7 +92,6 @@ class Gitlab extends Controller
return response($return_payloads);
}
- ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id);
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
if ($x_gitlab_event === 'push') {
@@ -117,7 +124,6 @@ class Gitlab extends Controller
'status' => 'failed',
'message' => 'Invalid signature.',
]);
- ray('Invalid signature');
continue;
}
@@ -128,7 +134,6 @@ class Gitlab extends Controller
'status' => 'failed',
'message' => 'Server is not functional',
]);
- ray('Server is not functional: '.$application->destination->server->name);
continue;
}
@@ -136,8 +141,7 @@ class Gitlab extends Controller
if ($application->isDeployable()) {
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
- ray('Deploying '.$application->name.' with branch '.$branch);
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -171,21 +175,31 @@ class Gitlab extends Controller
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
- ray('Deployments disabled for '.$application->name);
}
}
if ($x_gitlab_event === 'merge_request') {
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
if ($application->isPRDeployable()) {
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'gitlab',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'gitlab',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'gitlab',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
@@ -196,7 +210,6 @@ class Gitlab extends Controller
is_webhook: true,
git_type: 'gitlab'
);
- ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
@@ -208,7 +221,6 @@ class Gitlab extends Controller
'status' => 'failed',
'message' => 'Preview deployments disabled',
]);
- ray('Preview deployments disabled for '.$application->name);
}
} elseif ($action === 'closed' || $action === 'close' || $action === 'merge') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
@@ -242,8 +254,6 @@ class Gitlab extends Controller
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php
index e404a8ebc..e94209b23 100644
--- a/app/Http/Controllers/Webhook/Stripe.php
+++ b/app/Http/Controllers/Webhook/Stripe.php
@@ -5,15 +5,12 @@ namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
-use App\Jobs\SubscriptionTrialEndedJob;
-use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Subscription;
use App\Models\Team;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
class Stripe extends Controller
@@ -22,7 +19,6 @@ class Stripe extends Controller
{
try {
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
@@ -54,6 +50,30 @@ class Stripe extends Controller
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
+ case 'radar.early_fraud_warning.created':
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+ $id = data_get($data, 'id');
+ $charge = data_get($data, 'charge');
+ if ($charge) {
+ $stripe->refunds->create(['charge' => $charge]);
+ }
+ $pi = data_get($data, 'payment_intent');
+ $piData = $stripe->paymentIntents->retrieve($pi, []);
+ $customerId = data_get($piData, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscriptionId = data_get($subscription, 'stripe_subscription_id');
+ $stripe->subscriptions->cancel($subscriptionId, []);
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ } else {
+ send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+
+ return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
+ }
+ break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
@@ -68,18 +88,19 @@ class Stripe extends Controller
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
- throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+
+ return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
- send_internal_notification('Old subscription activated for team: '.$teamId);
+ // send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
- send_internal_notification('New subscription for team: '.$teamId);
+ // send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
@@ -92,90 +113,120 @@ class Stripe extends Controller
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
- send_internal_notification('Subscription excluded.');
+ // send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- Sleep::for(5)->seconds();
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
+ if ($subscription) {
+ $subscription->update([
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ return response("No subscription found for customer: {$customerId}", 400);
}
- $subscription->update([
- 'stripe_invoice_paid' => true,
- ]);
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
- send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
+ // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
$team = data_get($subscription, 'team');
if (! $team) {
- send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
+ // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
return response('No team found in Coolify.');
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
- send_internal_notification('Invoice payment failed: '.$customerId);
+ // send_internal_notification('Invoice payment failed: '.$customerId);
} else {
- send_internal_notification('Invoice payment failed but already paid: '.$customerId);
+ // send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
- send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
+ // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
if ($subscription->stripe_invoice_paid) {
- send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
+ // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
+ case 'customer.subscription.created':
+ $customerId = data_get($data, 'customer');
+ $subscriptionId = data_get($data, 'id');
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ if (! $teamId || ! $userId) {
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ return response("Subscription already exists for customer: {$customerId}", 200);
+ }
+
+ return response('No team id or user id found', 400);
+ }
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+
+ return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ return response("Subscription already exists for team: {$teamId}", 200);
+ } else {
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+
+ return response('Subscription created');
+ }
case 'customer.subscription.updated':
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
- send_internal_notification('Subscription excluded.');
+ // send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- Sleep::for(5)->seconds();
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- }
if (! $subscription) {
if ($status === 'incomplete_expired') {
- send_internal_notification('Subscription incomplete expired for customer: '.$customerId);
-
return response('Subscription incomplete expired', 200);
}
- send_internal_notification('No subscription found for: '.$customerId);
-
- return response('No subscription found', 400);
+ if ($teamId) {
+ $subscription = Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ } else {
+ return response('No subscription and team id found', 400);
+ }
}
- $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
- $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
- if (str($lookup_key)->contains('ultimate') || str($lookup_key)->contains('dynamic')) {
- if (str($lookup_key)->contains('dynamic')) {
- $quantity = data_get($data, 'items.data.0.quantity', 2);
- } else {
- $quantity = data_get($data, 'items.data.0.quantity', 10);
- }
+ if (str($lookup_key)->contains('dynamic')) {
+ $quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
@@ -194,28 +245,12 @@ class Stripe extends Controller
$subscription->update([
'stripe_invoice_paid' => false,
]);
- send_internal_notification('Subscription paused or incomplete for customer: '.$customerId);
}
-
- // Trial ended but subscribed, reactive servers
- if ($trialEndedAlready && $status === 'active') {
- $team = data_get($subscription, 'team');
- $team->trialEndedButSubscribed();
- }
-
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
}
- send_internal_notification($reason);
- }
- if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
- if ($cancelAtPeriodEnd) {
- // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
- } else {
- send_internal_notification('customer.subscription.updated for customer: '.$customerId);
- }
}
break;
case 'customer.subscription.deleted':
@@ -223,42 +258,7 @@ class Stripe extends Controller
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
- if ($team) {
- $team->trialEnded();
- }
- $subscription->update([
- 'stripe_subscription_id' => null,
- 'stripe_plan_id' => null,
- 'stripe_cancel_at_period_end' => false,
- 'stripe_invoice_paid' => false,
- 'stripe_trial_already_ended' => true,
- ]);
- send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
- break;
- case 'customer.subscription.trial_will_end':
- // Not used for now
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- throw new Exception('No team found for subscription: '.$subscription->id);
- }
- SubscriptionTrialEndsSoonJob::dispatch($team);
- break;
- case 'customer.subscription.paused':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- throw new Exception('No team found for subscription: '.$subscription->id);
- }
- $team->trialEnded();
- $subscription->update([
- 'stripe_trial_already_ended' => true,
- 'stripe_invoice_paid' => false,
- ]);
- SubscriptionTrialEndedJob::dispatch($team);
- send_internal_notification('Subscription paused for customer: '.$customerId);
+ $team?->subscriptionEnded();
break;
default:
// Unhandled event type
diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php
index ea635836c..dec8ca72d 100644
--- a/app/Http/Controllers/Webhook/Waitlist.php
+++ b/app/Http/Controllers/Webhook/Waitlist.php
@@ -13,7 +13,6 @@ class Waitlist extends Controller
{
$email = request()->get('email');
$confirmation_code = request()->get('confirmation_code');
- ray($email, $confirmation_code);
try {
$found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found) {
@@ -36,7 +35,6 @@ class Waitlist extends Controller
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist confirmation failed: '.$e->getMessage());
- ray($e->getMessage());
return redirect()->route('dashboard');
}
@@ -58,7 +56,6 @@ class Waitlist extends Controller
return redirect()->route('dashboard');
} catch (Exception $e) {
send_internal_notification('Waitlist cancellation failed: '.$e->getMessage());
- ray($e->getMessage());
return redirect()->route('dashboard');
}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index e29c4a307..5f1731071 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -67,5 +67,7 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+ 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
+ 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
}
diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php
new file mode 100644
index 000000000..dc6be5da3
--- /dev/null
+++ b/app/Http/Middleware/ApiAllowed.php
@@ -0,0 +1,32 @@
+is_api_enabled === false) {
+ return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
+ }
+
+ if (! isDev()) {
+ if ($settings->allowed_ips) {
+ $allowedIps = explode(',', $settings->allowed_ips);
+ if (! in_array($request->ip(), $allowedIps)) {
+ return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
+ }
+ }
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php
new file mode 100644
index 000000000..bd6cd1f8a
--- /dev/null
+++ b/app/Http/Middleware/IgnoreReadOnlyApiToken.php
@@ -0,0 +1,28 @@
+user()->currentAccessToken();
+ if ($token->can('*')) {
+ return $next($request);
+ }
+ if ($token->can('read-only')) {
+ return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php
new file mode 100644
index 000000000..8ff1fa0e5
--- /dev/null
+++ b/app/Http/Middleware/OnlyRootApiToken.php
@@ -0,0 +1,25 @@
+user()->currentAccessToken();
+ if ($token->can('*')) {
+ return $next($request);
+ }
+
+ return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
+ }
+}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 72d8c0ad1..27f77f7a1 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
+use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\Server;
@@ -25,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;
@@ -108,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;
@@ -126,7 +130,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private string $dockerfile_location = '/Dockerfile';
- private string $docker_compose_location = '/docker-compose.yml';
+ private string $docker_compose_location = '/docker-compose.yaml';
private ?string $docker_compose_custom_start_command = null;
@@ -156,6 +160,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
+ private bool $preserveRepository = false;
+
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
@@ -163,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;
@@ -186,6 +193,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->is_this_additional_server = $this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0;
+ $this->preserveRepository = $this->application->settings->is_preserve_repository_enabled;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}".rtrim($this->application->base_directory, '/');
@@ -193,9 +201,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
- ray('New container name: ', $this->container_name);
+ if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
+ 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}";
+ }
+ }
- savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -212,12 +225,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
+ public function tags(): array
+ {
+ return ['server:'.gethostname()];
+ }
+
public function handle(): void
{
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
- if (! $this->server->isFunctional()) {
+ if ($this->server->isFunctional() === false) {
$this->application_deployment_queue->addLogEntry('Server is not functional.');
$this->fail('Server is not functional.');
@@ -269,6 +287,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;
@@ -283,7 +302,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
}
- ray($e);
$this->fail($e);
throw $e;
} finally {
@@ -300,14 +318,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]
);
- // $this->execute_remote_command(
- // [
- // "docker image prune -f >/dev/null 2>&1",
- // "hidden" => true,
- // "ignore_errors" => true,
- // ]
- // );
-
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
@@ -339,7 +349,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
- GetContainersStatus::dispatch($this->server);
+ GetContainersStatus::dispatch($this->server)->onQueue('high');
// dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
@@ -382,7 +392,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->dockerImageTag = $this->application->docker_registry_image_tag;
}
- ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
$this->generate_image_names();
$this->prepare_builder_image();
@@ -415,15 +424,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']);
@@ -440,11 +476,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.');
@@ -458,7 +495,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
- $command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build";
+ $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@@ -474,49 +511,65 @@ 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();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
- $this->write_deployment_configurations();
} else {
$this->write_deployment_configurations();
- $server_workdir = $this->application->workdir();
+ $this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
if ($this->env_filename) {
- $command .= " --env-file {$this->workdir}/{$this->env_filename}";
+ $command .= " --env-file {$server_workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
-
$this->execute_remote_command(
['command' => $command, 'hidden' => true],
);
}
} else {
if ($this->docker_compose_custom_start_command) {
+ $this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
- $this->write_deployment_configurations();
} else {
$command = "{$this->coolify_variables} docker compose";
- if ($this->env_filename) {
- $command .= " --env-file {$this->workdir}/{$this->env_filename}";
+ 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->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();
}
- $command .= " --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();
}
}
@@ -601,26 +654,54 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function write_deployment_configurations()
{
+ if ($this->preserveRepository) {
+ if ($this->use_build_server) {
+ $this->server = $this->original_server;
+ }
+ if (str($this->configuration_dir)->isNotEmpty()) {
+ $this->execute_remote_command(
+ [
+ "mkdir -p $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;
+ }
+ }
if (isset($this->docker_compose_base64)) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
+
+ $mainDir = $this->configuration_dir;
+ if ($this->application->settings->is_raw_compose_deployment_enabled) {
+ $mainDir = $this->application->workdir();
+ }
if ($this->pull_request_id === 0) {
- $composeFileName = "$this->configuration_dir/docker-compose.yml";
+ $composeFileName = "$mainDir/docker-compose.yaml";
} else {
- $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
- $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
+ $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml";
+ $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml";
}
$this->execute_remote_command(
[
- "mkdir -p $this->configuration_dir",
+ "mkdir -p $mainDir",
],
[
"echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null",
],
[
- "echo '{$readme}' > $this->configuration_dir/README.md",
+ "echo '{$readme}' > $mainDir/README.md",
]
);
if ($this->use_build_server) {
@@ -633,45 +714,34 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{
$forceFail = true;
if (str($this->application->docker_registry_image_name)->isEmpty()) {
- ray('empty docker_registry_image_name');
-
return;
}
if ($this->restart_only) {
- ray('restart_only');
-
return;
}
if ($this->application->build_pack === 'dockerimage') {
- ray('dockerimage');
-
return;
}
if ($this->use_build_server) {
- ray('use_build_server');
$forceFail = true;
}
if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') {
- ray('isSwarm');
$forceFail = true;
}
if ($this->application->additional_servers->count() > 0) {
- ray('additional_servers');
$forceFail = true;
}
if ($this->is_this_additional_server) {
- ray('this is an additional_servers, no pushy pushy');
-
return;
}
- ray('push_to_docker_registry noww: '.$this->production_image_name);
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->application_deployment_queue->addLogEntry('----------------------------------------');
$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) {
@@ -679,10 +749,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,
],
);
}
@@ -691,7 +765,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($forceFail) {
throw new RuntimeException($e->getMessage(), 69420);
}
- ray($e);
}
}
@@ -779,14 +852,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',
]);
}
}
@@ -819,14 +898,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->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') {
@@ -841,8 +930,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
- if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
- $envs->push("PORT={$ports[0]}");
+ if ($this->build_pack !== 'dockercompose') {
+ if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
+ $envs->push("PORT={$ports[0]}");
+ }
}
// Add HOST if not exists
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
@@ -859,15 +950,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->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') {
@@ -877,21 +984,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$real_value = '\''.$real_value.'\'';
} else {
$real_value = escapeEnvVariables($env->real_value);
- ray($real_value);
}
}
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
- if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
- $envs->push("PORT={$ports[0]}");
+ if ($this->build_pack !== 'dockercompose') {
+ if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
+ $envs->push("PORT={$ports[0]}");
+ }
}
// Add HOST if not exists
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
}
-
if ($envs->isEmpty()) {
$this->env_filename = null;
if ($this->use_build_server) {
@@ -944,21 +1051,76 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
}
}
+ $this->environment_variables = $envs;
}
- private function framework_based_notification()
+ private function elixir_finetunes()
{
- // Laravel old env variables
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';
}
- if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) {
- $this->application_deployment_queue->addLogEntry('There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel', 'stderr');
+ $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) {
+ $envType = 'environment_variables';
+ } else {
+ $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';
+ $nixpacks_php_fallback_path->value = '/index.php';
+ $nixpacks_php_fallback_path->is_build_time = false;
+ $nixpacks_php_fallback_path->application_id = $this->application->id;
+ $nixpacks_php_fallback_path->save();
+ }
+ if (! $nixpacks_php_root_dir) {
+ $nixpacks_php_root_dir = new EnvironmentVariable;
+ $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
+ $nixpacks_php_root_dir->value = '/app/public';
+ $nixpacks_php_root_dir->is_build_time = false;
+ $nixpacks_php_root_dir->application_id = $this->application->id;
+ $nixpacks_php_root_dir->save();
+ }
+
+ return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir];
}
private function rolling_update()
@@ -976,7 +1138,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
- if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
+ if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
@@ -984,7 +1146,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
}
- if (isset($this->application->settings->custom_internal_name)) {
+ if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
}
if ($this->pull_request_id !== 0) {
@@ -1005,7 +1167,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
}
}
- $this->framework_based_notification();
}
private function health_check()
@@ -1059,13 +1220,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
}
- if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->application_deployment_queue->addLogEntry('New container is healthy.');
break;
}
- if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
$this->newVersionIsHealthy = false;
$this->query_logs();
break;
@@ -1077,7 +1238,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$sleeptime++;
}
}
- if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
+ if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
$this->query_logs();
}
}
@@ -1161,7 +1322,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);
@@ -1212,8 +1375,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return;
}
if ($destination_ids->contains($this->destination->id)) {
- ray('Same destination found in additional destinations. Skipping.');
-
return;
}
foreach ($destination_ids as $destination_id) {
@@ -1225,7 +1386,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue;
}
// ray('Deploying to additional destination: ', $server->name);
- $deployment_uuid = new Cuid2();
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
@@ -1281,10 +1442,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(
@@ -1313,7 +1474,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->execute_remote_command(
[
- $importCommands, 'hidden' => true,
+ $importCommands,
+ 'hidden' => true,
]
);
$this->create_workdir();
@@ -1366,17 +1528,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
}
}
+
if ($this->saved_outputs->get('nixpacks_plan')) {
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
if ($this->nixpacks_plan) {
$this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}.");
$this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}");
$parsed = Toml::Parse($this->nixpacks_plan);
+
// Do any modifications here
$this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
if (count($aptPkgs) === 0) {
+ $aptPkgs = ['curl', 'wget'];
data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']);
} else {
if (! in_array('curl', $aptPkgs)) {
@@ -1388,8 +1553,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs);
}
data_set($parsed, 'variables', $merged_envs->toArray());
+ $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false);
+ if ($is_laravel) {
+ $variables = $this->laravel_finetunes();
+ 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') {
+ // temporary: disable healthcheck for rust because the start phase does not have curl/wget
+ $this->application->health_check_enabled = false;
+ $this->application->save();
+ }
}
}
}
@@ -1492,7 +1671,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save();
} else {
- $labels = collect(generateLabelsApplication($this->application, $this->preview));
+ if (! $this->application->settings->is_container_label_readonly_enabled) {
+ $labels = collect(generateLabelsApplication($this->application, $this->preview));
+ }
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
@@ -1507,9 +1688,12 @@ 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::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
+ $dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
}
$docker_compose = [
@@ -1542,23 +1726,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
],
];
- if (isset($this->application->settings->custom_internal_name)) {
- $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name;
- }
- // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) {
- // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
- // $docker_compose['services'][$this->container_name]['env_file'][] = '.env';
- // } else {
- // $docker_compose['services'][$this->container_name]['env_file'] = ['.env'];
- // }
- // }
- // if ($this->env_filename) {
- // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
- // $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename;
- // } else {
- // $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
- // }
- // }
if (! is_null($this->env_filename)) {
$docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
}
@@ -1610,12 +1777,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
],
];
+ if (data_get($this->application, 'swarm_placement_constraints')) {
+ $swarm_placement_constraints = Yaml::parse(base64_decode(data_get($this->application, 'swarm_placement_constraints')));
+ $docker_compose['services'][$this->container_name]['deploy'] = array_merge(
+ $docker_compose['services'][$this->container_name]['deploy'],
+ $swarm_placement_constraints
+ );
+ }
if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) {
- $docker_compose['services'][$this->container_name]['deploy']['placement'] = [
- 'constraints' => [
- 'node.role == worker',
- ],
- ];
+ $docker_compose['services'][$this->container_name]['deploy']['placement']['constraints'][] = 'node.role == worker';
}
if ($this->pull_request_id !== 0) {
$docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1;
@@ -1624,14 +1794,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'] = [
@@ -1658,43 +1821,46 @@ 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;
}
- // if ($this->build_pack === 'dockerfile') {
- // $docker_compose['services'][$this->container_name]['build'] = [
- // 'context' => $this->workdir,
- // 'dockerfile' => $this->workdir . $this->dockerfile_location,
- // ];
- // }
if ($this->pull_request_id === 0) {
- $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
+ $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options);
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
- $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
- if (count($custom_compose) > 0) {
- $ipv4 = data_get($custom_compose, 'ip.0');
- $ipv6 = data_get($custom_compose, 'ip6.0');
- data_forget($custom_compose, 'ip');
- data_forget($custom_compose, 'ip6');
- if ($ipv4 || $ipv6) {
- data_forget($docker_compose['services'][$this->application->uuid], 'networks');
+ if (! $this->application->settings->custom_internal_name) {
+ $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
+ if (count($custom_compose) > 0) {
+ $ipv4 = data_get($custom_compose, 'ip.0');
+ $ipv6 = data_get($custom_compose, 'ip6.0');
+ data_forget($custom_compose, 'ip');
+ data_forget($custom_compose, 'ip6');
+ if ($ipv4 || $ipv6) {
+ data_forget($docker_compose['services'][$this->application->uuid], 'networks');
+ }
+ if ($ipv4) {
+ $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
+ }
+ if ($ipv6) {
+ $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
+ }
+ $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
- if ($ipv4) {
- $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
- }
- if ($ipv6) {
- $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
- }
- $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
} else {
if (count($custom_compose) > 0) {
@@ -1718,7 +1884,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]);
}
private function generate_local_persistent_volumes()
@@ -1791,13 +1957,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.');
@@ -1819,35 +1995,44 @@ COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
} else {
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
$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}"), '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}"), '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}";
}
+
+ $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, '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]);
} else {
if ($this->force_rebuild) {
@@ -1859,36 +2044,29 @@ 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,
]
);
}
-
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
-
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
@@ -1900,10 +2078,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 {
@@ -1917,10 +2101,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 {
@@ -1929,13 +2119,32 @@ 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}"), '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}"), '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, '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]);
} else {
if ($this->force_rebuild) {
@@ -1947,10 +2156,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,
]
);
}
@@ -1959,27 +2174,62 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
+ private function graceful_shutdown_container(string $containerName, int $timeout = 300)
+ {
+ try {
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ break;
+ }
+ usleep(100000);
+ }
+
+ $isRunning = $this->execute_remote_command(
+ ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
+ ) === 'true';
+
+ if ($isRunning) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
+ } catch (\Exception $error) {
+ $this->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)
{
$this->application_deployment_queue->addLogEntry('Removing old containers.');
if ($this->newVersionIsHealthy || $force) {
- $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
- if ($this->pull_request_id === 0) {
- $containers = $containers->filter(function ($container) {
- return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
+ if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
+ $this->graceful_shutdown_container($this->container_name);
+ } else {
+ $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
+ if ($this->pull_request_id === 0) {
+ $containers = $containers->filter(function ($container) {
+ return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
+ });
+ }
+ $containers->each(function ($container) {
+ $this->graceful_shutdown_container(data_get($container, 'Names'));
});
}
- $containers->each(function ($container) {
- $containerName = data_get($container, 'Names');
- $this->execute_remote_command(
- ["docker rm -f $containerName >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
- );
- });
- if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) {
- $this->execute_remote_command(
- ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
- );
- }
} else {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
@@ -1990,45 +2240,26 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
]);
- $this->execute_remote_command(
- ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
- );
+ $this->graceful_shutdown_container($this->container_name);
}
}
- private function build_by_compose_file()
- {
- $this->application_deployment_queue->addLogEntry('Pulling & building required images.');
- if ($this->application->build_pack === 'dockerimage') {
- $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), 'hidden' => true],
- );
- } else {
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true],
- );
- }
- $this->application_deployment_queue->addLogEntry('New images built.');
- }
-
private function start_by_compose_file()
{
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
);
} else {
if ($this->use_build_server) {
$this->execute_remote_command(
- ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
+ ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
);
} else {
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
);
}
}
@@ -2049,17 +2280,16 @@ 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::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
+ $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (data_get($env, 'is_multiline') === true) {
@@ -2075,7 +2305,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"));
@@ -2103,7 +2332,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,
],
);
@@ -2130,7 +2360,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) {
@@ -2181,13 +2413,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack !== 'dockercompose') {
$code = $exception->getCode();
- ray($code);
if ($code !== 69420) {
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
- $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr');
- $this->execute_remote_command(
- ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true]
- );
+ if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
+ // do not remove already running container
+ } else {
+ $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr');
+ $this->execute_remote_command(
+ ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
}
}
}
diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php
index d400642dd..2eefc4dd2 100755
--- a/app/Jobs/ApplicationPullRequestUpdateJob.php
+++ b/app/Jobs/ApplicationPullRequestUpdateJob.php
@@ -25,15 +25,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
- ) {
- }
+ ) {}
public function handle()
{
try {
if ($this->application->is_public_repository()) {
- ray('Public repository. Skipping comment update.');
-
return;
}
if ($this->status === ProcessStatus::CLOSED) {
@@ -54,16 +51,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
$this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n";
$this->body .= 'Last updated at: '.now()->toDateTimeString().' CET';
-
- ray('Updating comment', $this->body);
if ($this->preview->pull_request_issue_comment_id) {
$this->update_comment();
} else {
$this->create_comment();
}
} catch (\Throwable $e) {
- ray($e);
-
return $e;
}
}
@@ -74,7 +67,6 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
'body' => $this->body,
], throwError: false);
if (data_get($data, 'message') === 'Not Found') {
- ray('Comment not found. Creating new one.');
$this->create_comment();
}
}
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/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php
new file mode 100644
index 000000000..788db89ea
--- /dev/null
+++ b/app/Jobs/CheckAndStartSentinelJob.php
@@ -0,0 +1,52 @@
+server, false);
+ $sentinelFoundJson = json_decode($sentinelFound, true);
+ $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
+ if ($sentinelStatus !== 'running') {
+ StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
+
+ return;
+ }
+ // If sentinel is running, check if it needs an update
+ $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
+ if (empty($runningVersion)) {
+ $runningVersion = '0.0.0';
+ }
+ if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') {
+ StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest');
+
+ return;
+ } else {
+ if (version_compare($runningVersion, $latestVersion, '<')) {
+ StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
+
+ return;
+ }
+ }
+ }
+}
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
new file mode 100644
index 000000000..f2348118a
--- /dev/null
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -0,0 +1,44 @@
+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]);
+ }
+ }
+ } catch (\Throwable $e) {
+ // Consider implementing a notification to administrators
+ }
+ }
+}
diff --git a/app/Jobs/CheckHelperImageJob.php b/app/Jobs/CheckHelperImageJob.php
new file mode 100644
index 000000000..6abb8a150
--- /dev/null
+++ b/app/Jobs/CheckHelperImageJob.php
@@ -0,0 +1,39 @@
+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, '>')) {
+ $settings->update(['helper_version' => $latest_version]);
+ }
+ }
+ } catch (\Throwable $e) {
+ send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage());
+ throw $e;
+ }
+ }
+}
diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php
deleted file mode 100644
index 312200f66..000000000
--- a/app/Jobs/CheckLogDrainContainerJob.php
+++ /dev/null
@@ -1,95 +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/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php
index 8f2039ef2..7479867b6 100644
--- a/app/Jobs/CheckResaleLicenseJob.php
+++ b/app/Jobs/CheckResaleLicenseJob.php
@@ -14,9 +14,7 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct()
- {
- }
+ public function __construct() {}
public function handle(): void
{
@@ -24,7 +22,6 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue
CheckResaleLicense::run();
} catch (\Throwable $e) {
send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage());
- ray($e);
throw $e;
}
}
diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index 418c7a0f4..f185ab781 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -15,26 +15,20 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public Server $server)
- {
- }
+ public function __construct(public Server $server) {}
public function handle(): void
{
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');
- ray('Removing container '.$containerId);
+ $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) {
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}
}
} catch (\Throwable $e) {
send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage());
- ray($e->getMessage());
}
}
}
diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php
index b846ad2bc..84f14ed02 100644
--- a/app/Jobs/CleanupInstanceStuffsJob.php
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -3,54 +3,37 @@
namespace App\Jobs;
use App\Models\TeamInvitation;
-use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
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\Log;
class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct()
+ public function __construct() {}
+
+ public function middleware(): array
{
-
+ return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
}
- // public function uniqueId(): string
- // {
- // return $this->container_name;
- // }
-
public function handle(): void
{
try {
- // $this->cleanup_waitlist();
+ $this->cleanupInvitationLink();
} catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
- ray($e->getMessage());
- }
- try {
- $this->cleanup_invitation_link();
- } catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
- ray($e->getMessage());
+ Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
}
}
- private function cleanup_waitlist()
- {
- $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
- foreach ($waitlist as $item) {
- $item->delete();
- }
- }
-
- private function cleanup_invitation_link()
+ private function cleanupInvitationLink()
{
$invitation = TeamInvitation::all();
foreach ($invitation as $item) {
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 c50d17d4c..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
@@ -23,19 +22,7 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
- 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 __construct(public Server $server) {}
public function handle()
{
diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php
index e5f4dfd5e..c3692c30b 100755
--- a/app/Jobs/CoolifyTask.php
+++ b/app/Jobs/CoolifyTask.php
@@ -20,11 +20,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
*/
public function __construct(
public Activity $activity,
- public bool $ignore_errors = false,
- public $call_event_on_finish = null,
- public $call_event_data = null
- ) {
- }
+ public bool $ignore_errors,
+ public $call_event_on_finish,
+ public $call_event_data,
+ ) {}
/**
* Execute the job.
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 07386988c..fcfe2fe3d 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,55 +54,47 @@ 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 {
+ $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::class) {
+ $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);
- // Check if team is exists
- if (is_null($this->team)) {
- $this->backup->update(['status' => 'failed']);
- StopDatabase::run($this->database);
- $this->database->delete();
- return;
- }
- $status = Str::of(data_get($this->database, 'status'));
+ $status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
- ray('database not running');
-
return;
}
- if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
+ if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) {
$databaseType = $this->database->databaseType();
$serviceUuid = $this->database->service->uuid;
$serviceName = str($this->database->service->name)->slug();
@@ -133,6 +123,12 @@ 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;
@@ -223,7 +219,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
// Format: db1:collection1,collection2|db2:collection3,collection4
$databasesToBackup = explode('|', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
- ray($databasesToBackup);
} elseif (str($databaseType)->contains('mysql')) {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
@@ -236,8 +231,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
}
- $this->backup_dir = backup_dir().'/databases/'.Str::of($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
-
+ $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';
@@ -246,10 +240,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
foreach ($databasesToBackup as $database) {
$size = 0;
- ray('Backing up '.$database);
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,
@@ -278,6 +274,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,
@@ -287,6 +286,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,
@@ -325,18 +327,19 @@ 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);
+ }
}
}
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
- ray($this->database->toArray());
- $url = $this->database->get_db_url(useInternal: true);
+ $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 +354,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";
@@ -369,10 +372,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup_output === '') {
$this->backup_output = null;
}
- ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
- ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage());
throw $e;
}
}
@@ -381,16 +382,24 @@ 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;
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
- ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
- ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage());
throw $e;
}
}
@@ -399,17 +408,18 @@ 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 === '') {
$this->backup_output = null;
}
- ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
- ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage());
throw $e;
}
}
@@ -418,17 +428,18 @@ 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";
- ray($commands);
+ 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";
+ }
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
- ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
- ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage());
throw $e;
}
}
@@ -452,7 +463,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);
@@ -472,17 +483,35 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection(shouldSave: true);
- if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
+ if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) {
$network = $this->database->service->destination->network;
} else {
$network = $this->database->destination->network;
}
- $commands[] = "docker run --pull=always -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";
+
+ $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 +520,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
instant_remote_process([$command], $this->server);
}
}
+
+ 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 cf240e0d7..000000000
--- a/app/Jobs/DatabaseBackupStatusJob.php
+++ /dev/null
@@ -1,64 +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 6d4720f6b..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;
@@ -28,17 +29,22 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false)
- {
- }
+ public function __construct(
+ public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
+ public bool $deleteConfigurations = true,
+ public bool $deleteVolumes = true,
+ public bool $dockerCleanup = true,
+ public bool $deleteConnectedNetworks = true
+ ) {}
public function handle()
{
try {
- $this->resource->forceDelete();
+ $persistentStorages = collect();
switch ($this->resource->type()) {
case 'application':
- StopApplication::run($this->resource);
+ $persistentStorages = $this->resource?->persistentStorages()?->get();
+ StopApplication::run($this->resource, previewDeployments: true);
break;
case 'standalone-postgresql':
case 'standalone-redis':
@@ -48,21 +54,46 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
- StopDatabase::run($this->resource);
+ $persistentStorages = $this->resource?->persistentStorages()?->get();
+ 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;
}
+
+ if ($this->deleteVolumes && $this->resource->type() !== 'service') {
+ $this->resource?->delete_volumes($persistentStorages);
+ }
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 32c41e99c..0d7e63dd2 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -10,60 +10,63 @@ 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\Log;
-use RuntimeException;
class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $timeout = 300;
+ public $timeout = 600;
- public ?int $usageBefore = null;
+ public $tries = 1;
- public function __construct(public Server $server)
+ public ?string $usageBefore = null;
+
+ public function middleware(): array
{
+ return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
+ public function __construct(public Server $server, public bool $manualCleanup = false) {}
+
public function handle(): void
{
try {
- $isInprogress = false;
- $this->server->applications()->each(function ($application) use (&$isInprogress) {
- if ($application->isDeploymentInprogress()) {
- $isInprogress = true;
-
- return;
- }
- });
- if ($isInprogress) {
- throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
- }
if (! $this->server->isFunctional()) {
return;
}
+
+ 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;
+ }
+
$this->usageBefore = $this->server->getDiskUsage();
- ray('Usage before: '.$this->usageBefore);
- if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
- ray('Cleaning up '.$this->server->name);
- CleanupDocker::run($this->server);
+ 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);
+
+ return;
+ }
+ 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.'));
- // ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
- // send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name);
} else {
Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name);
}
} else {
- ray('No need to clean up '.$this->server->name);
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
- send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage());
- 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 bab8f3a25..d483fe4c2 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;
@@ -23,19 +22,7 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
- 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 __construct(public GithubApp $github_app) {}
public function handle()
{
@@ -55,7 +42,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
} catch (\Throwable $e) {
send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage());
- ray($e->getMessage());
throw $e;
}
}
diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php
deleted file mode 100644
index bce60bbc8..000000000
--- a/app/Jobs/InstanceAutoUpdateJob.php
+++ /dev/null
@@ -1,30 +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);
-
- $settings = InstanceSettings::get();
- $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;
- }
- instant_remote_process([
- 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
- "bash /data/coolify/source/upgrade.sh $latest_version",
- ], $server);
- } catch (\Throwable $e) {
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index d3bda2ea1..a92e44c6b 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.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 PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -17,31 +16,12 @@ 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 Server $server) {}
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');
- } catch (\Throwable $e) {
- send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
- ray($e->getMessage());
- throw $e;
- }
+ $helperImage = config('coolify.helper_image');
+ $latest_version = instanceSettings()->helper_version;
+ instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}
}
diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php
deleted file mode 100644
index 1dd4b1dd3..000000000
--- a/app/Jobs/PullSentinelImageJob.php
+++ /dev/null
@@ -1,60 +0,0 @@
-server->uuid))];
- }
-
- public function uniqueId(): string
- {
- return $this->server->uuid;
- }
-
- public function __construct(public Server $server)
- {
- }
-
- public function handle(): void
- {
- try {
- $version = get_latest_sentinel_version();
- if (! $version) {
- ray('Failed to get latest Sentinel version');
-
- return;
- }
- $local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
- if (empty($local_version)) {
- $local_version = '0.0.0';
- }
- if (version_compare($local_version, $version, '<')) {
- StartSentinel::run($this->server, $version, true);
-
- return;
- }
- ray('Sentinel image is up to date');
- } catch (\Throwable $e) {
- send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
- ray($e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php
index 948060033..bde5e6c7a 100644
--- a/app/Jobs/PullTemplatesFromCDN.php
+++ b/app/Jobs/PullTemplatesFromCDN.php
@@ -17,26 +17,23 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10;
- public function __construct()
- {
- }
+ public function __construct() {}
public function handle(): void
{
try {
- if (! isDev()) {
- ray('PullTemplatesAndVersions service-templates');
- $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 {
- send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
- }
+ if (isDev() || isCloud()) {
+ return;
+ }
+ $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 {
+ send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
}
} catch (\Throwable $e) {
send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage());
- ray($e->getMessage());
}
}
}
diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php
deleted file mode 100644
index 1ad4989de..000000000
--- a/app/Jobs/PullVersionsFromCDN.php
+++ /dev/null
@@ -1,41 +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));
- } else {
- send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
- }
- }
- } catch (\Throwable $e) {
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
new file mode 100644
index 000000000..9822ca071
--- /dev/null
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -0,0 +1,366 @@
+containers = collect();
+ $this->foundApplicationIds = collect();
+ $this->foundDatabaseUuids = collect();
+ $this->foundServiceApplicationIds = collect();
+ $this->foundApplicationPreviewsIds = collect();
+ $this->foundServiceDatabaseIds = collect();
+ $this->allApplicationIds = collect();
+ $this->allDatabaseUuids = collect();
+ $this->allTcpProxyUuids = collect();
+ $this->allServiceApplicationIds = collect();
+ $this->allServiceDatabaseIds = collect();
+ }
+
+ public function handle()
+ {
+ // TODO: Swarm is not supported yet
+ if (! $this->data) {
+ throw new \Exception('No data provided');
+ }
+ $data = collect($this->data);
+
+ $this->server->sentinelHeartbeat();
+
+ $this->containers = collect(data_get($data, 'containers'));
+
+ $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
+ ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
+
+ if ($this->containers->isEmpty()) {
+ return;
+ }
+ $this->applications = $this->server->applications();
+ $this->databases = $this->server->databases();
+ $this->previews = $this->server->previews();
+ $this->services = $this->server->services()->get();
+ $this->allApplicationIds = $this->applications->filter(function ($application) {
+ return $application->additional_servers->count() === 0;
+ })->pluck('id');
+ $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
+ return $application->additional_servers->count() > 0;
+ });
+ $this->allApplicationPreviewsIds = $this->previews->pluck('id');
+ $this->allDatabaseUuids = $this->databases->pluck('uuid');
+ $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
+ $this->services->each(function ($service) {
+ $service->applications()->pluck('id')->each(function ($applicationId) {
+ $this->allServiceApplicationIds->push($applicationId);
+ });
+ $service->databases()->pluck('id')->each(function ($databaseId) {
+ $this->allServiceDatabaseIds->push($databaseId);
+ });
+ });
+
+ foreach ($this->containers as $container) {
+ $containerStatus = data_get($container, 'state', 'exited');
+ $containerHealth = data_get($container, 'health_status', 'unhealthy');
+ $containerStatus = "$containerStatus ($containerHealth)";
+ $labels = collect(data_get($container, 'labels'));
+ $coolify_managed = $labels->has('coolify.managed');
+ if ($coolify_managed) {
+ $name = data_get($container, 'name');
+ if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
+ $this->foundLogDrainContainer = true;
+ }
+ if ($labels->has('coolify.applicationId')) {
+ $applicationId = $labels->get('coolify.applicationId');
+ $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
+ try {
+ if ($pullRequestId === '0') {
+ if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
+ $this->foundApplicationIds->push($applicationId);
+ }
+ $this->updateApplicationStatus($applicationId, $containerStatus);
+ } else {
+ if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) {
+ $this->foundApplicationPreviewsIds->push($applicationId);
+ }
+ $this->updateApplicationPreviewStatus($applicationId, $containerStatus);
+ }
+ } catch (\Exception $e) {
+ }
+ } elseif ($labels->has('coolify.serviceId')) {
+ $serviceId = $labels->get('coolify.serviceId');
+ $subType = $labels->get('coolify.service.subType');
+ $subId = $labels->get('coolify.service.subId');
+ if ($subType === 'application' && $this->isRunning($containerStatus)) {
+ $this->foundServiceApplicationIds->push($subId);
+ $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
+ } elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
+ $this->foundServiceDatabaseIds->push($subId);
+ $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
+ }
+ } else {
+ $uuid = $labels->get('com.docker.compose.service');
+ $type = $labels->get('coolify.type');
+ if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
+ $this->foundProxy = true;
+ } elseif ($type === 'service' && $this->isRunning($containerStatus)) {
+ } else {
+ if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->foundDatabaseUuids->push($uuid);
+ if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
+ } else {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $this->updateProxyStatus();
+
+ $this->updateNotFoundApplicationStatus();
+ $this->updateNotFoundApplicationPreviewStatus();
+ $this->updateNotFoundDatabaseStatus();
+ $this->updateNotFoundServiceStatus();
+
+ $this->updateAdditionalServersStatus();
+
+ $this->checkLogDrainContainer();
+ }
+
+ private function updateApplicationStatus(string $applicationId, string $containerStatus)
+ {
+ $application = $this->applications->where('id', $applicationId)->first();
+ if (! $application) {
+ return;
+ }
+ $application->status = $containerStatus;
+ $application->save();
+ }
+
+ private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
+ {
+ $application = $this->previews->where('id', $applicationId)->first();
+ if (! $application) {
+ return;
+ }
+ $application->status = $containerStatus;
+ $application->save();
+ }
+
+ private function updateNotFoundApplicationStatus()
+ {
+ $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
+ if ($notFoundApplicationIds->isNotEmpty()) {
+ $notFoundApplicationIds->each(function ($applicationId) {
+ $application = Application::find($applicationId);
+ if ($application) {
+ $application->status = 'exited';
+ $application->save();
+ }
+ });
+ }
+ }
+
+ private function updateNotFoundApplicationPreviewStatus()
+ {
+ $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
+ if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
+ $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
+ $applicationPreview = ApplicationPreview::find($applicationPreviewId);
+ if ($applicationPreview) {
+ $applicationPreview->status = 'exited';
+ $applicationPreview->save();
+ }
+ });
+ }
+ }
+
+ private function updateProxyStatus()
+ {
+ // If proxy is not found, start it
+ if ($this->server->isProxyShouldRun()) {
+ if ($this->foundProxy === false) {
+ try {
+ if (CheckProxy::run($this->server)) {
+ StartProxy::run($this->server, false);
+ $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
+ }
+ } catch (\Throwable $e) {
+ }
+ } else {
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
+ instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
+ }
+ }
+ }
+
+ private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false)
+ {
+ $database = $this->databases->where('uuid', $databaseUuid)->first();
+ if (! $database) {
+ return;
+ }
+ $database->status = $containerStatus;
+ $database->save();
+ if ($this->isRunning($containerStatus) && $tcpProxy) {
+ $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
+ return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
+ })->first();
+ if (! $tcpProxyContainerFound) {
+ StartDatabaseProxy::dispatch($database);
+ $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ } else {
+ }
+ }
+ }
+
+ private function updateNotFoundDatabaseStatus()
+ {
+ $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
+ if ($notFoundDatabaseUuids->isNotEmpty()) {
+ $notFoundDatabaseUuids->each(function ($databaseUuid) {
+ $database = $this->databases->where('uuid', $databaseUuid)->first();
+ if ($database) {
+ $database->status = 'exited';
+ $database->save();
+ if ($database->is_public) {
+ StopDatabaseProxy::dispatch($database);
+ }
+ }
+ });
+ }
+ }
+
+ private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
+ {
+ $service = $this->services->where('id', $serviceId)->first();
+ if (! $service) {
+ return;
+ }
+ if ($subType === 'application') {
+ $application = $service->applications()->where('id', $subId)->first();
+ $application->status = $containerStatus;
+ $application->save();
+ } elseif ($subType === 'database') {
+ $database = $service->databases()->where('id', $subId)->first();
+ $database->status = $containerStatus;
+ $database->save();
+ } else {
+ }
+ }
+
+ private function updateNotFoundServiceStatus()
+ {
+ $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
+ $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
+ if ($notFoundServiceApplicationIds->isNotEmpty()) {
+ $notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
+ $application = ServiceApplication::find($serviceApplicationId);
+ if ($application) {
+ $application->status = 'exited';
+ $application->save();
+ }
+ });
+ }
+ if ($notFoundServiceDatabaseIds->isNotEmpty()) {
+ $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
+ $database = ServiceDatabase::find($serviceDatabaseId);
+ if ($database) {
+ $database->status = 'exited';
+ $database->save();
+ }
+ });
+ }
+ }
+
+ private function updateAdditionalServersStatus()
+ {
+ $this->allApplicationsWithAdditionalServers->each(function ($application) {
+ ComplexStatusCheck::run($application);
+ });
+ }
+
+ private function isRunning(string $containerStatus)
+ {
+ return str($containerStatus)->contains('running');
+ }
+
+ private function checkLogDrainContainer()
+ {
+ if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
+ StartLogDrain::dispatch($this->server)->onQueue('high');
+ }
+ }
+}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 819e28f89..7bfc29af3 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Events\ScheduledTaskDone;
use App\Models\Application;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
@@ -13,14 +14,13 @@ 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
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public ?Team $team = null;
+ public Team $team;
public Server $server;
@@ -36,6 +36,8 @@ class ScheduledTaskJob implements ShouldQueue
public array $containers = [];
+ public string $server_timezone;
+
public function __construct($task)
{
$this->task = $task;
@@ -46,17 +48,19 @@ class ScheduledTaskJob implements ShouldQueue
} else {
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
- $this->team = Team::find($task->team_id);
+ $this->team = Team::findOrFail($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) {
+ return $this->resource->destination->server->settings->server_timezone;
+ } elseif ($this->resource instanceof Service) {
+ return $this->resource->server->settings->server_timezone;
+ }
- public function uniqueId(): int
- {
- return $this->task->id;
+ return 'UTC';
}
public function handle(): void
@@ -68,14 +72,14 @@ class ScheduledTaskJob implements ShouldQueue
$this->server = $this->resource->destination->server;
- if ($this->resource->type() == 'application') {
+ if ($this->resource->type() === 'application') {
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) {
$containers->each(function ($container) {
$this->containers[] = str_replace('/', '', $container['Names']);
});
}
- } elseif ($this->resource->type() == 'service') {
+ } elseif ($this->resource->type() === 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
$this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
@@ -121,6 +125,8 @@ class ScheduledTaskJob implements ShouldQueue
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
+ } finally {
+ ScheduledTaskDone::dispatch($this->team->id);
}
}
}
diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php
index 4d5618df0..7af8205fc 100755
--- a/app/Jobs/SendConfirmationForWaitlistJob.php
+++ b/app/Jobs/SendConfirmationForWaitlistJob.php
@@ -14,14 +14,12 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public string $email, public string $uuid)
- {
- }
+ public function __construct(public string $email, public string $uuid) {}
public function handle()
{
try {
- $mail = new MailMessage();
+ $mail = new MailMessage;
$confirmation_url = base_url().'/webhooks/waitlist/confirm?email='.$this->email.'&confirmation_code='.$this->uuid;
$cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid;
$mail->view('emails.waitlist-confirmation',
@@ -33,7 +31,6 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue
send_user_an_email($mail, $this->email);
} catch (\Throwable $e) {
send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage());
- ray($e->getMessage());
throw $e;
}
}
diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php
index 90f2e0b30..5b406f50f 100644
--- a/app/Jobs/SendMessageToDiscordJob.php
+++ b/app/Jobs/SendMessageToDiscordJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -29,19 +30,15 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public int $maxExceptions = 5;
public function __construct(
- public string $text,
+ public DiscordMessage $message,
public string $webhookUrl
- ) {
- }
+ ) {}
/**
* Execute the job.
*/
public function handle(): void
{
- $payload = [
- 'content' => $this->text,
- ];
- Http::post($this->webhookUrl, $payload);
+ Http::post($this->webhookUrl, $this->message->toPayload());
}
}
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index b81bbc50b..bf52b782f 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -33,8 +33,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token,
public string $chatId,
public ?string $topicId = null,
- ) {
- }
+ ) {}
/**
* Execute the job.
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
new file mode 100644
index 000000000..c584f493d
--- /dev/null
+++ b/app/Jobs/ServerCheckJob.php
@@ -0,0 +1,103 @@
+server->id))->dontRelease()];
+ }
+
+ public function __construct(public Server $server) {}
+
+ public function handle()
+ {
+ try {
+ if ($this->server->serverStatus() === false) {
+ return 'Server is not reachable or not ready.';
+ }
+
+ if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
+ ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
+ if (is_null($this->containers)) {
+ return 'No containers found.';
+ }
+ GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
+
+ if ($this->server->isSentinelEnabled()) {
+ CheckAndStartSentinelJob::dispatch($this->server);
+ }
+
+ if ($this->server->isLogDrainEnabled()) {
+ $this->checkLogDrainContainer();
+ }
+
+ if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
+ $this->server->proxyType();
+ $foundProxyContainer = $this->containers->filter(function ($value, $key) {
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
+ } else {
+ return data_get($value, 'Name') === '/coolify-proxy';
+ }
+ })->first();
+ if (! $foundProxyContainer) {
+ try {
+ $shouldStart = CheckProxy::run($this->server);
+ if ($shouldStart) {
+ StartProxy::run($this->server, false);
+ $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
+ }
+ } catch (\Throwable $e) {
+ }
+ } else {
+ $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
+ $this->server->save();
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
+ instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+
+ private function checkLogDrainContainer()
+ {
+ $foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
+ return data_get($value, 'Name') === '/coolify-log-drain';
+ })->first();
+ if ($foundLogDrainContainer) {
+ $status = data_get($foundLogDrainContainer, 'State.Status');
+ if ($status !== 'running') {
+ StartLogDrain::dispatch($this->server)->onQueue('high');
+ }
+ } else {
+ StartLogDrain::dispatch($this->server)->onQueue('high');
+ }
+ }
+}
diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php
new file mode 100644
index 000000000..3e8e60a31
--- /dev/null
+++ b/app/Jobs/ServerCheckNewJob.php
@@ -0,0 +1,34 @@
+server);
+ ResourcesCheck::dispatch($this->server);
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+}
diff --git a/app/Jobs/ServerCleanupMux.php b/app/Jobs/ServerCleanupMux.php
new file mode 100644
index 000000000..b793c3eca
--- /dev/null
+++ b/app/Jobs/ServerCleanupMux.php
@@ -0,0 +1,40 @@
+server->serverStatus() === false) {
+ return 'Server is not reachable or not ready.';
+ }
+ SshMultiplexingHelper::removeMuxFile($this->server);
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+}
diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php
index 2476c12dd..769dfc004 100644
--- a/app/Jobs/ServerFilesFromServerJob.php
+++ b/app/Jobs/ServerFilesFromServerJob.php
@@ -16,9 +16,7 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
- {
- }
+ public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {}
public function handle()
{
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 3eaf88ba7..aa82c6dad 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
@@ -24,30 +23,15 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
- 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 __construct(public Team $team) {}
public function handle()
{
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
- $limit = data_get($this->team->limits, 'serverLimit', 2);
- $number_of_servers_to_disable = $servers_count - $limit;
- ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
+ $number_of_servers_to_disable = $servers_count - $this->team->limits;
if ($number_of_servers_to_disable > 0) {
- ray('Disabling servers');
$servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable);
$servers_to_disable->each(function ($server) {
@@ -64,7 +48,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
}
} catch (\Throwable $e) {
send_internal_notification('ServerLimitCheckJob failed with: '.$e->getMessage());
- ray($e->getMessage());
return handleError($e);
}
diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php
deleted file mode 100644
index aaf8f5784..000000000
--- a/app/Jobs/ServerStatusJob.php
+++ /dev/null
@@ -1,137 +0,0 @@
-server->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->uuid;
- }
-
- public function handle()
- {
- if (! $this->server->isServerReady($this->tries)) {
- throw new \RuntimeException('Server is not ready.');
- }
- try {
- if ($this->server->isFunctional()) {
- $this->cleanup(notify: false);
- $this->remove_unnecessary_coolify_yaml();
- if (config('coolify.is_sentinel_enabled')) {
- $this->server->checkSentinel();
- }
- }
- } catch (\Throwable $e) {
- send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
- ray($e->getMessage());
-
- return handleError($e);
- }
- try {
- // $this->check_docker_engine();
- } catch (\Throwable $e) {
- // Do nothing
- }
- }
-
- private function check_docker_engine()
- {
- $version = instant_remote_process([
- 'docker info',
- ], $this->server, false);
- if (is_null($version)) {
- $os = instant_remote_process([
- 'cat /etc/os-release | grep ^ID=',
- ], $this->server, false);
- $os = str($os)->after('ID=')->trim();
- if ($os === 'ubuntu') {
- try {
- instant_remote_process([
- 'systemctl start docker',
- ], $this->server);
- } catch (\Throwable $e) {
- ray($e->getMessage());
-
- return handleError($e);
- }
- } else {
- try {
- instant_remote_process([
- 'service docker start',
- ], $this->server);
- } catch (\Throwable $e) {
- ray($e->getMessage());
-
- return handleError($e);
- }
- }
- }
- }
-
- private function remove_unnecessary_coolify_yaml()
- {
- // This will remote the coolify.yaml file from the server as it is not needed on cloud servers
- if (isCloud() && $this->server->id !== 0) {
- $file = $this->server->proxyPath().'/dynamic/coolify.yaml';
-
- return instant_remote_process([
- "rm -f $file",
- ], $this->server, false);
- }
- }
-
- public function cleanup(bool $notify = false): void
- {
- $this->disk_usage = $this->server->getDiskUsage();
- if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) {
- if ($notify) {
- if ($this->server->high_disk_usage_notification_sent) {
- ray('high disk usage notification already sent');
-
- return;
- } else {
- $this->server->high_disk_usage_notification_sent = true;
- $this->server->save();
- $this->server->team?->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
- }
- } else {
- DockerCleanupJob::dispatchSync($this->server);
- $this->cleanup(notify: true);
- }
- } else {
- $this->server->high_disk_usage_notification_sent = false;
- $this->server->save();
- }
- }
-}
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
new file mode 100644
index 000000000..0723ffcee
--- /dev/null
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -0,0 +1,65 @@
+server->isFunctional() === false) {
+ return 'Server is not functional.';
+ }
+ $team = data_get($this->server, 'team');
+ $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold');
+
+ if (is_null($this->percentage)) {
+ $this->percentage = $this->server->storageCheck();
+ }
+ if (! $this->percentage) {
+ return 'No percentage could be retrieved.';
+ }
+ if ($this->percentage > $serverDiskUsageNotificationThreshold) {
+ $executed = RateLimiter::attempt(
+ 'high-disk-usage:'.$this->server->id,
+ $maxAttempts = 0,
+ function () use ($team, $serverDiskUsageNotificationThreshold) {
+ $team->notify(new HighDiskUsage($this->server, $this->percentage, $serverDiskUsageNotificationThreshold));
+ },
+ $decaySeconds = 3600,
+ );
+
+ if (! $executed) {
+ return 'Too many messages sent!';
+ }
+ } else {
+ RateLimiter::hit('high-disk-usage:'.$this->server->id, 600);
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+}
diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php
index c94a3edc5..526cd5375 100644
--- a/app/Jobs/ServerStorageSaveJob.php
+++ b/app/Jobs/ServerStorageSaveJob.php
@@ -14,9 +14,7 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public LocalFileVolume $localFileVolume)
- {
- }
+ public function __construct(public LocalFileVolume $localFileVolume) {}
public function handle()
{
diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php
index e4cd219c8..aabeecef5 100755
--- a/app/Jobs/SubscriptionInvoiceFailedJob.php
+++ b/app/Jobs/SubscriptionInvoiceFailedJob.php
@@ -15,28 +15,24 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(protected Team $team)
- {
- }
+ public function __construct(protected Team $team) {}
public function handle()
{
try {
$session = getStripeCustomerPortalSession($this->team);
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->view('emails.subscription-invoice-failed', [
'stripeCustomerPortal' => $session->url,
]);
$mail->subject('Your last payment was failed for Coolify Cloud.');
$this->team->members()->each(function ($member) use ($mail) {
- ray($member);
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionInvoiceFailedJob failed with: '.$e->getMessage());
- ray($e->getMessage());
throw $e;
}
}
diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php
deleted file mode 100755
index ee260d8d9..000000000
--- a/app/Jobs/SubscriptionTrialEndedJob.php
+++ /dev/null
@@ -1,45 +0,0 @@
-team);
- $mail = new MailMessage();
- $mail->subject('Action required: You trial in Coolify Cloud ended.');
- $mail->view('emails.trial-ended', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- ray('Sending trial ended email to '.$member->email);
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- ray($e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php
deleted file mode 100755
index fba668108..000000000
--- a/app/Jobs/SubscriptionTrialEndsSoonJob.php
+++ /dev/null
@@ -1,45 +0,0 @@
-team);
- $mail = new MailMessage();
- $mail->subject('You trial in Coolify Cloud ends soon.');
- $mail->view('emails.trial-ends-soon', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- ray('Sending trial ending email to '.$member->email);
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- ray($e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
new file mode 100644
index 000000000..1e5197b6f
--- /dev/null
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -0,0 +1,49 @@
+new_version_available) {
+ Log::info('No new version available. Skipping update.');
+
+ return;
+ }
+
+ $server = Server::findOrFail(0);
+ if (! $server) {
+ Log::error('Server not found. Cannot proceed with update.');
+
+ return;
+ }
+
+ Log::info('Starting Coolify update process...');
+ UpdateCoolify::run(false); // false means it's not a manual update
+
+ $settings->update(['new_version_available' => false]);
+ Log::info('Coolify update completed successfully.');
+ } catch (\Throwable $e) {
+ Log::error('UpdateCoolifyJob failed: '.$e->getMessage());
+ // Consider implementing a notification to administrators
+ }
+ }
+}
diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php
index 9f676ca99..6c3ab83d8 100644
--- a/app/Listeners/MaintenanceModeDisabledNotification.php
+++ b/app/Listeners/MaintenanceModeDisabledNotification.php
@@ -9,13 +9,10 @@ use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class MaintenanceModeDisabledNotification
{
- public function __construct()
- {
- }
+ public function __construct() {}
public function handle(EventsMaintenanceModeDisabled $event): void
{
- ray('Maintenance mode disabled!');
$files = Storage::disk('webhooks-during-maintenance')->files();
$files = collect($files);
$files = $files->sort();
@@ -40,10 +37,9 @@ class MaintenanceModeDisabledNotification
$class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value());
$method = str($endpoint)->after('::')->value();
try {
- $instance = new $class();
+ $instance = new $class;
$instance->$method($request);
} catch (\Throwable $th) {
- ray($th);
} finally {
Storage::disk('webhooks-during-maintenance')->delete($file);
}
diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php
index b2cd8c738..5aab248ea 100644
--- a/app/Listeners/MaintenanceModeEnabledNotification.php
+++ b/app/Listeners/MaintenanceModeEnabledNotification.php
@@ -17,8 +17,5 @@ class MaintenanceModeEnabledNotification
/**
* Handle the event.
*/
- public function handle(EventsMaintenanceModeEnabled $event): void
- {
- ray('Maintenance mode enabled!');
- }
+ public function handle(EventsMaintenanceModeEnabled $event): void {}
}
diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php
index 64271cc52..d0541b162 100644
--- a/app/Listeners/ProxyStartedNotification.php
+++ b/app/Listeners/ProxyStartedNotification.php
@@ -9,9 +9,7 @@ class ProxyStartedNotification
{
public Server $server;
- public function __construct()
- {
- }
+ public function __construct() {}
public function handle(ProxyStarted $event): void
{
diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php
index bd1e30088..2e36f34ee 100644
--- a/app/Livewire/ActivityMonitor.php
+++ b/app/Livewire/ActivityMonitor.php
@@ -2,7 +2,6 @@
namespace App\Livewire;
-use App\Enums\ProcessStatus;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index 26b31e515..359db6329 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -2,76 +2,60 @@
namespace App\Livewire\Admin;
+use App\Models\Team;
use App\Models\User;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Index extends Component
{
- public $active_subscribers = [];
+ public int $activeSubscribers;
- public $inactive_subscribers = [];
+ public int $inactiveSubscribers;
- public $search = '';
+ public Collection $foundUsers;
- public function submitSearch()
- {
- if ($this->search !== '') {
- $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->where(function ($query) {
- $query->where('name', 'like', "%{$this->search}%")
- ->orWhere('email', 'like', "%{$this->search}%");
- })->get()->filter(function ($user) {
- return $user->id !== 0;
- });
- $this->active_subscribers = User::whereHas('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->where(function ($query) {
- $query->where('name', 'like', "%{$this->search}%")
- ->orWhere('email', 'like', "%{$this->search}%");
- })->get()->filter(function ($user) {
- return $user->id !== 0;
- });
- } else {
- $this->getSubscribers();
- }
- }
+ public string $search = '';
public function mount()
{
if (! isCloud()) {
return redirect()->route('dashboard');
}
- if (auth()->user()->id !== 0) {
+
+ if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
+ public function submitSearch()
+ {
+ if ($this->search !== '') {
+ $this->foundUsers = User::where(function ($query) {
+ $query->where('name', 'like', "%{$this->search}%")
+ ->orWhere('email', 'like', "%{$this->search}%");
+ })->get();
+ }
+ }
+
public function getSubscribers()
{
- $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->get()->filter(function ($user) {
- return $user->id !== 0;
- });
- $this->active_subscribers = User::whereHas('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->get()->filter(function ($user) {
- return $user->id !== 0;
- });
+ $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
+ $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
public function switchUser(int $user_id)
{
- if (auth()->user()->id !== 0) {
+ if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
- auth()->login($user);
+ Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index b787ed0cc..c9c3092b3 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -12,7 +12,7 @@ use Livewire\Component;
class Index extends Component
{
- protected $listeners = ['serverInstalled' => 'validateServer'];
+ protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
public string $currentState = 'welcome';
@@ -85,26 +85,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerDescription = 'Created by Coolify';
$this->remoteServerHost = 'coolify-testing-host';
}
- // if ($this->currentState === 'create-project') {
- // $this->getProjects();
- // }
- // if ($this->currentState === 'create-resource') {
- // $this->selectExistingServer();
- // $this->selectExistingProject();
- // }
- // if ($this->currentState === 'private-key') {
- // $this->setServerType('remote');
- // }
- // if ($this->currentState === 'create-server') {
- // $this->selectExistingPrivateKey();
- // }
- // if ($this->currentState === 'validate-server') {
- // $this->selectExistingServer();
- // }
- // if ($this->currentState === 'select-existing-server') {
- // $this->selectExistingServer();
- // }
-
}
public function explanation()
@@ -139,7 +119,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 +134,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,14 +153,23 @@ 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
- $this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
+ $this->selectProxy(ProxyTypes::TRAEFIK->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {
// $this->currentState = 'select-proxy';
@@ -219,27 +209,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) {
@@ -257,7 +255,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save();
- $this->createdServer->addInitialNetwork();
$this->selectedExistingServer = $this->createdServer->id;
$this->currentState = 'validate-server';
}
@@ -270,7 +267,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);
@@ -278,9 +275,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);
}
@@ -297,6 +297,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
+ $this->createdServer->settings()->update([
+ 'is_usable' => false,
+ ]);
+
return handleError(error: $e, livewire: $this);
}
}
@@ -350,6 +354,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..69ba19e40 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -16,29 +16,28 @@ class Dashboard extends Component
public Collection $servers;
- public Collection $private_keys;
+ public Collection $privateKeys;
- public $deployments_per_server;
+ public array $deploymentsPerServer = [];
public function mount()
{
- $this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
+ $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
- $this->get_deployments();
+ $this->loadDeployments();
}
- public function cleanup_queue()
+ public function cleanupQueue()
{
- $this->dispatch('success', 'Cleanup started.');
- Artisan::queue('cleanup:application-deployment-queue', [
+ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id,
]);
}
- public function get_deployments()
+ public function loadDeployments()
{
- $this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([
+ $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([
'id',
'application_id',
'application_name',
@@ -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
deleted file mode 100644
index 7125f2120..000000000
--- a/app/Livewire/Destination/Form.php
+++ /dev/null
@@ -1,46 +0,0 @@
- 'required',
- 'destination.network' => 'required',
- 'destination.server.ip' => 'required',
- ];
-
- protected $validationAttributes = [
- 'destination.name' => 'name',
- 'destination.network' => 'network',
- 'destination.server.ip' => 'IP Address/Domain',
- ];
-
- public function submit()
- {
- $this->validate();
- $this->destination->save();
- }
-
- public function delete()
- {
- try {
- if ($this->destination->getMorphClass() === 'App\Models\StandaloneDocker') {
- if ($this->destination->attachedTo()) {
- return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
- }
- instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
- instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
- }
- $this->destination->delete();
-
- return redirect()->route('dashboard');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-}
diff --git a/app/Livewire/Destination/Index.php b/app/Livewire/Destination/Index.php
new file mode 100644
index 000000000..a3df3fd56
--- /dev/null
+++ b/app/Livewire/Destination/Index.php
@@ -0,0 +1,23 @@
+servers = Server::isUsable()->get();
+ }
+
+ public function render()
+ {
+ return view('livewire.destination.index');
+ }
+}
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index f822cfa5f..f86f42e34 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -3,111 +3,91 @@
namespace App\Livewire\Destination\New;
use App\Models\Server;
-use App\Models\StandaloneDocker as ModelsStandaloneDocker;
+use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
-use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Docker extends Component
{
+ #[Locked]
+ public $servers;
+
+ #[Locked]
+ public Server $selectedServer;
+
+ #[Validate(['required', 'string'])]
public string $name;
+ #[Validate(['required', 'string'])]
public string $network;
- public ?Collection $servers = null;
+ #[Validate(['required', 'string'])]
+ public string $serverId;
- public Server $server;
+ #[Validate(['required', 'boolean'])]
+ public bool $isSwarm = false;
- public ?int $server_id = null;
-
- public bool $is_swarm = false;
-
- protected $rules = [
- 'name' => 'required|string',
- 'network' => 'required|string',
- 'server_id' => 'required|integer',
- 'is_swarm' => 'boolean',
- ];
-
- protected $validationAttributes = [
- 'name' => 'name',
- 'network' => 'network',
- 'server_id' => 'server',
- 'is_swarm' => 'swarm',
- ];
-
- public function mount()
+ public function mount(?string $server_id = null)
{
- if (is_null($this->servers)) {
- $this->servers = Server::isReachable()->get();
- }
- if (request()->query('server_id')) {
- $this->server_id = request()->query('server_id');
+ $this->network = new Cuid2;
+ $this->servers = Server::isUsable()->get();
+ if ($server_id) {
+ $this->selectedServer = $this->servers->find($server_id);
+ $this->serverId = $this->selectedServer->id;
} else {
- if ($this->servers->count() > 0) {
- $this->server_id = $this->servers->first()->id;
- }
- }
- if (request()->query('network_name')) {
- $this->network = request()->query('network_name');
- } else {
- $this->network = new Cuid2(7);
- }
- if ($this->servers->count() > 0) {
- $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();
+ $this->selectedServer = $this->servers->first();
+ $this->serverId = $this->selectedServer->id;
}
+ $this->generateName();
}
- public function generate_name()
+ public function updatedServerId()
{
- $this->server = Server::find($this->server_id);
- $this->name = str("{$this->server->name}-{$this->network}")->kebab();
+ $this->selectedServer = $this->servers->find($this->serverId);
+ $this->generateName();
+ }
+
+ public function generateName()
+ {
+ $name = data_get($this->selectedServer, 'name', new Cuid2);
+ $this->name = str("{$name}-{$this->network}")->kebab();
}
public function submit()
{
- $this->validate();
try {
- $this->server = Server::find($this->server_id);
- if ($this->is_swarm) {
- $found = $this->server->swarmDockers()->where('network', $this->network)->first();
+ $this->validate();
+ if ($this->isSwarm) {
+ $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first();
if ($found) {
- $this->dispatch('error', 'Network already added to this server.');
-
- return;
+ throw new \Exception('Network already added to this server.');
} else {
$docker = SwarmDocker::create([
'name' => $this->name,
'network' => $this->network,
- 'server_id' => $this->server_id,
+ 'server_id' => $this->selectedServer->id,
]);
}
} else {
- $found = $this->server->standaloneDockers()->where('network', $this->network)->first();
+ $found = $this->selectedServer->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
- $this->dispatch('error', 'Network already added to this server.');
-
- return;
+ throw new \Exception('Network already added to this server.');
} else {
- $docker = ModelsStandaloneDocker::create([
+ $docker = StandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
- 'server_id' => $this->server_id,
+ 'server_id' => $this->selectedServer->id,
]);
}
}
- $this->createNetworkAndAttachToProxy();
-
- return redirect()->route('destination.show', $docker->uuid);
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer);
+ instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false);
+ $this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
-
- private function createNetworkAndAttachToProxy()
- {
- $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
- instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
- }
}
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 5650e82ba..5c4d6c170 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -5,71 +5,91 @@ namespace App\Livewire\Destination;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
-use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Show extends Component
{
- public Server $server;
+ #[Locked]
+ public $destination;
- public Collection|array $networks = [];
+ #[Validate(['string', 'required'])]
+ public string $name;
- private function createNetworkAndAttachToProxy()
+ #[Validate(['string', 'required'])]
+ public string $network;
+
+ #[Validate(['string', 'required'])]
+ public string $serverIp;
+
+ public function mount(string $destination_uuid)
{
- $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
- instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
- }
+ try {
+ $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ??
+ SwarmDocker::whereUuid($destination_uuid)->firstOrFail();
- public function add($name)
- {
- if ($this->server->isSwarm()) {
- $found = $this->server->swarmDockers()->where('network', $name)->first();
- if ($found) {
- $this->dispatch('error', 'Network already added to this server.');
-
- return;
- } else {
- SwarmDocker::create([
- 'name' => $this->server->name.'-'.$name,
- 'network' => $this->name,
- 'server_id' => $this->server->id,
- ]);
+ $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) {
+ if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) {
+ $this->destination = $destination;
+ $this->syncData();
+ }
+ });
+ if ($ownedByTeam === false) {
+ return redirect()->route('destination.index');
}
- } else {
- $found = $this->server->standaloneDockers()->where('network', $name)->first();
- if ($found) {
- $this->dispatch('error', 'Network already added to this server.');
-
- return;
- } else {
- StandaloneDocker::create([
- 'name' => $this->server->name.'-'.$name,
- 'network' => $name,
- 'server_id' => $this->server->id,
- ]);
- }
- $this->createNetworkAndAttachToProxy();
+ $this->destination = $destination;
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
- public function scan()
+ public function syncData(bool $toModel = false)
{
- if ($this->server->isSwarm()) {
- $alreadyAddedNetworks = $this->server->swarmDockers;
+ if ($toModel) {
+ $this->validate();
+ $this->destination->name = $this->name;
+ $this->destination->network = $this->network;
+ $this->destination->server->ip = $this->serverIp;
+ $this->destination->save();
} else {
- $alreadyAddedNetworks = $this->server->standaloneDockers;
+ $this->name = $this->destination->name;
+ $this->network = $this->destination->network;
+ $this->serverIp = $this->destination->server->ip;
}
- $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
- $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
- return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';
- })->filter(function ($network) use ($alreadyAddedNetworks) {
- return ! $alreadyAddedNetworks->contains('network', $network['Name']);
- });
- if ($this->networks->count() === 0) {
- $this->dispatch('success', 'No new networks found.');
+ }
- return;
+ public function submit()
+ {
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'Destination saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->dispatch('success', 'Scan done.');
+ }
+
+ public function delete()
+ {
+ try {
+ if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) {
+ if ($this->destination->attachedTo()) {
+ return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
+ }
+ instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
+ instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
+ }
+ $this->destination->delete();
+
+ return redirect()->route('destination.index');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.destination.show');
}
}
diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php
index a732ef1c9..61a2a20e9 100644
--- a/app/Livewire/ForcePasswordReset.php
+++ b/app/Livewire/ForcePasswordReset.php
@@ -4,6 +4,7 @@ namespace App\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Support\Facades\Hash;
+use Illuminate\Validation\Rules\Password;
use Livewire\Component;
class ForcePasswordReset extends Component
@@ -16,14 +17,19 @@ class ForcePasswordReset extends Component
public string $password_confirmation;
- protected $rules = [
- 'email' => 'required|email',
- 'password' => 'required|min:8',
- 'password_confirmation' => 'required|same:password',
- ];
+ public function rules(): array
+ {
+ return [
+ 'email' => ['required', 'email'],
+ 'password' => ['required', Password::defaults(), 'confirmed'],
+ ];
+ }
public function mount()
{
+ if (auth()->user()->force_password_reset === false) {
+ return redirect()->route('dashboard');
+ }
$this->email = auth()->user()->email;
}
@@ -34,6 +40,10 @@ class ForcePasswordReset extends Component
public function submit()
{
+ if (auth()->user()->force_password_reset === false) {
+ return redirect()->route('dashboard');
+ }
+
try {
$this->rateLimit(10);
$this->validate();
diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php
index 2fbd2bc7e..f51527fbe 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -2,59 +2,42 @@
namespace App\Livewire;
-use App\Models\InstanceSettings;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\Route;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Help extends Component
{
use WithRateLimiting;
+ #[Validate(['required', 'min:10', 'max:1000'])]
public string $description;
+ #[Validate(['required', 'min:3'])]
public string $subject;
- public ?string $path = null;
-
- protected $rules = [
- 'description' => 'required|min:10',
- 'subject' => 'required|min:3',
- ];
-
- public function mount()
- {
- $this->path = Route::current()?->uri() ?? null;
- if (isDev()) {
- $this->description = "I'm having trouble with {$this->path}";
- $this->subject = "Help with {$this->path}";
- }
- }
-
public function submit()
{
try {
- $this->rateLimit(3, 30);
$this->validate();
- $debug = "Route: {$this->path}";
- $mail = new MailMessage();
+ $this->rateLimit(3, 30);
+
+ $settings = instanceSettings();
+ $mail = new MailMessage;
$mail->view(
'emails.help',
[
'description' => $this->description,
- 'debug' => $debug,
]
);
$mail->subject("[HELP]: {$this->subject}");
- $settings = InstanceSettings::get();
$type = set_transanctional_email_settings($settings);
- if (! $type) {
+
+ // Sending feedback through Cloud API
+ if ($type === false) {
$url = 'https://app.coolify.io/api/feedback';
- if (isDev()) {
- $url = 'http://localhost:80/api/feedback';
- }
Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
]);
@@ -62,6 +45,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/MonacoEditor.php b/app/Livewire/MonacoEditor.php
new file mode 100644
index 000000000..42d276e64
--- /dev/null
+++ b/app/Livewire/MonacoEditor.php
@@ -0,0 +1,51 @@
+ '$refresh',
+ ];
+
+ public function __construct(
+ public ?string $id,
+ public ?string $name,
+ public ?string $type,
+ public ?string $monacoContent,
+ public ?string $value,
+ public ?string $label,
+ public ?string $placeholder,
+ public bool $required,
+ public bool $disabled,
+ public bool $readonly,
+ public bool $allowTab,
+ public bool $spellcheck,
+ public ?string $helper,
+ public bool $realtimeValidation,
+ public bool $allowToPeak,
+ public string $defaultClass,
+ public string $defaultClassInput,
+ public ?string $language
+
+ ) {
+ //
+ }
+
+ public function render()
+ {
+ if (is_null($this->id)) {
+ $this->id = new Cuid2;
+ }
+
+ if (is_null($this->name)) {
+ $this->name = $this->id;
+ }
+
+ return view('components.forms.monaco-editor');
+ }
+}
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
new file mode 100644
index 000000000..cc5d78f60
--- /dev/null
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -0,0 +1,54 @@
+team = currentTeam()->name;
+ }
+
+ public function delete($password)
+ {
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+ }
+
+ $currentTeam = currentTeam();
+ $currentTeam->delete();
+
+ $currentTeam->members->each(function ($user) use ($currentTeam) {
+ if ($user->id === AttributesAuth::id()) {
+ return;
+ }
+ $user->teams()->detach($currentTeam);
+ $session = DB::table('sessions')->where('user_id', $user->id)->first();
+ if ($session) {
+ DB::table('sessions')->where('id', $session->id)->delete();
+ }
+ });
+
+ refreshSession();
+
+ return redirect()->route('team.index');
+ }
+
+ public function render()
+ {
+ return view('livewire.navbar-delete-team');
+ }
+}
diff --git a/app/Livewire/NewActivityMonitor.php b/app/Livewire/NewActivityMonitor.php
index 10dbb9ce7..a9334e710 100644
--- a/app/Livewire/NewActivityMonitor.php
+++ b/app/Livewire/NewActivityMonitor.php
@@ -68,7 +68,6 @@ class NewActivityMonitor extends Component
} else {
$this->dispatch($this->eventToDispatch);
}
- ray('Dispatched event: '.$this->eventToDispatch.' with data: '.$this->eventData);
}
}
}
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index f2219bbc6..7a177a227 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -4,60 +4,124 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Discord extends Component
{
public Team $team;
- protected $rules = [
- 'team.discord_enabled' => 'nullable|boolean',
- 'team.discord_webhook_url' => 'required|url',
- 'team.discord_notifications_test' => 'nullable|boolean',
- 'team.discord_notifications_deployments' => 'nullable|boolean',
- 'team.discord_notifications_status_changes' => 'nullable|boolean',
- 'team.discord_notifications_database_backups' => 'nullable|boolean',
- 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
- ];
+ #[Validate(['boolean'])]
+ public bool $discordEnabled = false;
- protected $validationAttributes = [
- 'team.discord_webhook_url' => 'Discord Webhook',
- ];
+ #[Validate(['url', 'nullable'])]
+ public ?string $discordWebhookUrl = null;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsTest = false;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsDeployments = false;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsStatusChanges = false;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsDatabaseBackups = false;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsScheduledTasks = false;
+
+ #[Validate(['boolean'])]
+ public bool $discordNotificationsServerDiskUsage = false;
public function mount()
{
- $this->team = auth()->user()->currentTeam();
+ try {
+ $this->team = auth()->user()->currentTeam();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->discord_enabled = $this->discordEnabled;
+ $this->team->discord_webhook_url = $this->discordWebhookUrl;
+ $this->team->discord_notifications_test = $this->discordNotificationsTest;
+ $this->team->discord_notifications_deployments = $this->discordNotificationsDeployments;
+ $this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges;
+ $this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups;
+ $this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks;
+ $this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->discordEnabled = $this->team->discord_enabled;
+ $this->discordWebhookUrl = $this->team->discord_webhook_url;
+ $this->discordNotificationsTest = $this->team->discord_notifications_test;
+ $this->discordNotificationsDeployments = $this->team->discord_notifications_deployments;
+ $this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes;
+ $this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups;
+ $this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks;
+ $this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage;
+ }
+ }
+
+ public function instantSaveDiscordEnabled()
+ {
+ try {
+ $this->validate([
+ 'discordWebhookUrl' => 'required',
+ ], [
+ 'discordWebhookUrl.required' => 'Discord Webhook URL is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->discordEnabled = false;
+
+ return handleError($e, $this);
+ }
}
public function instantSave()
{
try {
- $this->submit();
+ $this->syncData(true);
} catch (\Throwable $e) {
- ray($e->getMessage());
- $this->team->discord_enabled = false;
- $this->validate();
+ return handleError($e, $this);
}
}
public function submit()
{
- $this->resetErrorBag();
- $this->validate();
- $this->saveModel();
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
- $this->team?->notify(new Test());
- $this->dispatch('success', 'Test notification sent.');
+ try {
+ $this->team->notify(new Test);
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 91c108edc..56f07f3a9 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -2,118 +2,199 @@
namespace App\Livewire\Notifications;
-use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Test;
+use Illuminate\Support\Facades\RateLimiter;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Email extends Component
{
public Team $team;
+ #[Locked]
public string $emails;
- public bool $sharedEmailEnabled = false;
+ #[Validate(['boolean'])]
+ public bool $smtpEnabled = false;
- protected $rules = [
- 'team.smtp_enabled' => 'nullable|boolean',
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- 'team.smtp_recipients' => 'nullable',
- 'team.smtp_host' => 'required',
- 'team.smtp_port' => 'required',
- 'team.smtp_encryption' => 'nullable',
- 'team.smtp_username' => 'nullable',
- 'team.smtp_password' => 'nullable',
- 'team.smtp_timeout' => 'nullable',
- 'team.smtp_notifications_test' => 'nullable|boolean',
- 'team.smtp_notifications_deployments' => 'nullable|boolean',
- 'team.smtp_notifications_status_changes' => 'nullable|boolean',
- 'team.smtp_notifications_database_backups' => 'nullable|boolean',
- 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
- 'team.use_instance_email_settings' => 'boolean',
- 'team.resend_enabled' => 'nullable|boolean',
- 'team.resend_api_key' => 'nullable',
- ];
+ #[Validate(['boolean'])]
+ public bool $useInstanceEmailSettings = false;
- protected $validationAttributes = [
- 'team.smtp_from_address' => 'From Address',
- 'team.smtp_from_name' => 'From Name',
- 'team.smtp_recipients' => 'Recipients',
- 'team.smtp_host' => 'Host',
- 'team.smtp_port' => 'Port',
- 'team.smtp_encryption' => 'Encryption',
- 'team.smtp_username' => 'Username',
- 'team.smtp_password' => 'Password',
- 'team.smtp_timeout' => 'Timeout',
- 'team.resend_enabled' => 'Resend Enabled',
- 'team.resend_api_key' => 'Resend API Key',
- ];
+ #[Validate(['nullable', 'email'])]
+ public ?string $smtpFromAddress = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpFromName = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpRecipients = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpHost = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpEncryption = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpUsername = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpPassword = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpTimeout = null;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsTest = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsDeployments = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsStatusChanges = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsDatabaseBackups = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsScheduledTasks = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsServerDiskUsage = false;
+
+ #[Validate(['boolean'])]
+ public bool $resendEnabled;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $resendApiKey = null;
public function mount()
- {
- $this->team = auth()->user()->currentTeam();
- ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
- $this->emails = auth()->user()->email;
- }
-
- public function submitFromFields()
{
try {
- $this->resetErrorBag();
- $this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- ]);
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->team = auth()->user()->currentTeam();
+ $this->emails = auth()->user()->email;
+ $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->smtp_enabled = $this->smtpEnabled;
+ $this->team->smtp_from_address = $this->smtpFromAddress;
+ $this->team->smtp_from_name = $this->smtpFromName;
+ $this->team->smtp_host = $this->smtpHost;
+ $this->team->smtp_port = $this->smtpPort;
+ $this->team->smtp_encryption = $this->smtpEncryption;
+ $this->team->smtp_username = $this->smtpUsername;
+ $this->team->smtp_password = $this->smtpPassword;
+ $this->team->smtp_timeout = $this->smtpTimeout;
+ $this->team->smtp_recipients = $this->smtpRecipients;
+ $this->team->smtp_notifications_test = $this->smtpNotificationsTest;
+ $this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments;
+ $this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges;
+ $this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups;
+ $this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks;
+ $this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage;
+ $this->team->use_instance_email_settings = $this->useInstanceEmailSettings;
+ $this->team->resend_enabled = $this->resendEnabled;
+ $this->team->resend_api_key = $this->resendApiKey;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->smtpEnabled = $this->team->smtp_enabled;
+ $this->smtpFromAddress = $this->team->smtp_from_address;
+ $this->smtpFromName = $this->team->smtp_from_name;
+ $this->smtpHost = $this->team->smtp_host;
+ $this->smtpPort = $this->team->smtp_port;
+ $this->smtpEncryption = $this->team->smtp_encryption;
+ $this->smtpUsername = $this->team->smtp_username;
+ $this->smtpPassword = $this->team->smtp_password;
+ $this->smtpTimeout = $this->team->smtp_timeout;
+ $this->smtpRecipients = $this->team->smtp_recipients;
+ $this->smtpNotificationsTest = $this->team->smtp_notifications_test;
+ $this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments;
+ $this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes;
+ $this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups;
+ $this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks;
+ $this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage;
+ $this->useInstanceEmailSettings = $this->team->use_instance_email_settings;
+ $this->resendEnabled = $this->team->resend_enabled;
+ $this->resendApiKey = $this->team->resend_api_key;
+ }
+ }
+
public function sendTestNotification()
{
- $this->team?->notify(new Test($this->emails));
- $this->dispatch('success', 'Test Email sent.');
+ try {
+ $executed = RateLimiter::attempt(
+ 'test-email:'.$this->team->id,
+ $perMinute = 0,
+ function () {
+ $this->team?->notify(new Test($this->emails));
+ $this->dispatch('success', 'Test Email sent.');
+ },
+ $decaySeconds = 10,
+ );
+
+ if (! $executed) {
+ throw new \Exception('Too many messages sent!');
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function instantSaveInstance()
{
try {
- if (! $this->sharedEmailEnabled) {
- throw new \Exception('Not allowed to change settings. Please upgrade your subscription.');
- }
- $this->team->smtp_enabled = false;
- $this->team->resend_enabled = false;
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->smtpEnabled = false;
+ $this->resendEnabled = false;
+ $this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function instantSaveSmtpEnabled()
+ {
+ try {
+ $this->validate([
+ 'smtpHost' => 'required',
+ 'smtpPort' => 'required|numeric',
+ ], [
+ 'smtpHost.required' => 'SMTP Host is required.',
+ 'smtpPort.required' => 'SMTP Port is required.',
+ ]);
+ $this->resendEnabled = false;
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->smtpEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
public function instantSaveResend()
{
try {
- $this->team->smtp_enabled = false;
- $this->submitResend();
+ $this->validate([
+ 'resendApiKey' => 'required',
+ ], [
+ 'resendApiKey.required' => 'Resend API Key is required.',
+ ]);
+ $this->smtpEnabled = false;
+ $this->saveModel();
} catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function instantSave()
- {
- try {
- $this->team->resend_enabled = false;
- $this->submit();
- } catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
+ $this->resendEnabled = false;
return handleError($e, $this);
}
@@ -121,7 +202,7 @@ class Email extends Component
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
@@ -130,79 +211,37 @@ class Email extends Component
{
try {
$this->resetErrorBag();
- if (! $this->team->use_instance_email_settings) {
- $this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- 'team.smtp_host' => 'required',
- 'team.smtp_port' => 'required|numeric',
- 'team.smtp_encryption' => 'nullable',
- 'team.smtp_username' => 'nullable',
- 'team.smtp_password' => 'nullable',
- 'team.smtp_timeout' => 'nullable',
- ]);
- }
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->saveModel();
} catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function submitResend()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- 'team.resend_api_key' => 'required',
- ]);
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->team->resend_enabled = false;
-
return handleError($e, $this);
}
}
public function copyFromInstanceSettings()
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
+
if ($settings->smtp_enabled) {
- $team = currentTeam();
- $team->update([
- 'smtp_enabled' => $settings->smtp_enabled,
- 'smtp_from_address' => $settings->smtp_from_address,
- 'smtp_from_name' => $settings->smtp_from_name,
- 'smtp_recipients' => $settings->smtp_recipients,
- 'smtp_host' => $settings->smtp_host,
- 'smtp_port' => $settings->smtp_port,
- 'smtp_encryption' => $settings->smtp_encryption,
- 'smtp_username' => $settings->smtp_username,
- 'smtp_password' => $settings->smtp_password,
- 'smtp_timeout' => $settings->smtp_timeout,
- ]);
- refreshSession();
- $this->team = $team;
- $this->dispatch('success', 'Settings saved.');
+ $this->smtpEnabled = true;
+ $this->smtpFromAddress = $settings->smtp_from_address;
+ $this->smtpFromName = $settings->smtp_from_name;
+ $this->smtpRecipients = $settings->smtp_recipients;
+ $this->smtpHost = $settings->smtp_host;
+ $this->smtpPort = $settings->smtp_port;
+ $this->smtpEncryption = $settings->smtp_encryption;
+ $this->smtpUsername = $settings->smtp_username;
+ $this->smtpPassword = $settings->smtp_password;
+ $this->smtpTimeout = $settings->smtp_timeout;
+ $this->resendEnabled = false;
+ $this->saveModel();
return;
}
if ($settings->resend_enabled) {
- $team = currentTeam();
- $team->update([
- 'resend_enabled' => $settings->resend_enabled,
- 'resend_api_key' => $settings->resend_api_key,
- ]);
- refreshSession();
- $this->team = $team;
- $this->dispatch('success', 'Settings saved.');
+ $this->resendEnabled = true;
+ $this->resendApiKey = $settings->resend_api_key;
+ $this->smtpEnabled = false;
+ $this->saveModel();
return;
}
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index 16123f123..15ec20577 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -4,67 +4,157 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Telegram extends Component
{
public Team $team;
- protected $rules = [
- 'team.telegram_enabled' => 'nullable|boolean',
- 'team.telegram_token' => 'required|string',
- 'team.telegram_chat_id' => 'required|string',
- 'team.telegram_notifications_test' => 'nullable|boolean',
- 'team.telegram_notifications_deployments' => 'nullable|boolean',
- 'team.telegram_notifications_status_changes' => 'nullable|boolean',
- 'team.telegram_notifications_database_backups' => 'nullable|boolean',
- 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean',
- 'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string',
- ];
+ #[Validate(['boolean'])]
+ public bool $telegramEnabled = false;
- protected $validationAttributes = [
- 'team.telegram_token' => 'Token',
- 'team.telegram_chat_id' => 'Chat ID',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramToken = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramChatId = null;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsTest = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsDeployments = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsStatusChanges = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsDatabaseBackups = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsScheduledTasks = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsTestMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDeploymentsMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsStatusChangesMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsScheduledTasksThreadId = null;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsServerDiskUsage = false;
public function mount()
{
- $this->team = auth()->user()->currentTeam();
+ try {
+ $this->team = auth()->user()->currentTeam();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->telegram_enabled = $this->telegramEnabled;
+ $this->team->telegram_token = $this->telegramToken;
+ $this->team->telegram_chat_id = $this->telegramChatId;
+ $this->team->telegram_notifications_test = $this->telegramNotificationsTest;
+ $this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments;
+ $this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges;
+ $this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups;
+ $this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks;
+ $this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId;
+ $this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId;
+ $this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId;
+ $this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId;
+ $this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId;
+ $this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->telegramEnabled = $this->team->telegram_enabled;
+ $this->telegramToken = $this->team->telegram_token;
+ $this->telegramChatId = $this->team->telegram_chat_id;
+ $this->telegramNotificationsTest = $this->team->telegram_notifications_test;
+ $this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments;
+ $this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes;
+ $this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups;
+ $this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks;
+ $this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id;
+ $this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id;
+ $this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id;
+ $this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id;
+ $this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id;
+ $this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage;
+ }
+
}
public function instantSave()
{
try {
- $this->submit();
+ $this->syncData(true);
} catch (\Throwable $e) {
- ray($e->getMessage());
- $this->team->telegram_enabled = false;
- $this->validate();
+ return handleError($e, $this);
}
}
public function submit()
{
- $this->resetErrorBag();
- $this->validate();
- $this->saveModel();
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSaveTelegramEnabled()
+ {
+ try {
+ $this->validate([
+ 'telegramToken' => 'required',
+ 'telegramChatId' => 'required',
+ ], [
+ 'telegramToken.required' => 'Telegram Token is required.',
+ 'telegramChatId.required' => 'Telegram Chat ID is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->telegramEnabled = false;
+
+ return handleError($e, $this);
+ }
}
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
- $this->team?->notify(new Test());
- $this->dispatch('success', 'Test notification sent.');
+ try {
+ $this->team->notify(new Test);
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php
index 3be1b05ce..53314cd5c 100644
--- a/app/Livewire/Profile/Index.php
+++ b/app/Livewire/Profile/Index.php
@@ -2,7 +2,9 @@
namespace App\Livewire\Profile;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
+use Illuminate\Validation\Rules\Password;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -23,9 +25,9 @@ class Index extends Component
public function mount()
{
- $this->userId = auth()->user()->id;
- $this->name = auth()->user()->name;
- $this->email = auth()->user()->email;
+ $this->userId = Auth::id();
+ $this->name = Auth::user()->name;
+ $this->email = Auth::user()->email;
}
public function submit()
@@ -34,7 +36,7 @@ class Index extends Component
$this->validate([
'name' => 'required',
]);
- auth()->user()->update([
+ Auth::user()->update([
'name' => $this->name,
]);
@@ -48,9 +50,8 @@ class Index extends Component
{
try {
$this->validate([
- 'current_password' => 'required',
- 'new_password' => 'required|min:8',
- 'new_password_confirmation' => 'required|min:8|same:new_password',
+ 'current_password' => ['required'],
+ 'new_password' => ['required', Password::defaults(), 'confirmed'],
]);
if (! Hash::check($this->current_password, auth()->user()->password)) {
$this->dispatch('error', 'Current password is incorrect.');
diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php
index c3353be84..fd976548a 100644
--- a/app/Livewire/Project/AddEmpty.php
+++ b/app/Livewire/Project/AddEmpty.php
@@ -3,24 +3,17 @@
namespace App\Livewire\Project;
use App\Models\Project;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class AddEmpty extends Component
{
- public string $name = '';
+ #[Validate(['required', 'string', 'min:3'])]
+ public string $name;
+ #[Validate(['nullable', 'string'])]
public string $description = '';
- protected $rules = [
- 'name' => 'required|string|min:3',
- 'description' => 'nullable|string',
- ];
-
- protected $validationAttributes = [
- 'name' => 'Project Name',
- 'description' => 'Project Description',
- ];
-
public function submit()
{
try {
@@ -34,8 +27,6 @@ class AddEmpty extends Component
return redirect()->route('project.show', $project->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
- } finally {
- $this->name = '';
}
}
}
diff --git a/app/Livewire/Project/AddEnvironment.php b/app/Livewire/Project/AddEnvironment.php
deleted file mode 100644
index 7b2767dc6..000000000
--- a/app/Livewire/Project/AddEnvironment.php
+++ /dev/null
@@ -1,44 +0,0 @@
- 'required|string|min:3',
- ];
-
- protected $validationAttributes = [
- 'name' => 'Environment Name',
- ];
-
- public function submit()
- {
- try {
- $this->validate();
- $environment = Environment::create([
- 'name' => $this->name,
- 'project_id' => $this->project->id,
- ]);
-
- return redirect()->route('project.resource.index', [
- 'project_uuid' => $this->project->uuid,
- 'environment_name' => $environment->name,
- ]);
- } catch (\Throwable $e) {
- handleError($e, $this);
- } finally {
- $this->name = '';
- }
- }
-}
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index 3b402b3ec..05ac25429 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -3,100 +3,200 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
- public bool $is_force_https_enabled;
+ #[Validate(['boolean'])]
+ public bool $isForceHttpsEnabled = false;
- public bool $is_gzip_enabled;
+ #[Validate(['boolean'])]
+ public bool $isGitSubmodulesEnabled = false;
- public bool $is_stripprefix_enabled;
+ #[Validate(['boolean'])]
+ public bool $isGitLfsEnabled = false;
- protected $rules = [
- 'application.settings.is_git_submodules_enabled' => 'boolean|required',
- 'application.settings.is_git_lfs_enabled' => 'boolean|required',
- 'application.settings.is_preview_deployments_enabled' => 'boolean|required',
- 'application.settings.is_auto_deploy_enabled' => 'boolean|required',
- 'is_force_https_enabled' => 'boolean|required',
- 'application.settings.is_log_drain_enabled' => 'boolean|required',
- 'application.settings.is_gpu_enabled' => 'boolean|required',
- 'application.settings.is_build_server_enabled' => 'boolean|required',
- 'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
- 'application.settings.custom_internal_name' => 'string|nullable',
- 'application.settings.is_gzip_enabled' => 'boolean|required',
- 'application.settings.is_stripprefix_enabled' => 'boolean|required',
- 'application.settings.gpu_driver' => 'string|required',
- 'application.settings.gpu_count' => 'string|required',
- 'application.settings.gpu_device_ids' => 'string|required',
- 'application.settings.gpu_options' => 'string|required',
- 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
- 'application.settings.connect_to_docker_network' => 'boolean|required',
- ];
+ #[Validate(['boolean'])]
+ public bool $isPreviewDeploymentsEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isAutoDeployEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isGpuEnabled = false;
+
+ #[Validate(['string'])]
+ public string $gpuDriver = '';
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuCount = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuDeviceIds = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuOptions = null;
+
+ #[Validate(['boolean'])]
+ public bool $isBuildServerEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isConsistentContainerNameEnabled = false;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $customInternalName = null;
+
+ #[Validate(['boolean'])]
+ public bool $isGzipEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $isStripprefixEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $isRawComposeDeploymentEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isConnectToDockerNetworkEnabled = false;
public function mount()
{
- $this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
- $this->is_gzip_enabled = $this->application->isGzipEnabled();
- $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
+ try {
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->application->settings->is_force_https_enabled = $this->isForceHttpsEnabled;
+ $this->application->settings->is_git_submodules_enabled = $this->isGitSubmodulesEnabled;
+ $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled;
+ $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled;
+ $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled;
+ $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->application->settings->is_gpu_enabled = $this->isGpuEnabled;
+ $this->application->settings->gpu_driver = $this->gpuDriver;
+ $this->application->settings->gpu_count = $this->gpuCount;
+ $this->application->settings->gpu_device_ids = $this->gpuDeviceIds;
+ $this->application->settings->gpu_options = $this->gpuOptions;
+ $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled;
+ $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled;
+ $this->application->settings->custom_internal_name = $this->customInternalName;
+ $this->application->settings->is_gzip_enabled = $this->isGzipEnabled;
+ $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
+ $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
+ $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
+ $this->application->settings->save();
+ } else {
+ $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
+ $this->isGzipEnabled = $this->application->isGzipEnabled();
+ $this->isStripprefixEnabled = $this->application->isStripprefixEnabled();
+ $this->isLogDrainEnabled = $this->application->isLogDrainEnabled();
+
+ $this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled;
+ $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled;
+ $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled;
+ $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled;
+ $this->isGpuEnabled = $this->application->settings->is_gpu_enabled;
+ $this->gpuDriver = $this->application->settings->gpu_driver;
+ $this->gpuCount = $this->application->settings->gpu_count;
+ $this->gpuDeviceIds = $this->application->settings->gpu_device_ids;
+ $this->gpuOptions = $this->application->settings->gpu_options;
+ $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
+ $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled;
+ $this->customInternalName = $this->application->settings->custom_internal_name;
+ $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
+ $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
+ }
}
public function instantSave()
{
- if ($this->application->isLogDrainEnabled()) {
- if (! $this->application->destination->server->isLogDrainEnabled()) {
- $this->application->settings->is_log_drain_enabled = false;
- $this->dispatch('error', 'Log drain is not enabled on this server.');
+ try {
+ if ($this->isLogDrainEnabled) {
+ if (! $this->application->destination->server->isLogDrainEnabled()) {
+ $this->isLogDrainEnabled = false;
+ $this->syncData(true);
+ $this->dispatch('error', 'Log drain is not enabled on this server.');
- return;
+ return;
+ }
}
+ if ($this->application->isForceHttpsEnabled() !== $this->isForceHttpsEnabled ||
+ $this->application->isGzipEnabled() !== $this->isGzipEnabled ||
+ $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled
+ ) {
+ $this->dispatch('resetDefaultLabels', false);
+ }
+
+ if ($this->application->settings->is_raw_compose_deployment_enabled) {
+ $this->application->oldRawParser();
+ } else {
+ $this->application->parse();
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Settings saved.');
+ $this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) {
- $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) {
- $this->application->settings->is_gzip_enabled = $this->is_gzip_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) {
- $this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->parseRawCompose();
- } else {
- $this->application->parseCompose();
- }
- $this->application->settings->save();
- $this->dispatch('success', 'Settings saved.');
- $this->dispatch('configurationChanged');
}
public function submit()
{
- if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
- $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.');
- $this->application->settings->gpu_count = null;
- $this->application->settings->gpu_device_ids = null;
- $this->application->settings->save();
+ try {
+ if ($this->gpuCount && $this->gpuDeviceIds) {
+ $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.');
+ $this->gpuCount = null;
+ $this->gpuDeviceIds = null;
+ $this->syncData(true);
- return;
+ return;
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Settings saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->application->settings->save();
- $this->dispatch('success', 'Settings saved.');
}
public function saveCustomName()
{
- if (isset($this->application->settings->custom_internal_name)) {
- $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value();
+ if (str($this->customInternalName)->isNotEmpty()) {
+ $this->customInternalName = str($this->customInternalName)->slug()->value();
} else {
- $this->application->settings->custom_internal_name = null;
+ $this->customInternalName = null;
}
- $this->application->settings->save();
+ if (is_null($this->customInternalName)) {
+ $this->syncData(true);
+ $this->dispatch('success', 'Custom name saved.');
+
+ return;
+ }
+ $customInternalName = $this->customInternalName;
+ $server = $this->application->destination->server;
+ $allApplications = $server->applications();
+
+ $foundSameInternalName = $allApplications->filter(function ($application) {
+ return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->customInternalName;
+ });
+ if ($foundSameInternalName->isNotEmpty()) {
+ $this->dispatch('error', 'This custom container name is already in use by another application on this server.');
+ $this->customInternalName = $customInternalName;
+ $this->syncData(true);
+
+ return;
+ }
+ $this->syncData(true);
$this->dispatch('success', 'Custom name saved.');
}
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index 84a24255c..04170fa28 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -64,11 +64,25 @@ class Show extends Component
{
$this->dispatch('deploymentFinished');
$this->application_deployment_queue->refresh();
- if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') {
+ if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
$this->isKeepAliveOn = false;
}
}
+ 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 cbbe98d99..6a6fa2482 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -46,18 +46,21 @@ class DeploymentNavbar extends Component
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
- ray($e);
-
return handleError($e, $this);
}
}
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 {
- $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
- $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
- $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);
@@ -76,14 +79,13 @@ class DeploymentNavbar extends Component
}
instant_remote_process([$kill_command], $server);
} catch (\Throwable $e) {
- ray($e);
-
return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
+ next_after_cancel($server);
}
}
}
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 60cdee48e..ff29b74e9 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -2,11 +2,11 @@
namespace App\Livewire\Project\Application;
+use App\Actions\Application\GenerateConfig;
use App\Models\Application;
-use App\Models\LocalFileVolume;
use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
use Livewire\Component;
+use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component
@@ -31,6 +31,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;
@@ -41,8 +43,6 @@ class General extends Component
public ?string $initialDockerComposeLocation = null;
- public ?string $initialDockerComposePrLocation = null;
-
public ?Collection $parsedServices;
public $parsedServiceDomains = [];
@@ -73,11 +73,8 @@ class General extends Component
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
- 'application.docker_compose_pr_location' => 'nullable',
'application.docker_compose' => 'nullable',
- 'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
- 'application.docker_compose_pr_raw' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
@@ -87,9 +84,12 @@ class General extends Component
'application.pre_deployment_command_container' => 'nullable',
'application.post_deployment_command' => 'nullable',
'application.post_deployment_command_container' => 'nullable',
+ 'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
+ 'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
+ 'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
];
@@ -115,19 +115,19 @@ class General extends Component
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.docker_compose_location' => 'Docker compose location',
- 'application.docker_compose_pr_location' => 'Docker compose location',
'application.docker_compose' => 'Docker compose',
- 'application.docker_compose_pr' => 'Docker compose',
'application.docker_compose_raw' => 'Docker compose raw',
- 'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
+ 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
+ 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
+ 'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled',
'application.watch_paths' => 'Watch paths',
'application.redirect' => 'Redirect',
];
@@ -135,7 +135,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.');
@@ -150,9 +150,10 @@ 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') {
+ if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
@@ -173,9 +174,19 @@ 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)
@@ -184,46 +195,24 @@ class General extends Component
if ($isInit && $this->application->docker_compose_raw) {
return;
}
- ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
+
+ // 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::of($volume)->before(':');
- $target = Str::of($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('refresh_storages');
+ $this->dispatch('refreshStorages');
$this->dispatch('refreshEnvs');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
- $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
@@ -234,7 +223,7 @@ class General extends Component
public function generateDomain(string $serviceName)
{
- $uuid = new Cuid2(7);
+ $uuid = new Cuid2;
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
@@ -254,15 +243,11 @@ class General extends Component
}
}
- public function updatedApplicationFqdn()
+ public function updatedApplicationSettingsIsStatic($value)
{
- $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->resetDefaultLabels();
+ if ($value) {
+ $this->generateNginxConfiguration();
+ }
}
public function updatedApplicationBuildPack()
@@ -281,6 +266,7 @@ class General extends Component
if ($this->application->build_pack === 'static') {
$this->application->ports_exposes = $this->ports_exposes = 80;
$this->resetDefaultLabels(false);
+ $this->generateNginxConfiguration();
}
$this->submit();
$this->dispatch('buildPackUpdated');
@@ -298,17 +284,31 @@ class General extends Component
}
}
- public function resetDefaultLabels()
+ public function generateNginxConfiguration()
{
- $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
- $this->ports_exposes = $this->application->ports_exposes;
- $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
- $this->application->custom_labels = base64_encode($this->customLabels);
+ $this->application->custom_nginx_configuration = defaultNginxConfiguration();
$this->application->save();
- if ($this->application->build_pack === 'dockercompose') {
- $this->loadComposeFile();
+ $this->dispatch('success', 'Nginx configuration generated.');
+ }
+
+ public function resetDefaultLabels($manualReset = false)
+ {
+ try {
+ if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
+ return;
+ }
+ $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
+ $this->ports_exposes = $this->application->ports_exposes;
+ $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
+ $this->application->custom_labels = base64_encode($this->customLabels);
+ $this->application->save();
+ if ($this->application->build_pack === 'dockercompose') {
+ $this->loadComposeFile();
+ }
+ $this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->dispatch('configurationChanged');
}
public function checkFqdns($showToaster = true)
@@ -347,19 +347,29 @@ class General extends Component
public function submit($showToaster = true)
{
try {
- $this->set_redirect();
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
+
return str($domain)->trim()->lower();
});
+
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
+ $warning = sslipDomainWarning($this->application->fqdn);
+ if ($warning) {
+ $this->dispatch('warning', __('warning.sslipdomain'));
+ }
+ $this->resetDefaultLabels();
+
+ if ($this->application->isDirty('redirect')) {
+ $this->set_redirect();
+ }
$this->checkFqdns();
$this->application->save();
-
- if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
+ if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
@@ -372,6 +382,7 @@ class General extends Component
}
}
$this->validate();
+
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels();
}
@@ -398,6 +409,7 @@ class General extends Component
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
+
foreach ($this->parsedServiceDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
@@ -407,14 +419,35 @@ class General extends Component
check_domain_usage(resource: $this->application);
}
}
+ if ($this->application->isDirty('docker_compose_domains')) {
+ $this->resetDefaultLabels();
+ }
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
- $showToaster && $this->dispatch('success', 'Application settings updated!');
+ $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
+
return handleError($e, $this);
} finally {
$this->dispatch('configurationChanged');
}
}
+
+ public function downloadConfig()
+ {
+ $config = GenerateConfig::run($this->application, true);
+ $fileName = str($this->application->name)->slug()->append('_config.json');
+
+ return response()->streamDownload(function () use ($config) {
+ echo $config;
+ }, $fileName, [
+ 'Content-Type' => 'application/json',
+ 'Content-Disposition' => 'attachment; filename='.$fileName,
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index d224f4a9d..1082b48cd 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -5,8 +5,6 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
-use App\Jobs\ContainerStatusJob;
-use App\Jobs\ServerStatusJob;
use App\Models\Application;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -23,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
+ public bool $docker_cleanup = true;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -45,12 +45,8 @@ class Heading extends Component
public function check_status($showNotification = false)
{
if ($this->application->destination->server->isFunctional()) {
- GetContainersStatus::dispatch($this->application->destination->server);
- // dispatch(new ContainerStatusJob($this->application->destination->server));
- } else {
- dispatch(new ServerStatusJob($this->application->destination->server));
+ GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high');
}
-
if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.');
}
@@ -102,13 +98,13 @@ class Heading extends Component
protected function setDeploymentUuid()
{
- $this->deploymentUuid = new Cuid2(7);
+ $this->deploymentUuid = new Cuid2;
$this->parameters['deployment_uuid'] = $this->deploymentUuid;
}
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) {
@@ -141,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/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php
index cf5ab9c82..edcab44c8 100644
--- a/app/Livewire/Project/Application/Preview/Form.php
+++ b/app/Livewire/Project/Application/Preview/Form.php
@@ -3,7 +3,7 @@
namespace App\Livewire\Project\Application\Preview;
use App\Models\Application;
-use Illuminate\Support\Str;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
@@ -11,45 +11,53 @@ class Form extends Component
{
public Application $application;
- public string $preview_url_template;
-
- protected $rules = [
- 'application.preview_url_template' => 'required',
- ];
-
- protected $validationAttributes = [
- 'application.preview_url_template' => 'preview url template',
- ];
-
- public function resetToDefault()
- {
- $this->application->preview_url_template = '{{pr_id}}.{{domain}}';
- $this->preview_url_template = $this->application->preview_url_template;
- $this->application->save();
- $this->generate_real_url();
- }
-
- public function generate_real_url()
- {
- if (data_get($this->application, 'fqdn')) {
- $firstFqdn = Str::of($this->application->fqdn)->before(',');
- $url = Url::fromString($firstFqdn);
- $host = $url->getHost();
- $this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host);
- }
- }
+ #[Validate('required')]
+ public string $previewUrlTemplate;
public function mount()
{
- $this->generate_real_url();
+ try {
+ $this->previewUrlTemplate = $this->application->preview_url_template;
+ $this->generateRealUrl();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
{
- $this->validate();
- $this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template);
- $this->application->save();
- $this->dispatch('success', 'Preview url template updated.');
- $this->generate_real_url();
+ try {
+ $this->resetErrorBag();
+ $this->validate();
+ $this->application->preview_url_template = str_replace(' ', '', $this->previewUrlTemplate);
+ $this->application->save();
+ $this->dispatch('success', 'Preview url template updated.');
+ $this->generateRealUrl();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function resetToDefault()
+ {
+ try {
+ $this->application->preview_url_template = '{{pr_id}}.{{domain}}';
+ $this->previewUrlTemplate = $this->application->preview_url_template;
+ $this->application->save();
+ $this->generateRealUrl();
+ $this->dispatch('success', 'Preview url template updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function generateRealUrl()
+ {
+ if (data_get($this->application, 'fqdn')) {
+ $firstFqdn = str($this->application->fqdn)->before(',');
+ $url = Url::fromString($firstFqdn);
+ $host = $url->getHost();
+ $this->previewUrlTemplate = str($this->application->preview_url_template)->replace('{{domain}}', $host);
+ }
}
}
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index ca911339e..d42bf03d7 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -5,7 +5,10 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use Carbon\Carbon;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -79,13 +82,20 @@ 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();
$schema = $url->getScheme();
- $random = new Cuid2(7);
+ $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn);
@@ -131,6 +141,12 @@ class Previews extends Component
}
}
+ public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
+ {
+ $this->add($pull_request_id, $pull_request_html_url);
+ $this->deploy($pull_request_id, $pull_request_html_url);
+ }
+
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null)
{
try {
@@ -164,24 +180,27 @@ class Previews extends Component
protected function setDeploymentUuid()
{
- $this->deployment_uuid = new Cuid2(7);
+ $this->deployment_uuid = new Cuid2;
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
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);
- $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);
}
@@ -190,16 +209,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.');
@@ -207,4 +231,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 = Carbon::now()->getTimestamp();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
+ }
+
+ private function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(["docker kill $containerName"], $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
}
diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php
index bf4478e53..b3e838bb3 100644
--- a/app/Livewire/Project/Application/PreviewsCompose.php
+++ b/app/Livewire/Project/Application/PreviewsCompose.php
@@ -44,7 +44,7 @@ class PreviewsCompose extends Component
$template = $this->preview->application->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
- $random = new Cuid2(7);
+ $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php
index 41fe598b1..1e58a1458 100644
--- a/app/Livewire/Project/Application/Rollback.php
+++ b/app/Livewire/Project/Application/Rollback.php
@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
-use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -24,7 +23,7 @@ class Rollback extends Component
public function rollbackImage($commit)
{
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
queue_application_deployment(
application: $this->application,
@@ -50,16 +49,16 @@ class Rollback extends Component
$output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
], $this->application->destination->server, throwError: false);
- $current_tag = Str::of($output)->trim()->explode(':');
+ $current_tag = str($output)->trim()->explode(':');
$this->current = data_get($current_tag, 1);
$output = instant_remote_process([
"docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'",
], $this->application->destination->server);
- $this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) {
- return Str::of($item)->contains($image);
+ $this->images = str($output)->trim()->explode("\n")->filter(function ($item) use ($image) {
+ return str($item)->contains($image);
})->map(function ($item) {
- $item = Str::of($item)->explode('#');
+ $item = str($item)->explode('#');
if ($item[1] === $this->current) {
// $is_current = true;
}
diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php
index 426626e55..ade297d50 100644
--- a/app/Livewire/Project/Application/Source.php
+++ b/app/Livewire/Project/Application/Source.php
@@ -4,55 +4,92 @@ namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\PrivateKey;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Source extends Component
{
- public $applicationId;
-
public Application $application;
- public $private_keys;
+ #[Locked]
+ public $privateKeys;
- protected $rules = [
- 'application.git_repository' => 'required',
- 'application.git_branch' => 'required',
- 'application.git_commit_sha' => 'nullable',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $privateKeyName = null;
- protected $validationAttributes = [
- 'application.git_repository' => 'repository',
- 'application.git_branch' => 'branch',
- 'application.git_commit_sha' => 'commit sha',
- ];
+ #[Validate(['nullable', 'integer'])]
+ public ?int $privateKeyId = null;
+
+ #[Validate(['required', 'string'])]
+ public string $gitRepository;
+
+ #[Validate(['required', 'string'])]
+ public string $gitBranch;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $gitCommitSha = null;
public function mount()
{
- $this->get_private_keys();
+ try {
+ $this->syncData();
+ $this->getPrivateKeys();
+ } catch (\Throwable $e) {
+ handleError($e, $this);
+ }
}
- private function get_private_keys()
+ public function syncData(bool $toModel = false)
{
- $this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
- return $key->id == $this->application->private_key_id;
+ if ($toModel) {
+ $this->validate();
+ $this->application->update([
+ 'git_repository' => $this->gitRepository,
+ 'git_branch' => $this->gitBranch,
+ 'git_commit_sha' => $this->gitCommitSha,
+ 'private_key_id' => $this->privateKeyId,
+ ]);
+ } else {
+ $this->gitRepository = $this->application->git_repository;
+ $this->gitBranch = $this->application->git_branch;
+ $this->gitCommitSha = $this->application->git_commit_sha;
+ $this->privateKeyId = $this->application->private_key_id;
+ $this->privateKeyName = data_get($this->application, 'private_key.name');
+ }
+ }
+
+ private function getPrivateKeys()
+ {
+ $this->privateKeys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
+ return $key->id == $this->privateKeyId;
});
}
- public function setPrivateKey(int $private_key_id)
+ public function setPrivateKey(int $privateKeyId)
{
- $this->application->private_key_id = $private_key_id;
- $this->application->save();
- $this->application->refresh();
- $this->get_private_keys();
+ try {
+ $this->privateKeyId = $privateKeyId;
+ $this->syncData(true);
+ $this->getPrivateKeys();
+ $this->application->refresh();
+ $this->privateKeyName = $this->application->private_key->name;
+ $this->dispatch('success', 'Private key updated!');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
{
- $this->validate();
- if (! $this->application->git_commit_sha) {
- $this->application->git_commit_sha = 'HEAD';
+ try {
+ if (str($this->gitCommitSha)->isEmpty()) {
+ $this->gitCommitSha = 'HEAD';
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Application source updated!');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->application->save();
- $this->dispatch('success', 'Application source updated!');
}
}
diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php
index 0151b5222..197dc41ed 100644
--- a/app/Livewire/Project/Application/Swarm.php
+++ b/app/Livewire/Project/Application/Swarm.php
@@ -3,32 +3,55 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Swarm extends Component
{
public Application $application;
- public string $swarm_placement_constraints = '';
+ #[Validate('required')]
+ public int $swarmReplicas;
- protected $rules = [
- 'application.swarm_replicas' => 'required',
- 'application.swarm_placement_constraints' => 'nullable',
- 'application.settings.is_swarm_only_worker_nodes' => 'required',
- ];
+ #[Validate(['nullable'])]
+ public ?string $swarmPlacementConstraints = null;
+
+ #[Validate('required')]
+ public bool $isSwarmOnlyWorkerNodes;
public function mount()
{
- if ($this->application->swarm_placement_constraints) {
- $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints);
+ try {
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->application->swarm_replicas = $this->swarmReplicas;
+ $this->application->swarm_placement_constraints = $this->swarmPlacementConstraints ? base64_encode($this->swarmPlacementConstraints) : null;
+ $this->application->settings->is_swarm_only_worker_nodes = $this->isSwarmOnlyWorkerNodes;
+ $this->application->save();
+ $this->application->settings->save();
+ } else {
+ $this->swarmReplicas = $this->application->swarm_replicas;
+ if ($this->application->swarm_placement_constraints) {
+ $this->swarmPlacementConstraints = base64_decode($this->application->swarm_placement_constraints);
+ } else {
+ $this->swarmPlacementConstraints = null;
+ }
+ $this->isSwarmOnlyWorkerNodes = $this->application->settings->is_swarm_only_worker_nodes;
}
}
public function instantSave()
{
try {
- $this->validate();
- $this->application->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -38,14 +61,7 @@ class Swarm extends Component
public function submit()
{
try {
- $this->validate();
- if ($this->swarm_placement_constraints) {
- $this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints);
- } else {
- $this->application->swarm_placement_constraints = null;
- }
- $this->application->save();
-
+ $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php
index 5373f1b3f..4d2bc6589 100644
--- a/app/Livewire/Project/CloneMe.php
+++ b/app/Livewire/Project/CloneMe.php
@@ -47,7 +47,7 @@ class CloneMe extends Component
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
- $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2(7))->slug();
+ $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
}
public function render()
@@ -106,7 +106,7 @@ class CloneMe extends Component
$databases = $this->environment->databases();
$services = $this->environment->services;
foreach ($applications as $application) {
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$newApplication = $application->replicate()->fill([
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
@@ -133,7 +133,7 @@ class CloneMe extends Component
}
}
foreach ($databases as $database) {
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$newDatabase = $database->replicate()->fill([
'uuid' => $uuid,
'status' => 'exited',
@@ -161,7 +161,7 @@ class CloneMe extends Component
}
}
foreach ($services as $service) {
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $environment->id,
diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php
index d9a4b623d..9ff2f48d5 100644
--- a/app/Livewire/Project/Database/Backup/Index.php
+++ b/app/Livewire/Project/Database/Backup/Index.php
@@ -24,10 +24,10 @@ class Index extends Component
}
// No backups
if (
- $database->getMorphClass() === 'App\Models\StandaloneRedis' ||
- $database->getMorphClass() === 'App\Models\StandaloneKeydb' ||
- $database->getMorphClass() === 'App\Models\StandaloneDragonfly' ||
- $database->getMorphClass() === 'App\Models\StandaloneClickhouse'
+ $database->getMorphClass() === \App\Models\StandaloneRedis::class ||
+ $database->getMorphClass() === \App\Models\StandaloneKeydb::class ||
+ $database->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
+ $database->getMorphClass() === \App\Models\StandaloneClickhouse::class
) {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 59f2f9a39..b3a54f0ab 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -2,55 +2,113 @@
namespace App\Livewire\Project\Database;
+use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
+use Exception;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
class BackupEdit extends Component
{
- public ?ScheduledDatabaseBackup $backup;
+ public ScheduledDatabaseBackup $backup;
+ #[Locked]
public $s3s;
+ #[Locked]
+ public $parameters;
+
+ #[Validate(['required', 'boolean'])]
+ public bool $delete_associated_backups_locally = false;
+
+ #[Validate(['required', 'boolean'])]
+ public bool $delete_associated_backups_s3 = false;
+
+ #[Validate(['required', 'boolean'])]
+ public bool $delete_associated_backups_sftp = false;
+
+ #[Validate(['nullable', 'string'])]
public ?string $status = null;
- public array $parameters;
+ #[Validate(['required', 'boolean'])]
+ public bool $backupEnabled = false;
- protected $rules = [
- 'backup.enabled' => 'required|boolean',
- 'backup.frequency' => 'required|string',
- 'backup.number_of_backups_locally' => 'required|integer|min:1',
- 'backup.save_s3' => 'required|boolean',
- 'backup.s3_storage_id' => 'nullable|integer',
- 'backup.databases_to_backup' => 'nullable',
- ];
+ #[Validate(['required', 'string'])]
+ public string $frequency = '';
- protected $validationAttributes = [
- 'backup.enabled' => 'Enabled',
- 'backup.frequency' => 'Frequency',
- 'backup.number_of_backups_locally' => 'Number of Backups Locally',
- 'backup.save_s3' => 'Save to S3',
- 'backup.s3_storage_id' => 'S3 Storage',
- 'backup.databases_to_backup' => 'Databases to Backup',
- ];
+ #[Validate(['required', 'integer', 'min:1'])]
+ public int $numberOfBackupsLocally = 1;
- protected $messages = [
- 'backup.s3_storage_id' => 'Select a S3 Storage',
- ];
+ #[Validate(['required', 'boolean'])]
+ public bool $saveS3 = false;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $s3StorageId = 1;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $databasesToBackup = null;
+
+ #[Validate(['required', 'boolean'])]
+ public bool $dumpAll = false;
public function mount()
{
- $this->parameters = get_route_parameters();
- if (is_null(data_get($this->backup, 's3_storage_id'))) {
- data_set($this->backup, 's3_storage_id', 'default');
+ try {
+ $this->parameters = get_route_parameters();
+ $this->syncData();
+ } catch (Exception $e) {
+ return handleError($e, $this);
}
}
- public function delete()
+ public function syncData(bool $toModel = false)
{
+ if ($toModel) {
+ $this->customValidate();
+ $this->backup->enabled = $this->backupEnabled;
+ $this->backup->frequency = $this->frequency;
+ $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally;
+ $this->backup->save_s3 = $this->saveS3;
+ $this->backup->s3_storage_id = $this->s3StorageId;
+ $this->backup->databases_to_backup = $this->databasesToBackup;
+ $this->backup->dump_all = $this->dumpAll;
+ $this->backup->save();
+ } else {
+ $this->backupEnabled = $this->backup->enabled;
+ $this->frequency = $this->backup->frequency;
+ $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally;
+ $this->saveS3 = $this->backup->save_s3;
+ $this->s3StorageId = $this->backup->s3_storage_id;
+ $this->databasesToBackup = $this->backup->databases_to_backup;
+ $this->dumpAll = $this->backup->dump_all;
+ }
+ }
+
+ public function delete($password)
+ {
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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') {
+
+ if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
$url = $url->withoutQueryParameter('selectedBackupId');
@@ -69,16 +127,14 @@ class BackupEdit extends Component
public function instantSave()
{
try {
- $this->custom_validate();
- $this->backup->save();
- $this->backup->refresh();
+ $this->syncData(true);
$this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
}
- private function custom_validate()
+ private function customValidate()
{
if (! is_numeric($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = null;
@@ -93,15 +149,72 @@ class BackupEdit extends Component
public function submit()
{
try {
- $this->custom_validate();
- if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
- $this->backup->databases_to_backup = null;
- }
- $this->backup->save();
- $this->backup->refresh();
- $this->dispatch('success', 'Backup updated successfully');
+ $this->syncData(true);
+ $this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
}
+
+ private function deleteAssociatedBackupsLocally()
+ {
+ $executions = $this->backup->executions;
+ $backupFolder = null;
+
+ foreach ($executions as $execution) {
+ if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
+ $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 (str($backupFolder)->isNotEmpty()) {
+ $this->deleteEmptyBackupFolder($backupFolder, $server);
+ }
+ }
+
+ private function deleteAssociatedBackupsS3()
+ {
+ //Add function to delete backups from S3
+ }
+
+ private 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..f91b8bfaf 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -2,24 +2,32 @@
namespace App\Livewire\Project\Database;
+use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
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',
- 'deleteBackup',
];
}
@@ -32,19 +40,37 @@ class BackupExecutions extends Component
}
}
- public function deleteBackup($exeuctionId)
+ public function deleteBackup($executionId, $password)
{
- $execution = $this->backup->executions()->where('id', $exeuctionId)->first();
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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') {
+
+ if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
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,65 @@ 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';
+ }
+
+ return $server->settings->server_timezone;
+ }
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->getServerTimezone();
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } catch (\Exception) {
+ $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 875a36141..2d39c5151 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -7,6 +7,8 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
@@ -15,54 +17,106 @@ class General extends Component
public StandaloneClickhouse $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $listeners = ['refresh'];
+ #[Validate(['required', 'string'])]
+ public string $clickhouseAdminUser;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.clickhouse_admin_user' => 'required',
- 'database.clickhouse_admin_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- ];
+ #[Validate(['required', 'string'])]
+ public string $clickhouseAdminPassword;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.clickhouse_admin_user' => 'Postgres User',
- 'database.clickhouse_admin_password' => 'Postgres Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- ];
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->clickhouse_admin_user = $this->clickhouseAdminUser;
+ $this->database->clickhouse_admin_password = $this->clickhouseAdminPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->clickhouseAdminUser = $this->database->clickhouse_admin_user;
+ $this->clickhouseAdminPassword = $this->database->clickhouse_admin_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
}
- $this->server = data_get($this->database, 'destination.server');
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -73,48 +127,47 @@ class General extends Component
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
+ $this->isPublic = false;
return;
}
- if ($this->database->is_public) {
+ if ($this->isPublic) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
+ $this->isPublic = false;
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
- $this->database->save();
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
return handleError($e, $this);
}
}
- public function refresh(): void
+ public function databaseProxyStopped()
{
- $this->database->refresh();
+ $this->syncData();
}
public function submit()
{
try {
- if (str($this->database->public_port)->isEmpty()) {
- $this->database->public_port = null;
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
}
- $this->validate();
- $this->database->save();
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php
index 5ed74a6c3..0903efdfd 100644
--- a/app/Livewire/Project/Database/CreateScheduledBackup.php
+++ b/app/Livewire/Project/Database/CreateScheduledBackup.php
@@ -4,44 +4,45 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class CreateScheduledBackup extends Component
{
- public $database;
-
+ #[Validate(['required', 'string'])]
public $frequency;
+ #[Validate(['required', 'boolean'])]
+ public bool $saveToS3 = false;
+
+ #[Locked]
+ public $database;
+
public bool $enabled = true;
- public bool $save_s3 = false;
+ #[Validate(['required', 'integer'])]
+ public int $s3StorageId;
- public $s3_storage_id;
-
- public Collection $s3s;
-
- protected $rules = [
- 'frequency' => 'required|string',
- 'save_s3' => 'required|boolean',
- ];
-
- protected $validationAttributes = [
- 'frequency' => 'Backup Frequency',
- 'save_s3' => 'Save to S3',
- ];
+ public Collection $definedS3s;
public function mount()
{
- $this->s3s = currentTeam()->s3s;
- if ($this->s3s->count() > 0) {
- $this->s3_storage_id = $this->s3s->first()->id;
+ try {
+ $this->definedS3s = currentTeam()->s3s;
+ if ($this->definedS3s->count() > 0) {
+ $this->s3StorageId = $this->definedS3s->first()->id;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
- public function submit(): void
+ public function submit()
{
try {
$this->validate();
+
$isValid = validate_cron_expression($this->frequency);
if (! $isValid) {
$this->dispatch('error', 'Invalid Cron / Human expression.');
@@ -51,8 +52,8 @@ class CreateScheduledBackup extends Component
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
- 'save_s3' => $this->save_s3,
- 's3_storage_id' => $this->s3_storage_id,
+ 'save_s3' => $this->saveToS3,
+ 's3_storage_id' => $this->s3StorageId,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
@@ -66,16 +67,16 @@ class CreateScheduledBackup extends Component
}
$databaseBackup = ScheduledDatabaseBackup::create($payload);
- if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$this->dispatch('refreshScheduledBackups', $databaseBackup->id);
} else {
$this->dispatch('refreshScheduledBackups');
}
} catch (\Throwable $e) {
- handleError($e, $this);
+ return handleError($e, $this);
} finally {
$this->frequency = '';
- $this->save_s3 = true;
+ $this->saveToS3 = true;
}
}
}
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index d6c4eb2ce..ea6cd46b0 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -7,60 +7,111 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneDragonfly;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
{
- protected $listeners = ['refresh'];
-
public Server $server;
public StandaloneDragonfly $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.dragonfly_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- ];
+ #[Validate(['required', 'string'])]
+ public string $dragonflyPassword;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.dragonfly_password' => 'Redis Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- ];
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->dragonfly_password = $this->dragonflyPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->dragonflyPassword = $this->database->dragonfly_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
}
- $this->server = data_get($this->database, 'destination.server');
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -68,11 +119,50 @@ class General extends Component
}
}
+ public function instantSave()
+ {
+ try {
+ if ($this->isPublic && ! $this->publicPort) {
+ $this->dispatch('error', 'Public port is required.');
+ $this->isPublic = false;
+
+ return;
+ }
+ if ($this->isPublic) {
+ if (! str($this->database->status)->startsWith('running')) {
+ $this->dispatch('error', 'Database must be started to be publicly accessible.');
+ $this->isPublic = false;
+
+ return;
+ }
+ StartDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is now publicly accessible.');
+ } else {
+ StopDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is no longer publicly accessible.');
+ }
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function databaseProxyStopped()
+ {
+ $this->syncData();
+ }
+
public function submit()
{
try {
- $this->validate();
- $this->database->save();
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
+ }
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -84,46 +174,4 @@ class General extends Component
}
}
}
-
- public function instantSave()
- {
- try {
- if ($this->database->is_public && ! $this->database->public_port) {
- $this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
-
- return;
- }
- if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
- $this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
-
- return;
- }
- StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
- $this->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
- $this->dispatch('success', 'Database is no longer publicly accessible.');
- }
- $this->database->save();
- } catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
-
- return handleError($e, $this);
- }
- }
-
- public function refresh(): void
- {
- $this->database->refresh();
- }
-
- public function render()
- {
- return view('livewire.project.database.dragonfly.general');
- }
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 61dafa76f..fc0febd02 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -2,17 +2,11 @@
namespace App\Livewire\Project\Database;
-use App\Actions\Database\StartClickhouse;
-use App\Actions\Database\StartDragonfly;
-use App\Actions\Database\StartKeydb;
-use App\Actions\Database\StartMariadb;
-use App\Actions\Database\StartMongodb;
-use App\Actions\Database\StartMysql;
-use App\Actions\Database\StartPostgresql;
-use App\Actions\Database\StartRedis;
+use App\Actions\Database\RestartDatabase;
+use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
-use App\Jobs\ContainerStatusJob;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Heading extends Component
@@ -21,9 +15,11 @@ class Heading extends Component
public array $parameters;
+ public $docker_cleanup = true;
+
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished',
@@ -48,7 +44,6 @@ class Heading extends Component
public function check_status($showNotification = false)
{
GetContainersStatus::run($this->database->destination->server);
- // dispatch_sync(new ContainerStatusJob($this->database->destination->server));
$this->database->refresh();
if ($showNotification) {
$this->dispatch('success', 'Database status updated.');
@@ -62,38 +57,30 @@ 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();
}
+ public function restart()
+ {
+ $activity = RestartDatabase::run($this->database);
+ $this->dispatch('activityMonitor', $activity->id);
+ }
+
public function start()
{
- if ($this->database->type() === 'standalone-postgresql') {
- $activity = StartPostgresql::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-redis') {
- $activity = StartRedis::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-mongodb') {
- $activity = StartMongodb::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-mysql') {
- $activity = StartMysql::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-mariadb') {
- $activity = StartMariadb::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-keydb') {
- $activity = StartKeydb::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-dragonfly') {
- $activity = StartDragonfly::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- } elseif ($this->database->type() === 'standalone-clickhouse') {
- $activity = StartClickhouse::run($this->database);
- $this->dispatch('activityMonitor', $activity->id);
- }
+ $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/Import.php b/app/Livewire/Project/Database/Import.php
index dfaa4461b..062f454b1 100644
--- a/app/Livewire/Project/Database/Import.php
+++ b/app/Livewire/Project/Database/Import.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Database;
use App\Models\Server;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
@@ -46,7 +47,7 @@ class Import extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
@@ -77,10 +78,10 @@ class Import extends Component
}
if (
- $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
- $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' ||
- $this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' ||
- $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse'
+ $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
) {
$this->unsupported = true;
}
@@ -88,8 +89,7 @@ class Import extends Component
public function runImport()
{
-
- if ($this->filename == '') {
+ if ($this->filename === '') {
$this->dispatch('error', 'Please select a file to import.');
return;
@@ -108,19 +108,19 @@ class Import extends Component
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
switch ($this->resource->getMorphClass()) {
- case 'App\Models\StandaloneMariadb':
+ case \App\Models\StandaloneMariadb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
- case 'App\Models\StandaloneMysql':
+ case \App\Models\StandaloneMysql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
- case 'App\Models\StandalonePostgresql':
+ case \App\Models\StandalonePostgresql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
- case 'App\Models\StandaloneMongodb':
+ case \App\Models\StandaloneMongodb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php
index 336762981..e3baa1c8e 100644
--- a/app/Livewire/Project/Database/InitScript.php
+++ b/app/Livewire/Project/Database/InitScript.php
@@ -3,39 +3,39 @@
namespace App\Livewire\Project\Database;
use Exception;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class InitScript extends Component
{
+ #[Locked]
public array $script;
+ #[Locked]
public int $index;
- public ?string $filename;
+ #[Validate(['nullable', 'string'])]
+ public ?string $filename = null;
- public ?string $content;
-
- protected $rules = [
- 'filename' => 'required|string',
- 'content' => 'required|string',
- ];
-
- protected $validationAttributes = [
- 'filename' => 'Filename',
- 'content' => 'Content',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $content = null;
public function mount()
{
- $this->index = data_get($this->script, 'index');
- $this->filename = data_get($this->script, 'filename');
- $this->content = data_get($this->script, 'content');
+ try {
+ $this->index = data_get($this->script, 'index');
+ $this->filename = data_get($this->script, 'filename');
+ $this->content = data_get($this->script, 'content');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
{
- $this->validate();
try {
+ $this->validate();
$this->script['index'] = $this->index;
$this->script['content'] = $this->content;
$this->script['filename'] = $this->filename;
@@ -47,6 +47,10 @@ class InitScript extends Component
public function delete()
{
- $this->dispatch('delete_init_script', $this->script);
+ try {
+ $this->dispatch('delete_init_script', $this->script);
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 381711946..e768495eb 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -7,63 +7,116 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneKeydb;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
{
- protected $listeners = ['refresh'];
-
public Server $server;
public StandaloneKeydb $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.keydb_conf' => 'nullable',
- 'database.keydb_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $keydbConf = null;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.keydb_conf' => 'Redis Configuration',
- 'database.keydb_password' => 'Redis Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- ];
+ #[Validate(['required', 'string'])]
+ public string $keydbPassword;
+
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->server = data_get($this->database, 'destination.server');
+ }
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->keydb_conf = $this->keydbConf;
+ $this->database->keydb_password = $this->keydbPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->keydbConf = $this->database->keydb_conf;
+ $this->keydbPassword = $this->database->keydb_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ }
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -71,14 +124,50 @@ class General extends Component
}
}
+ public function instantSave()
+ {
+ try {
+ if ($this->isPublic && ! $this->publicPort) {
+ $this->dispatch('error', 'Public port is required.');
+ $this->isPublic = false;
+
+ return;
+ }
+ if ($this->isPublic) {
+ if (! str($this->database->status)->startsWith('running')) {
+ $this->dispatch('error', 'Database must be started to be publicly accessible.');
+ $this->isPublic = false;
+
+ return;
+ }
+ StartDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is now publicly accessible.');
+ } else {
+ StopDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is no longer publicly accessible.');
+ }
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function databaseProxyStopped()
+ {
+ $this->syncData();
+ }
+
public function submit()
{
try {
- $this->validate();
- if ($this->database->keydb_conf === '') {
- $this->database->keydb_conf = null;
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
}
- $this->database->save();
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -90,46 +179,4 @@ class General extends Component
}
}
}
-
- public function instantSave()
- {
- try {
- if ($this->database->is_public && ! $this->database->public_port) {
- $this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
-
- return;
- }
- if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
- $this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
-
- return;
- }
- StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
- $this->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
- $this->dispatch('success', 'Database is no longer publicly accessible.');
- }
- $this->database->save();
- } catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
-
- return handleError($e, $this);
- }
- }
-
- public function refresh(): void
- {
- $this->database->refresh();
- }
-
- public function render()
- {
- return view('livewire.project.database.keydb.general');
- }
}
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 8b4b35d11..c9d473223 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,16 +49,14 @@ 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()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
- }
+ $this->db_url = $this->database->internal_db_url;
+ $this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
-
}
public function instantSaveAdvanced()
@@ -114,13 +113,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
+ $this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index ee639ae41..e19895dae 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,16 +47,14 @@ 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()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
- }
+ $this->db_url = $this->database->internal_db_url;
+ $this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
-
}
public function instantSaveAdvanced()
@@ -115,13 +114,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
+ $this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index fc0767109..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,14 +49,13 @@ 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()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
- }
+ $this->db_url = $this->database->internal_db_url;
+ $this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@@ -113,13 +113,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
+ $this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 38cac2e5c..c12fa49f3 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -25,7 +25,14 @@ class General extends Component
public ?string $db_url_public = null;
- protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
+ public function getListeners()
+ {
+ return [
+ 'refresh',
+ 'save_init_script',
+ 'delete_init_script',
+ ];
+ }
protected $rules = [
'database.name' => 'required',
@@ -42,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 = [
@@ -58,14 +66,13 @@ 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()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
- }
+ $this->db_url = $this->database->internal_db_url;
+ $this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@@ -103,13 +110,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
+ $this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index b5c1dd881..25a96b292 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -11,12 +11,21 @@ use Livewire\Component;
class General extends Component
{
- protected $listeners = ['refresh'];
+ protected $listeners = [
+ 'envsUpdated' => 'refresh',
+ 'refresh',
+ ];
public Server $server;
public StandaloneRedis $database;
+ public string $redis_username;
+
+ public string $redis_password;
+
+ public string $redis_version;
+
public ?string $db_url = null;
public ?string $db_url_public = null;
@@ -25,33 +34,33 @@ class General extends Component
'database.name' => 'required',
'database.description' => 'nullable',
'database.redis_conf' => 'nullable',
- 'database.redis_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
+ 'redis_username' => 'required',
+ 'redis_password' => 'required',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.redis_conf' => 'Redis Configuration',
- 'database.redis_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Options',
+ 'redis_username' => 'Redis Username',
+ 'redis_password' => 'Redis Password',
];
public function mount()
{
- $this->db_url = $this->database->get_db_url(true);
- if ($this->database->is_public) {
- $this->db_url_public = $this->database->get_db_url();
- }
$this->server = data_get($this->database, 'destination.server');
-
+ $this->refreshView();
}
public function instantSaveAdvanced()
@@ -75,13 +84,24 @@ class General extends Component
{
try {
$this->validate();
- if ($this->database->redis_conf === '') {
- $this->database->redis_conf = null;
+
+ if (version_compare($this->redis_version, '6.0', '>=')) {
+ $this->database->runtime_environment_variables()->updateOrCreate(
+ ['key' => 'REDIS_USERNAME'],
+ ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id]
+ );
}
+ $this->database->runtime_environment_variables()->updateOrCreate(
+ ['key' => 'REDIS_PASSWORD'],
+ ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id]
+ );
+
$this->database->save();
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
+ } finally {
+ $this->dispatch('refreshEnvs');
}
}
@@ -102,13 +122,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
- $this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
- $this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
+ $this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;
@@ -120,10 +139,25 @@ class General extends Component
public function refresh(): void
{
$this->database->refresh();
+ $this->refreshView();
+ }
+
+ private function refreshView()
+ {
+ $this->db_url = $this->database->internal_db_url;
+ $this->db_url_public = $this->database->external_db_url;
+ $this->redis_version = $this->database->getRedisVersion();
+ $this->redis_username = $this->database->redis_username;
+ $this->redis_password = $this->database->redis_password;
}
public function render()
{
return view('livewire.project.database.redis.general');
}
+
+ public function isSharedVariable($name)
+ {
+ return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists();
+ }
}
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index beb5a9c39..412240bd4 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -26,10 +26,10 @@ 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') {
+ if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$this->type = 'service-database';
} else {
$this->type = 'database';
@@ -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..1ee5de269 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -7,15 +7,22 @@ use Livewire\Component;
class DeleteEnvironment extends Component
{
- public array $parameters;
-
public int $environment_id;
public bool $disabled = false;
+ public string $environmentName = '';
+
+ public array $parameters;
+
public function mount()
{
- $this->parameters = get_route_parameters();
+ try {
+ $this->environmentName = Environment::findOrFail($this->environment_id)->name;
+ $this->parameters = get_route_parameters();
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
}
public function delete()
@@ -27,9 +34,9 @@ class DeleteEnvironment extends Component
if ($environment->isEmpty()) {
$environment->delete();
- return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
+ return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
}
- return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
+ return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first.");
}
}
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 499b86e3e..f320a19b0 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()
@@ -24,11 +27,12 @@ class DeleteProject extends Component
'project_id' => 'required|int',
]);
$project = Project::findOrFail($this->project_id);
- if ($project->applications->count() > 0) {
- return $this->dispatch('error', 'Project has resources defined, please delete them first.');
- }
- $project->delete();
+ if ($project->isEmpty()) {
+ $project->delete();
- return redirect()->route('project.index');
+ return redirect()->route('project.index');
+ }
+
+ return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first.");
}
}
diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php
index bebec4752..463febb10 100644
--- a/app/Livewire/Project/Edit.php
+++ b/app/Livewire/Project/Edit.php
@@ -3,34 +3,47 @@
namespace App\Livewire\Project;
use App\Models\Project;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Edit extends Component
{
public Project $project;
- protected $rules = [
- 'project.name' => 'required|min:3|max:255',
- 'project.description' => 'nullable|string|max:255',
- ];
+ #[Validate(['required', 'string', 'min:3', 'max:255'])]
+ public string $name;
- public function mount()
+ #[Validate(['nullable', 'string', 'max:255'])]
+ public ?string $description = null;
+
+ public function mount(string $project_uuid)
{
- $projectUuid = request()->route('project_uuid');
- $teamId = currentTeam()->id;
- $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
- if (! $project) {
- return redirect()->route('dashboard');
+ try {
+ $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->project->update([
+ 'name' => $this->name,
+ 'description' => $this->description,
+ ]);
+ } else {
+ $this->name = $this->project->name;
+ $this->description = $this->project->description;
}
- $this->project = $project;
}
public function submit()
{
try {
- $this->validate();
- $this->project->save();
- $this->dispatch('saved');
+ $this->syncData(true);
$this->dispatch('success', 'Project updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php
index 16fc7bc36..f48220b3d 100644
--- a/app/Livewire/Project/EnvironmentEdit.php
+++ b/app/Livewire/Project/EnvironmentEdit.php
@@ -4,6 +4,8 @@ namespace App\Livewire\Project;
use App\Models\Application;
use App\Models\Project;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class EnvironmentEdit extends Component
@@ -12,29 +14,45 @@ class EnvironmentEdit extends Component
public Application $application;
+ #[Locked]
public $environment;
- public array $parameters;
+ #[Validate(['required', 'string', 'min:3', 'max:255'])]
+ public string $name;
- protected $rules = [
- 'environment.name' => 'required|min:3|max:255',
- 'environment.description' => 'nullable|min:3|max:255',
- ];
+ #[Validate(['nullable', 'string', 'max:255'])]
+ public ?string $description = null;
- public function mount()
+ public function mount(string $project_uuid, string $environment_name)
{
- $this->parameters = get_route_parameters();
- $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first();
- $this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first();
+ try {
+ $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
+ $this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->environment->update([
+ 'name' => $this->name,
+ 'description' => $this->description,
+ ]);
+ } else {
+ $this->name = $this->environment->name;
+ $this->description = $this->environment->description;
+ }
}
public function submit()
{
- $this->validate();
try {
- $this->environment->save();
-
- return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]);
+ $this->syncData(true);
+ $this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php
index 0e4f15a5c..f8eb838be 100644
--- a/app/Livewire/Project/Index.php
+++ b/app/Livewire/Project/Index.php
@@ -18,7 +18,11 @@ class Index extends Component
public function mount()
{
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
- $this->projects = Project::ownedByCurrentTeam()->get();
+ $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
+ $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
+
+ return $project;
+ });
$this->servers = Server::ownedByCurrentTeam()->count();
}
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/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 65a98b37f..417fb2ea0 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -6,7 +6,6 @@ use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
-use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -29,9 +28,9 @@ class DockerImage extends Component
$this->validate([
'dockerImage' => 'required',
]);
- $image = Str::of($this->dockerImage)->before(':');
- if (Str::of($this->dockerImage)->contains(':')) {
- $tag = Str::of($this->dockerImage)->after(':');
+ $image = str($this->dockerImage)->before(':');
+ if (str($this->dockerImage)->contains(':')) {
+ $tag = str($this->dockerImage)->after(':');
} else {
$tag = 'latest';
}
@@ -47,9 +46,8 @@ class DockerImage extends Component
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
- ray($image, $tag);
$application = Application::create([
- 'name' => 'docker-image-'.new Cuid2(7),
+ 'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main',
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 76b337c01..2f4f5a25c 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -53,6 +53,12 @@ class GithubPrivateRepository extends Component
public ?string $publish_directory = null;
+ // In case of docker compose
+ public ?string $base_directory = null;
+
+ public ?string $docker_compose_location = '/docker-compose.yaml';
+ // End of docker compose
+
protected int $page = 1;
public $build_pack = 'nixpacks';
@@ -68,6 +74,16 @@ class GithubPrivateRepository extends Component
$this->github_apps = GithubApp::private();
}
+ public function updatedBaseDirectory()
+ {
+ if ($this->base_directory) {
+ $this->base_directory = rtrim($this->base_directory, '/');
+ if (! str($this->base_directory)->startsWith('/')) {
+ $this->base_directory = '/'.$this->base_directory;
+ }
+ }
+ }
+
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
@@ -137,7 +153,6 @@ class GithubPrivateRepository extends Component
protected function loadBranchByPage()
{
- ray('Loading page '.$this->page);
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
@@ -184,6 +199,10 @@ class GithubPrivateRepository extends Component
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application->health_check_enabled = false;
}
+ if ($this->build_pack === 'dockercompose') {
+ $application['docker_compose_location'] = $this->docker_compose_location;
+ $application['base_directory'] = $this->base_directory;
+ }
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index 690149cc4..b46c4a794 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -33,6 +33,12 @@ class GithubPrivateRepositoryDeployKey extends Component
public ?string $publish_directory = null;
+ // In case of docker compose
+ public ?string $base_directory = null;
+
+ public ?string $docker_compose_location = '/docker-compose.yaml';
+ // End of docker compose
+
public string $repository_url;
public string $branch;
@@ -163,6 +169,10 @@ class GithubPrivateRepositoryDeployKey extends Component
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application_init['health_check_enabled'] = false;
}
+ if ($this->build_pack === 'dockercompose') {
+ $application_init['docker_compose_location'] = $this->docker_compose_location;
+ $application_init['base_directory'] = $this->base_directory;
+ }
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();
@@ -188,12 +198,12 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
- if ($this->git_host == 'github.com') {
+ if ($this->git_host === 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
return;
}
- if (Str::of($this->repository_url)->startsWith('http')) {
+ if (str($this->repository_url)->startsWith('http')) {
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 739061f1f..bd35dccef 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -25,14 +25,22 @@ class PublicGitRepository extends Component
public $query;
- public bool $branch_found = false;
+ public bool $branchFound = false;
- public string $selected_branch = 'main';
+ public string $selectedBranch = 'main';
- public bool $is_static = false;
+ public bool $isStatic = false;
+
+ public bool $checkCoolifyConfig = true;
public ?string $publish_directory = null;
+ // In case of docker compose
+ public string $base_directory = '/';
+
+ public ?string $docker_compose_location = '/docker-compose.yaml';
+ // End of docker compose
+
public string $git_branch = 'main';
public int $rate_limit_remaining = 0;
@@ -56,17 +64,21 @@ class PublicGitRepository extends Component
protected $rules = [
'repository_url' => 'required|url',
'port' => 'required|numeric',
- 'is_static' => 'required|boolean',
+ 'isStatic' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
+ 'base_directory' => 'nullable|string',
+ 'docker_compose_location' => 'nullable|string',
];
protected $validationAttributes = [
'repository_url' => 'repository',
'port' => 'port',
- 'is_static' => 'static',
+ 'isStatic' => 'static',
'publish_directory' => 'publish directory',
'build_pack' => 'build pack',
+ 'base_directory' => 'base directory',
+ 'docker_compose_location' => 'docker compose location',
];
public function mount()
@@ -79,6 +91,26 @@ class PublicGitRepository extends Component
$this->query = request()->query();
}
+ public function updatedBaseDirectory()
+ {
+ if ($this->base_directory) {
+ $this->base_directory = rtrim($this->base_directory, '/');
+ if (! str($this->base_directory)->startsWith('/')) {
+ $this->base_directory = '/'.$this->base_directory;
+ }
+ }
+ }
+
+ 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') {
@@ -86,17 +118,17 @@ class PublicGitRepository extends Component
$this->port = 3000;
} elseif ($this->build_pack === 'static') {
$this->show_is_static = false;
- $this->is_static = false;
+ $this->isStatic = false;
$this->port = 80;
} else {
$this->show_is_static = false;
- $this->is_static = false;
+ $this->isStatic = false;
}
}
public function instantSave()
{
- if ($this->is_static) {
+ if ($this->isStatic) {
$this->port = 80;
$this->publish_directory = '/dist';
} else {
@@ -106,12 +138,7 @@ class PublicGitRepository extends Component
$this->dispatch('success', 'Application settings updated!');
}
- public function load_any_git()
- {
- $this->branch_found = true;
- }
-
- public function load_branch()
+ public function loadBranch()
{
try {
if (str($this->repository_url)->startsWith('git@')) {
@@ -128,23 +155,28 @@ class PublicGitRepository extends Component
) {
$this->repository_url = $this->repository_url.'.git';
}
- if (str($this->repository_url)->contains('github.com')) {
- $this->repository_url = str($this->repository_url)->before('.git')->value();
+ if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) {
+ $this->repository_url = str($this->repository_url)->beforeLast('.git')->value();
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
try {
- $this->branch_found = false;
- $this->get_git_source();
- $this->get_branch();
- $this->selected_branch = $this->git_branch;
+ $this->branchFound = false;
+ $this->getGitSource();
+ $this->getBranch();
+ $this->selectedBranch = $this->git_branch;
} catch (\Throwable $e) {
- ray($e->getMessage());
- if (! $this->branch_found && $this->git_branch == 'main') {
+ if ($this->rate_limit_remaining == 0) {
+ $this->selectedBranch = $this->git_branch;
+ $this->branchFound = true;
+
+ return;
+ }
+ if (! $this->branchFound && $this->git_branch === 'main') {
try {
$this->git_branch = 'master';
- $this->get_branch();
+ $this->getBranch();
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -154,14 +186,17 @@ class PublicGitRepository extends Component
}
}
- private function get_git_source()
+ private function getGitSource()
{
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
- $this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
-
- if ($this->git_host == 'github.com') {
+ if ($this->repository_url_parsed->getSegment(3) === 'tree') {
+ $this->git_branch = str($this->repository_url_parsed->getPath())->after('tree/')->value();
+ } else {
+ $this->git_branch = 'main';
+ }
+ if ($this->git_host === 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
return;
@@ -170,17 +205,17 @@ class PublicGitRepository extends Component
$this->git_source = 'other';
}
- private function get_branch()
+ private function getBranch()
{
if ($this->git_source === 'other') {
- $this->branch_found = true;
+ $this->branchFound = true;
return;
}
- if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') {
+ if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) {
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s');
- $this->branch_found = true;
+ $this->branchFound = true;
}
}
@@ -242,6 +277,7 @@ class PublicGitRepository extends Component
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
+ 'base_directory' => $this->base_directory,
];
} else {
$application_init = [
@@ -256,20 +292,30 @@ class PublicGitRepository extends Component
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
+ 'base_directory' => $this->base_directory,
];
}
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application_init['health_check_enabled'] = false;
}
+ if ($this->build_pack === 'dockercompose') {
+ $application_init['docker_compose_location'] = $this->docker_compose_location;
+ $application_init['base_directory'] = $this->base_directory;
+ }
$application = Application::create($application_init);
- $application->settings->is_static = $this->is_static;
+ $application->settings->is_static = $this->isStatic;
$application->settings->save();
-
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->save();
+ if ($this->checkCoolifyConfig) {
+ // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
+ // if ($config) {
+ // $application->setConfig($config);
+ // }
+ }
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index b8d186dab..2dc9abbf1 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -45,13 +45,12 @@ class Select extends Component
public ?string $selectedEnvironment = null;
- public ?string $existingPostgresqlUrl = null;
+ public string $postgresql_type = 'postgres:16-alpine';
- public ?string $search = null;
+ public ?string $existingPostgresqlUrl = null;
protected $queryString = [
'server_id',
- 'search',
];
public function mount()
@@ -88,40 +87,119 @@ class Select extends Component
// }
// }
- public function updatedSearch()
+ public function loadServices()
{
- $this->loadServices();
- }
+ $services = get_service_templates(true);
+ $services = collect($services)->map(function ($service, $key) {
+ return [
+ 'name' => str($key)->headline(),
+ 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
+ ] + (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 management system. ',
+ 'logo' => '
+
+
+
+ ',
- public function loadServices(bool $force = false)
- {
- try {
- $this->loadingServices = true;
- if (count($this->allServices) > 0 && ! $force) {
- if (! $this->search) {
- $this->services = $this->allServices;
+ ],
+ [
+ 'id' => 'mariadb',
+ 'name' => 'MariaDB',
+ 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'redis',
+ 'name' => 'Redis',
+ 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'keydb',
+ 'name' => 'KeyDB',
+ 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'dragonfly',
+ 'name' => 'Dragonfly',
+ 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'mongodb',
+ 'name' => 'MongoDB',
+ 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'clickhouse',
+ 'name' => 'ClickHouse',
+ 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.',
+ 'logo' => '
',
+ ],
- return;
- }
- $this->services = $this->allServices->filter(function ($service, $key) {
- $tags = collect(data_get($service, 'tags', []));
+ ];
- return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) {
- return str_contains(strtolower($tag), strtolower($this->search));
- });
- });
- } else {
- $this->search = null;
- $this->allServices = get_service_templates($force);
- $this->services = $this->allServices->filter(function ($service, $key) {
- return str_contains(strtolower($key), strtolower($this->search));
- });
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
- $this->loadingServices = false;
- }
+ return [
+ 'services' => $services,
+ 'gitBasedApplications' => $gitBasedApplications,
+ 'dockerBasedApplications' => $dockerBasedApplications,
+ 'databases' => $databases,
+ ];
}
public function instantSave()
@@ -139,6 +217,7 @@ class Select extends Component
public function setType(string $type)
{
+ $type = str($type)->lower()->slug()->value();
if ($this->loading) {
return;
}
@@ -176,10 +255,12 @@ class Select extends Component
return;
}
- // if (count($this->servers) === 1) {
- // $server = $this->servers->first();
- // $this->setServer($server);
- // }
+ if (count($this->servers) === 1) {
+ $server = $this->servers->first();
+ if ($server instanceof Server) {
+ $this->setServer($server);
+ }
+ }
if (! is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server->id)->first();
if ($foundServer) {
@@ -195,6 +276,15 @@ class Select extends Component
$this->server = $server;
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
+ $count = count($this->standaloneDockers) + count($this->swarmDockers);
+ if ($count === 1) {
+ $docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
+ if ($docker) {
+ $this->setDestination($docker->uuid);
+
+ return $this->whatToDoNext();
+ }
+ }
$this->current_step = 'destinations';
}
@@ -202,18 +292,41 @@ 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();
+ $this->servers = Server::isUsable()->get()->sortBy('name');
$this->allServers = $this->servers;
}
}
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index 6f6bc9185..3c7f42329 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -53,7 +53,7 @@ CMD ["nginx", "-g", "daemon off;"]
$port = 80;
}
$application = Application::create([
- 'name' => 'dockerfile-'.new Cuid2(7),
+ 'name' => 'dockerfile-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main',
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 341dd93d8..9266a57fc 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,15 @@ 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/Resource/EnvironmentSelect.php b/app/Livewire/Project/Resource/EnvironmentSelect.php
new file mode 100644
index 000000000..efb1b6ca2
--- /dev/null
+++ b/app/Livewire/Project/Resource/EnvironmentSelect.php
@@ -0,0 +1,35 @@
+selectedEnvironment = request()->route('environment_name');
+ $this->project_uuid = request()->route('project_uuid');
+ }
+
+ public function updatedSelectedEnvironment($value)
+ {
+ if ($value === 'edit') {
+ return redirect()->route('project.show', [
+ 'project_uuid' => $this->project_uuid,
+ ]);
+ } else {
+ return redirect()->route('project.resource.index', [
+ 'project_uuid' => $this->project_uuid,
+ 'environment_name' => $value,
+ ]);
+ }
+ }
+}
diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php
index 71ce2c356..283496887 100644
--- a/app/Livewire/Project/Resource/Index.php
+++ b/app/Livewire/Project/Resource/Index.php
@@ -32,8 +32,11 @@ class Index extends Component
public $services = [];
+ public array $parameters;
+
public function mount()
{
+ $this->parameters = get_route_parameters();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
@@ -44,7 +47,6 @@ class Index extends Component
}
$this->project = $project;
$this->environment = $environment;
-
$this->applications = $this->environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index 47534ded1..319ead361 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Service;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Configuration extends Component
@@ -20,7 +21,7 @@ class Configuration extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
@@ -52,7 +53,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);
@@ -65,7 +66,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);
@@ -76,8 +77,13 @@ class Configuration extends Component
{
try {
GetContainersStatus::run($this->service->server);
- // dispatch_sync(new ContainerStatusJob($this->service->server));
- $this->dispatch('refresh')->self();
+ $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/Database.php b/app/Livewire/Project/Service/Database.php
index 9804fb5ba..9f02db05c 100644
--- a/app/Livewire/Project/Service/Database.php
+++ b/app/Livewire/Project/Service/Database.php
@@ -95,8 +95,7 @@ class Database extends Component
$this->database->save();
updateCompose($this->database);
$this->dispatch('success', 'Database saved.');
- } catch (\Throwable $e) {
- ray($e);
+ } catch (\Throwable) {
} finally {
$this->dispatch('generateDockerCompose');
}
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index fd4d684b1..dc043e65a 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -11,12 +11,29 @@ class EditCompose extends Component
public $serviceId;
+ protected $listeners = [
+ 'refreshEnvs',
+ 'envsUpdated',
+ 'refresh' => 'envsUpdated',
+ ];
+
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.is_container_label_escape_enabled' => 'required',
];
+ public function envsUpdated()
+ {
+ $this->dispatch('saveCompose', $this->service->docker_compose_raw);
+ $this->refreshEnvs();
+ }
+
+ public function refreshEnvs()
+ {
+ $this->service = Service::find($this->serviceId);
+ }
+
public function mount()
{
$this->service = Service::find($this->serviceId);
@@ -26,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/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php
index 70e8006c7..e89aeda85 100644
--- a/app/Livewire/Project/Service/EditDomain.php
+++ b/app/Livewire/Project/Service/EditDomain.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
+use Spatie\Url\Url;
class EditDomain extends Component
{
@@ -21,20 +22,21 @@ class EditDomain extends Component
$this->application = ServiceApplication::find($this->applicationId);
}
- public function updatedApplicationFqdn()
- {
- $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->application->save();
- }
-
public function submit()
{
try {
+ $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
+
+ return str($domain)->trim()->lower();
+ });
+ $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
+ $warning = sslipDomainWarning($this->application->fqdn);
+ if ($warning) {
+ $this->dispatch('warning', __('warning.sslipdomain'));
+ }
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -42,14 +44,18 @@ class EditDomain extends Component
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED. Only use multiple domains if you know what you are doing.');
} else {
- $this->dispatch('success', 'Service saved.');
+ ! $warning && $this->dispatch('success', 'Service saved.');
}
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
+
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 201ebf58f..4d070bc0c 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Service;
use App\Models\Application;
+use App\Models\InstanceSettings;
use App\Models\LocalFileVolume;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -14,7 +15,8 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
-use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
@@ -27,23 +29,27 @@ class FileStorage extends Component
public ?string $workdir = null;
+ public bool $permanently_delete = true;
+
protected $rules = [
'fileStorage.is_directory' => 'required',
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
+ 'fileStorage.is_based_on_git' => 'required|boolean',
];
public function mount()
{
$this->resource = $this->fileStorage->service;
- if (Str::of($this->fileStorage->fs_path)->startsWith('.')) {
+ if (str($this->fileStorage->fs_path)->startsWith('.')) {
$this->workdir = $this->resource->service?->workdir();
- $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.');
+ $this->fs_path = str($this->fileStorage->fs_path)->after('.');
} else {
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
+ $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -52,12 +58,13 @@ 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) {
return handleError($e, $this);
} finally {
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
}
}
@@ -67,25 +74,43 @@ 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) {
return handleError($e, $this);
} finally {
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
}
}
- public function delete()
+ public function delete($password)
{
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+ }
+
try {
- $this->fileStorage->deleteStorageOnServer();
+ $message = 'File deleted.';
+ if ($this->fileStorage->is_directory) {
+ $message = 'Directory deleted.';
+ }
+ if ($this->permanently_delete) {
+ $message = 'Directory deleted from the server.';
+ $this->fileStorage->deleteStorageOnServer();
+ }
$this->fileStorage->delete();
- $this->dispatch('success', 'File deleted.');
+ $this->dispatch('success', $message);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
}
}
@@ -115,6 +140,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/Index.php b/app/Livewire/Project/Service/Index.php
index 0a7b6ec90..ba4ebe2fc 100644
--- a/app/Livewire/Project/Service/Index.php
+++ b/app/Livewire/Project/Service/Index.php
@@ -48,7 +48,6 @@ class Index extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
-
}
public function generateDockerCompose()
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index 7d3987b3d..ee43dc911 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -7,6 +7,7 @@ use App\Actions\Service\StopService;
use App\Actions\Shared\PullImage;
use App\Events\ServiceStatusChanged;
use App\Models\Service;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
@@ -20,10 +21,13 @@ 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)) {
- ray('isConfigurationChanged init');
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
@@ -31,16 +35,17 @@ class Navbar extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
+ 'envsUpdated' => '$refresh',
];
}
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');
@@ -49,24 +54,30 @@ class Navbar extends Component
}
}
+ public function check_status_without_notification()
+ {
+ $this->dispatch('check_status');
+ }
+
public function check_status()
{
$this->dispatch('check_status');
$this->dispatch('success', 'Service status updated.');
}
- public function render()
- {
- return view('livewire.project.service.navbar');
- }
-
public function checkDeployments()
{
- $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
- $status = data_get($activity, 'properties.status');
- if ($status === 'queued' || $status === 'in_progress') {
- $this->isDeploymentProgress = true;
- } else {
+ try {
+ // TODO: This is a temporary solution. We need to refactor this.
+ // We need to delete null bytes somehow.
+ $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
+ $status = data_get($activity, 'properties.status');
+ if ($status === 'queued' || $status === 'in_progress') {
+ $this->isDeploymentProgress = true;
+ } else {
+ $this->isDeploymentProgress = false;
+ }
+ } catch (\Throwable) {
$this->isDeploymentProgress = false;
}
}
@@ -84,14 +95,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();
}
@@ -103,11 +109,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..8324ee645 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -2,8 +2,12 @@
namespace App\Livewire\Project\Service;
+use App\Models\InstanceSettings;
use App\Models\ServiceApplication;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
+use Spatie\Url\Url;
class ServiceApplicationView extends Component
{
@@ -11,6 +15,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,22 +31,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();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->application->save();
- }
-
public function instantSave()
{
$this->submit();
@@ -56,8 +48,16 @@ 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 (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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.');
@@ -76,6 +76,18 @@ class ServiceApplicationView extends Component
public function submit()
{
try {
+ $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
+
+ return str($domain)->trim()->lower();
+ });
+ $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
+ $warning = sslipDomainWarning($this->application->fqdn);
+ if ($warning) {
+ $this->dispatch('warning', __('warning.sslipdomain'));
+ }
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -83,12 +95,29 @@ class ServiceApplicationView extends Component
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED. Only use multiple domains if you know what you are doing.');
} else {
- $this->dispatch('success', 'Service saved.');
+ ! $warning && $this->dispatch('success', 'Service saved.');
}
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
$this->dispatch('generateDockerCompose');
+ } catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
+
+ return handleError($e, $this);
}
}
+
+ 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 05917f895..2c751aa92 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -33,7 +33,8 @@ 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);
+ $customHelper = data_get($field, 'customHelper', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -41,19 +42,28 @@ class StackForm extends Component
'value' => $value,
'isPassword' => $isPassword,
'rules' => $rules,
+ 'customHelper' => $customHelper,
]);
$this->rules["fields.$key.value"] = $rules;
$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 +72,7 @@ class StackForm extends Component
$this->dispatch('success', 'Service settings saved.');
}
- public function submit()
+ public function submit($notify = true)
{
try {
$this->validate();
@@ -75,14 +85,12 @@ class StackForm extends Component
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
- $this->dispatch('refreshStacks');
$this->dispatch('refreshEnvs');
- $this->dispatch('success', 'Service saved.');
+ $notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
if (is_null($this->service->config_hash)) {
- ray('asdf');
$this->service->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php
index 161c38097..4b64a8b5e 100644
--- a/app/Livewire/Project/Service/Storage.php
+++ b/app/Livewire/Project/Service/Storage.php
@@ -9,14 +9,36 @@ class Storage extends Component
{
public $resource;
+ public $fileStorage;
+
public function getListeners()
{
+ $teamId = auth()->user()->currentTeam()->id;
+
return [
+ "echo-private:team.{$teamId},FileStorageChanged" => 'refreshStoragesFromEvent',
+ 'refreshStorages',
'addNewVolume',
- 'refresh_storages' => '$refresh',
];
}
+ public function mount()
+ {
+ $this->refreshStorages();
+ }
+
+ public function refreshStoragesFromEvent()
+ {
+ $this->refreshStorages();
+ $this->dispatch('warning', 'File storage changed. Usually it means that the file / directory is already defined on the server, so Coolify set it up for you properly on the UI.');
+ }
+
+ public function refreshStorages()
+ {
+ $this->fileStorage = $this->resource->fileStorages()->get();
+ $this->dispatch('$refresh');
+ }
+
public function addNewVolume($data)
{
try {
@@ -30,7 +52,7 @@ class Storage extends Component
$this->resource->refresh();
$this->dispatch('success', 'Storage added successfully');
$this->dispatch('clearAddStorage');
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index e754749a4..a0b4ac2c4 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -3,6 +3,12 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
+use App\Models\InstanceSettings;
+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,28 +16,94 @@ class Danger extends Component
{
public $resource;
+ public $resourceName;
+
public $projectUuid;
public $environmentName;
public bool $delete_configurations = true;
+ 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(7);
$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;
+ }
+
+ $this->resourceName = match ($this->resource->type()) {
+ 'application' => $this->resource->name ?? 'Application',
+ 'standalone-postgresql',
+ 'standalone-redis',
+ 'standalone-mongodb',
+ 'standalone-mysql',
+ 'standalone-mariadb',
+ 'standalone-keydb',
+ 'standalone-dragonfly',
+ 'standalone-clickhouse' => $this->resource->name ?? 'Database',
+ 'service' => $this->resource->name ?? 'Service',
+ 'service-application' => $this->resource->name ?? 'Service Application',
+ 'service-database' => $this->resource->name ?? 'Service Database',
+ default => 'Unknown Resource',
+ };
}
- public function delete()
+ public function delete($password)
{
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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);
+ 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,
@@ -41,4 +113,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 22ada8ab8..c305e817c 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -5,9 +5,11 @@ namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
-use App\Jobs\ContainerStatusJob;
+use App\Models\InstanceSettings;
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;
@@ -67,7 +69,7 @@ class Destination extends Component
return;
}
- $deployment_uuid = new Cuid2(7);
+ $deployment_uuid = new Cuid2;
$server = Server::find($server_id);
$destination = StandaloneDocker::find($network_id);
queue_application_deployment(
@@ -115,8 +117,16 @@ 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 (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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 b732b6b52..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 4c06bfe23..787d33a69 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 = [
@@ -34,227 +35,234 @@ class All extends Component
public function mount()
{
$this->resourceClass = get_class($this->resource);
- $resourceWithPreviews = ['App\Models\Application'];
+ $resourceWithPreviews = [\App\Models\Application::class];
$simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile'));
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
$this->showPreview = true;
}
- $this->modalId = new Cuid2(7);
- $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->modalId = new Cuid2;
+ $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);
- ray($variables, $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);
}
+
+ $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();
+ }
+ $order++;
+ }
+ }
+ }
+
+ private function handleBulkSubmit()
+ {
+ $variables = parseEnvFormatToArray($this->variables);
+ $this->deleteRemovedVariables(false, $variables);
+ $this->updateOrCreateVariables(false, $variables);
+
+ 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) {
- continue;
+ if (! $found->is_shown_once && ! $found->is_multiline) {
+ $found->value = $value;
+ $found->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.');
-
- return;
- }
- }
- $found->save();
-
- continue;
} else {
- $environment = new EnvironmentVariable();
+ $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 e77c05d6b..e71cd9f42 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -24,6 +24,8 @@ class Show extends Component
public string $type;
protected $listeners = [
+ 'refreshEnvs' => 'refresh',
+ 'refresh',
'compose_loaded' => '$refresh',
];
@@ -35,6 +37,7 @@ class Show extends Component
'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
+ 'env.is_required' => 'required|boolean',
];
protected $validationAttributes = [
@@ -44,14 +47,21 @@ class Show extends Component
'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once',
+ 'env.is_required' => 'Required',
];
+ public function refresh()
+ {
+ $this->env->refresh();
+ $this->checkEnvs();
+ }
+
public function mount()
{
- if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
+ if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) {
$this->isSharedVariable = true;
}
- $this->modalId = new Cuid2(7);
+ $this->modalId = new Cuid2;
$this->parameters = get_route_parameters();
$this->checkEnvs();
}
@@ -70,7 +80,7 @@ class Show extends Component
public function serialize()
{
data_forget($this->env, 'real_value');
- if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
+ if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) {
data_forget($this->env, 'is_build_time');
}
}
@@ -101,18 +111,24 @@ class Show extends Component
} else {
$this->validate();
}
- if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
- $type = str($this->env->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;
- }
+ if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) {
+ $oldValue = $this->env->getOriginal('value');
+ $this->env->value = $oldValue;
+ $this->dispatch('error', 'Required environment variable cannot be empty.');
+
+ return;
}
+
$this->serialize();
+
+ if ($this->isSharedVariable) {
+ unset($this->env->is_required);
+ }
+
$this->env->save();
$this->dispatch('success', 'Environment variable updated.');
- $this->dispatch('refreshEnvs');
+ $this->dispatch('envsUpdated');
} catch (\Exception $e) {
return handleError($e);
}
@@ -122,7 +138,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 dc3a62c56..621ab1bac 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -6,13 +6,14 @@ 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;
@@ -22,8 +23,6 @@ class ExecuteContainerCommand extends Component
public string $type;
- public string $workDir = '';
-
public Server $server;
public Collection $servers;
@@ -32,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();
@@ -51,6 +52,7 @@ class ExecuteContainerCommand extends Component
$this->servers = $this->servers->push($server);
}
}
+ $this->loadContainers();
} elseif (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
@@ -61,23 +63,18 @@ 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);
+ $this->loadContainers();
} 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();
+ $this->loadContainers();
+ } elseif (data_get($this->parameters, 'server_uuid')) {
+ $this->type = 'server';
+ $this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail();
+ $this->server = $this->resource;
}
}
@@ -95,50 +92,97 @@ 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();
+ }
+ if ($this->containers->count() === 1) {
+ $this->selected_container = data_get($this->containers->first(), 'container.Names');
}
}
- public function runCommand()
+ #[On('connectToServer')]
+ public function connectToServer()
{
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();
+ if ($this->server->isForceDisabled()) {
+ throw new \RuntimeException('Server is disabled.');
}
+ $this->dispatch(
+ 'send-terminal-command',
+ false,
+ data_get($this->server, 'name'),
+ data_get($this->server, 'uuid')
+ );
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ #[On('connectToContainer')]
+ public function connectToContainer()
+ {
+ if ($this->selected_container === 'default') {
+ $this->dispatch('error', 'Please select a container.');
+
+ return;
+ }
+ try {
+ $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 = remote_process([$exec], $server, ignore_errors: true);
- $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..43fd97c34 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;
@@ -38,12 +39,12 @@ class GetLogs extends Component
public ?bool $showTimeStamps = true;
- public int $numberOfLines = 100;
+ public ?int $numberOfLines = 100;
public function mount()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === 'App\Models\Application') {
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
@@ -52,7 +53,7 @@ class GetLogs extends Component
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
- if ($this->resource?->getMorphClass() === 'App\Models\Application') {
+ if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
}
@@ -68,11 +69,11 @@ class GetLogs extends Component
public function instantSave()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === 'App\Models\Application') {
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
}
- if ($this->resource->getMorphClass() === 'App\Models\Service') {
+ if ($this->resource->getMorphClass() === \App\Models\Service::class) {
$serviceName = str($this->container)->beforeLast('-')->value();
$subType = $this->resource->applications()->where('name', $serviceName)->first();
if ($subType) {
@@ -94,10 +95,10 @@ class GetLogs extends Component
if (! $this->server->isFunctional()) {
return;
}
- if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
+ if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
return;
}
- if (! $this->numberOfLines) {
+ if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
$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/Logs.php b/app/Livewire/Project/Shared/Logs.php
index 008d743ed..12022b1ee 100644
--- a/app/Livewire/Project/Shared/Logs.php
+++ b/app/Livewire/Project/Shared/Logs.php
@@ -59,15 +59,6 @@ class Logs extends Component
}
}
- public function loadMetrics()
- {
- return;
- $server = data_get($this->resource, 'destination.server');
- if ($server->isFunctional()) {
- $this->cpu = $server->getMetrics();
- }
- }
-
public function mount()
{
try {
@@ -118,11 +109,7 @@ class Logs extends Component
$this->containers = $this->containers->filter(function ($container) {
return str_contains($container, $this->query['pull_request_id']);
});
- ray($this->containers);
-
}
-
- $this->loadMetrics();
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php
new file mode 100644
index 000000000..fdc35fc0f
--- /dev/null
+++ b/app/Livewire/Project/Shared/Metrics.php
@@ -0,0 +1,59 @@
+poll || $this->interval <= 10) {
+ $this->loadData();
+ if ($this->interval > 10) {
+ $this->poll = false;
+ }
+ }
+ }
+
+ public function loadData()
+ {
+ try {
+ $cpuMetrics = $this->resource->getCpuMetrics($this->interval);
+ $memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
+ $this->dispatch("refreshChartData-{$this->chartId}-cpu", [
+ 'seriesData' => $cpuMetrics,
+ ]);
+ $this->dispatch("refreshChartData-{$this->chartId}-memory", [
+ 'seriesData' => $memoryMetrics,
+ ]);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function setInterval()
+ {
+ if ($this->interval <= 10) {
+ $this->poll = true;
+ }
+ $this->loadData();
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.metrics');
+ }
+}
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index 586a125ae..e67df6aa9 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -39,9 +39,9 @@ class ResourceOperations extends Component
if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.');
}
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$server = $new_destination->server;
- if ($this->resource->getMorphClass() === 'App\Models\Application') {
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
@@ -78,16 +78,16 @@ class ResourceOperations extends Component
return redirect()->to($route);
} elseif (
- $this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneRedis' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' ||
- $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse'
+ $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
) {
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
@@ -121,7 +121,7 @@ class ResourceOperations extends Component
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
- $uuid = (string) new Cuid2(7);
+ $uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate()->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
@@ -147,7 +147,6 @@ class ResourceOperations extends Component
return redirect()->to($route);
}
-
}
public function moveTo($environment_id)
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php
index f36b7b141..adfd59217 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Add.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php
@@ -55,8 +55,8 @@ class Add extends Component
return;
}
- if (empty($this->container) || $this->container == 'null') {
- if ($this->type == 'service') {
+ if (empty($this->container) || $this->container === 'null') {
+ if ($this->type === 'service') {
$this->container = $this->subServiceName;
}
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php
index 1aa5a2b87..6ab8426f3 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/All.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/All.php
@@ -21,12 +21,12 @@ class All extends Component
public function mount()
{
$this->parameters = get_route_parameters();
- if ($this->resource->type() == 'service') {
+ if ($this->resource->type() === 'service') {
$this->containerNames = $this->resource->applications()->pluck('name');
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
- } elseif ($this->resource->type() == 'application') {
+ } 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 {
@@ -43,7 +43,7 @@ class All extends Component
public function submit($data)
{
try {
- $task = new ScheduledTask();
+ $task = new ScheduledTask;
$task->name = $data['name'];
$task->command = $data['command'];
$task->frequency = $data['frequency'];
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
index 7a2e14e89..0710e37ff 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
@@ -2,21 +2,60 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
+use App\Models\ScheduledTask;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Locked;
use Livewire\Component;
class Executions extends Component
{
- public $executions = [];
+ public ScheduledTask $task;
- public $selectedKey;
+ #[Locked]
+ public int $taskId;
+
+ #[Locked]
+ public Collection $executions;
+
+ #[Locked]
+ public ?int $selectedKey = null;
+
+ #[Locked]
+ public ?string $serverTimezone = null;
public function getListeners()
{
+ $teamId = Auth::user()->currentTeam()->id;
+
return [
- 'selectTask',
+ "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions',
];
}
+ public function mount($taskId)
+ {
+ try {
+ $this->taskId = $taskId;
+ $this->task = ScheduledTask::findOrFail($taskId);
+ $this->executions = $this->task->executions()->take(20)->get();
+ $this->serverTimezone = data_get($this->task, 'application.destination.server.settings.server_timezone');
+ if (! $this->serverTimezone) {
+ $this->serverTimezone = data_get($this->task, 'service.destination.server.settings.server_timezone');
+ }
+ if (! $this->serverTimezone) {
+ $this->serverTimezone = 'UTC';
+ }
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
+ }
+
+ public function refreshExecutions(): void
+ {
+ $this->executions = $this->task->executions()->take(20)->get();
+ }
+
public function selectTask($key): void
{
if ($key == $this->selectedKey) {
@@ -26,4 +65,17 @@ class Executions extends Component
}
$this->selectedKey = $key;
}
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->serverTimezone;
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } catch (\Exception) {
+ $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 dbd420d94..0900a1d70 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -2,71 +2,124 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
+use App\Jobs\ScheduledTaskJob;
use App\Models\Application;
-use App\Models\ScheduledTask as ModelsScheduledTask;
+use App\Models\ScheduledTask;
use App\Models\Service;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
-use Visus\Cuid2\Cuid2;
class Show extends Component
{
- public $parameters;
-
public Application|Service $resource;
- public ModelsScheduledTask $task;
+ public ScheduledTask $task;
- public ?string $modalId = null;
+ #[Locked]
+ public array $parameters;
+ #[Locked]
public string $type;
- protected $rules = [
- 'task.enabled' => 'required|boolean',
- 'task.name' => 'required|string',
- 'task.command' => 'required|string',
- 'task.frequency' => 'required|string',
- 'task.container' => 'nullable|string',
- ];
+ #[Validate(['boolean'])]
+ public bool $isEnabled = false;
- protected $validationAttributes = [
- 'name' => 'name',
- 'command' => 'command',
- 'frequency' => 'frequency',
- 'container' => 'container',
- ];
+ #[Validate(['string', 'required'])]
+ public string $name;
- public function mount()
+ #[Validate(['string', 'required'])]
+ public string $command;
+
+ #[Validate(['string', 'required'])]
+ public string $frequency;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $container = null;
+
+ #[Locked]
+ public ?string $application_uuid;
+
+ #[Locked]
+ public ?string $service_uuid;
+
+ #[Locked]
+ public string $task_uuid;
+
+ public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null)
{
- $this->parameters = get_route_parameters();
+ try {
+ $this->task_uuid = $task_uuid;
+ if ($application_uuid) {
+ $this->type = 'application';
+ $this->application_uuid = $application_uuid;
+ $this->resource = Application::ownedByCurrentTeam()->where('uuid', $application_uuid)->firstOrFail();
+ } elseif ($service_uuid) {
+ $this->type = 'service';
+ $this->service_uuid = $service_uuid;
+ $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail();
+ }
+ $this->parameters = [
+ 'environment_name' => $environment_name,
+ 'project_uuid' => $project_uuid,
+ 'application_uuid' => $application_uuid,
+ 'service_uuid' => $service_uuid,
+ ];
- if (data_get($this->parameters, 'application_uuid')) {
- $this->type = 'application';
- $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
- } elseif (data_get($this->parameters, 'service_uuid')) {
- $this->type = 'service';
- $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
+ $this->task = $this->resource->scheduled_tasks()->where('uuid', $task_uuid)->firstOrFail();
+ $this->syncData();
+ } catch (\Exception $e) {
+ return handleError($e);
}
+ }
- $this->modalId = new Cuid2(7);
- $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->task->enabled = $this->isEnabled;
+ $this->task->name = str($this->name)->trim()->value();
+ $this->task->command = str($this->command)->trim()->value();
+ $this->task->frequency = str($this->frequency)->trim()->value();
+ $this->task->container = str($this->container)->trim()->value();
+ $this->task->save();
+ } else {
+ $this->isEnabled = $this->task->enabled;
+ $this->name = $this->task->name;
+ $this->command = $this->task->command;
+ $this->frequency = $this->task->frequency;
+ $this->container = $this->task->container;
+ }
}
public function instantSave()
{
- $this->validateOnly('task.enabled');
- $this->task->save(['enabled' => $this->task->enabled]);
- $this->dispatch('success', 'Scheduled task updated.');
- $this->dispatch('refreshTasks');
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'Scheduled task updated.');
+ $this->refreshTasks();
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
}
public function submit()
{
- $this->validate();
- $this->task->name = str($this->task->name)->trim()->value();
- $this->task->container = str($this->task->container)->trim()->value();
- $this->task->save();
- $this->dispatch('success', 'Scheduled task updated.');
- $this->dispatch('refreshTasks');
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'Scheduled task updated.');
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
+ }
+
+ public function refreshTasks()
+ {
+ try {
+ $this->task->refresh();
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
}
public function delete()
@@ -74,13 +127,23 @@ class Show extends Component
try {
$this->task->delete();
- if ($this->type == 'application') {
- return redirect()->route('project.application.configuration', $this->parameters);
+ if ($this->type === 'application') {
+ return redirect()->route('project.application.configuration', $this->parameters, $this->task->name);
} else {
- return redirect()->route('project.service.configuration', $this->parameters);
+ return redirect()->route('project.service.configuration', $this->parameters, $this->task->name);
}
} catch (\Exception $e) {
return handleError($e);
}
}
+
+ public function executeNow()
+ {
+ try {
+ ScheduledTaskJob::dispatch($this->task);
+ $this->dispatch('success', 'Scheduled task executed.');
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
+ }
}
diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php
index d22f3b05f..6e250bd90 100644
--- a/app/Livewire/Project/Shared/Storages/Add.php
+++ b/app/Livewire/Project/Shared/Storages/Add.php
@@ -54,7 +54,11 @@ class Add extends Component
public function mount()
{
- $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}";
+ if (str($this->resource->getMorphClass())->contains('Standalone')) {
+ $this->file_storage_directory_source = database_configuration_dir()."/{$this->resource->uuid}";
+ } else {
+ $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}";
+ }
$this->uuid = $this->resource->uuid;
$this->parameters = get_route_parameters();
if (data_get($this->parameters, 'application_uuid')) {
@@ -79,7 +83,7 @@ class Add extends Component
]);
$this->file_storage_path = trim($this->file_storage_path);
$this->file_storage_path = str($this->file_storage_path)->start('/')->value();
- if ($this->resource->getMorphClass() === 'App\Models\Application') {
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
$fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path;
}
LocalFileVolume::create(
@@ -92,11 +96,10 @@ class Add extends Component
'resource_type' => get_class($this->resource),
],
);
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
-
}
public function submitFileStorageDirectory()
@@ -119,11 +122,10 @@ class Add extends Component
'resource_type' => get_class($this->resource),
],
);
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
-
}
public function submitPersistentVolume()
@@ -140,7 +142,6 @@ class Add extends Component
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,
]);
-
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php
index d2014694e..c26315d3b 100644
--- a/app/Livewire/Project/Shared/Storages/All.php
+++ b/app/Livewire/Project/Shared/Storages/All.php
@@ -8,5 +8,5 @@ class All extends Component
{
public $resource;
- protected $listeners = ['refresh_storages' => '$refresh'];
+ protected $listeners = ['refreshStorages' => '$refresh'];
}
diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php
index 52b52ef6d..54b1be3af 100644
--- a/app/Livewire/Project/Shared/Storages/Show.php
+++ b/app/Livewire/Project/Shared/Storages/Show.php
@@ -2,9 +2,11 @@
namespace App\Livewire\Project\Shared\Storages;
+use App\Models\InstanceSettings;
use App\Models\LocalPersistentVolume;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
-use Visus\Cuid2\Cuid2;
class Show extends Component
{
@@ -12,8 +14,6 @@ class Show extends Component
public bool $isReadOnly = false;
- public ?string $modalId = null;
-
public bool $isFirst = true;
public bool $isService = false;
@@ -32,11 +32,6 @@ class Show extends Component
'host_path' => 'host',
];
- public function mount()
- {
- $this->modalId = new Cuid2(7);
- }
-
public function submit()
{
$this->validate();
@@ -44,9 +39,17 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully');
}
- public function delete()
+ public function delete($password)
{
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+ }
+
$this->storage->delete();
- $this->dispatch('refresh_storages');
+ $this->dispatch('refreshStorages');
}
}
diff --git a/app/Livewire/Project/Shared/Tags.php b/app/Livewire/Project/Shared/Tags.php
index 85d5c21dc..dca6180ff 100644
--- a/app/Livewire/Project/Shared/Tags.php
+++ b/app/Livewire/Project/Shared/Tags.php
@@ -3,32 +3,63 @@
namespace App\Livewire\Project\Shared;
use App\Models\Tag;
+use Livewire\Attributes\Validate;
use Livewire\Component;
+// Refactored ✅
class Tags extends Component
{
public $resource = null;
- public ?string $new_tag = null;
+ #[Validate('required|string|min:2')]
+ public string $newTags;
public $tags = [];
- protected $listeners = [
- 'refresh' => '$refresh',
- ];
-
- protected $rules = [
- 'resource.tags.*.name' => 'required|string|min:2',
- 'new_tag' => 'required|string|min:2',
- ];
-
- protected $validationAttributes = [
- 'new_tag' => 'tag',
- ];
+ public $filteredTags = [];
public function mount()
+ {
+ $this->loadTags();
+ }
+
+ public function loadTags()
{
$this->tags = Tag::ownedByCurrentTeam()->get();
+ $this->filteredTags = $this->tags->filter(function ($tag) {
+ return ! $this->resource->tags->contains($tag);
+ });
+ }
+
+ public function submit()
+ {
+ try {
+ $this->validate();
+ $tags = str($this->newTags)->trim()->explode(' ');
+ foreach ($tags as $tag) {
+ if (strlen($tag) < 2) {
+ $this->dispatch('error', 'Invalid tag.', "Tag $tag is invalid. Min length is 2.");
+
+ continue;
+ }
+ if ($this->resource->tags()->where('name', $tag)->exists()) {
+ $this->dispatch('error', 'Duplicate tags.', "Tag $tag already added.");
+
+ continue;
+ }
+ $found = Tag::ownedByCurrentTeam()->where(['name' => $tag])->exists();
+ if (! $found) {
+ $found = Tag::create([
+ 'name' => $tag,
+ 'team_id' => currentTeam()->id,
+ ]);
+ }
+ $this->resource->tags()->attach($found->id);
+ }
+ $this->refresh();
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
}
public function addTag(string $id, string $name)
@@ -39,8 +70,9 @@ class Tags extends Component
return;
}
- $this->resource->tags()->syncWithoutDetaching($id);
+ $this->resource->tags()->attach($id);
$this->refresh();
+ $this->dispatch('success', 'Tag added.');
} catch (\Exception $e) {
return handleError($e, $this);
}
@@ -50,12 +82,12 @@ class Tags extends Component
{
try {
$this->resource->tags()->detach($id);
-
- $found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
- if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) {
+ $found_more_tags = Tag::ownedByCurrentTeam()->find($id);
+ if ($found_more_tags && $found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) {
$found_more_tags->delete();
}
$this->refresh();
+ $this->dispatch('success', 'Tag deleted.');
} catch (\Exception $e) {
return handleError($e, $this);
}
@@ -63,41 +95,8 @@ class Tags extends Component
public function refresh()
{
- $this->resource->load(['tags']);
- $this->tags = Tag::ownedByCurrentTeam()->get();
- $this->new_tag = null;
- }
-
- public function submit()
- {
- try {
- $this->validate([
- 'new_tag' => 'required|string|min:2',
- ]);
- $tags = str($this->new_tag)->trim()->explode(' ');
- foreach ($tags as $tag) {
- if ($this->resource->tags()->where('name', $tag)->exists()) {
- $this->dispatch('error', 'Duplicate tags.', "Tag $tag already added.");
-
- continue;
- }
- $found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first();
- if (! $found) {
- $found = Tag::create([
- 'name' => $tag,
- 'team_id' => currentTeam()->id,
- ]);
- }
- $this->resource->tags()->syncWithoutDetaching($found->id);
- }
- $this->refresh();
- } catch (\Exception $e) {
- return handleError($e, $this);
- }
- }
-
- public function render()
- {
- return view('livewire.project.shared.tags');
+ $this->resource->refresh(); // Remove this when legacy_model_binding is false
+ $this->loadTags();
+ $this->reset('newTags');
}
}
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
new file mode 100644
index 000000000..5af8f057e
--- /dev/null
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -0,0 +1,57 @@
+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/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php
new file mode 100644
index 000000000..1b10f588b
--- /dev/null
+++ b/app/Livewire/Project/Shared/UploadConfig.php
@@ -0,0 +1,46 @@
+config = '{
+ "build_pack": "nixpacks",
+ "base_directory": "/nodejs",
+ "publish_directory": "/",
+ "ports_exposes": "3000",
+ "settings": {
+ "is_static": false
+ }
+}';
+ }
+ }
+
+ public function uploadConfig()
+ {
+ try {
+ $application = Application::findOrFail($this->applicationId);
+ $application->setConfig($this->config);
+ $this->dispatch('success', 'Application settings updated');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+
+ return;
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.upload-config');
+ }
+}
diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php
index e96bd888e..aab1fdc47 100644
--- a/app/Livewire/Project/Shared/Webhooks.php
+++ b/app/Livewire/Project/Shared/Webhooks.php
@@ -4,49 +4,61 @@ namespace App\Livewire\Project\Shared;
use Livewire\Component;
+// Refactored ✅
class Webhooks extends Component
{
public $resource;
- public ?string $deploywebhook = null;
+ public ?string $deploywebhook;
- public ?string $githubManualWebhook = null;
+ public ?string $githubManualWebhook;
- public ?string $gitlabManualWebhook = null;
+ public ?string $gitlabManualWebhook;
- public ?string $bitbucketManualWebhook = null;
+ public ?string $bitbucketManualWebhook;
- public ?string $giteaManualWebhook = null;
+ public ?string $giteaManualWebhook;
- protected $rules = [
- 'resource.manual_webhook_secret_github' => 'nullable|string',
- 'resource.manual_webhook_secret_gitlab' => 'nullable|string',
- 'resource.manual_webhook_secret_bitbucket' => 'nullable|string',
- 'resource.manual_webhook_secret_gitea' => 'nullable|string',
- ];
+ public ?string $githubManualWebhookSecret = null;
- public function saveSecret()
+ public ?string $gitlabManualWebhookSecret = null;
+
+ public ?string $bitbucketManualWebhookSecret = null;
+
+ public ?string $giteaManualWebhookSecret = null;
+
+ public function mount()
+ {
+ // ray()->clearAll();
+ // ray()->showQueries();
+ $this->deploywebhook = generateDeployWebhook($this->resource);
+
+ $this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
+ $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
+
+ $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
+ $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
+
+ $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
+ $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
+
+ $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
+ $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
+ }
+
+ public function submit()
{
try {
- $this->validate();
- $this->resource->save();
+ $this->authorize('update', $this->resource);
+ $this->resource->update([
+ 'manual_webhook_secret_github' => $this->githubManualWebhookSecret,
+ 'manual_webhook_secret_gitlab' => $this->gitlabManualWebhookSecret,
+ 'manual_webhook_secret_bitbucket' => $this->bitbucketManualWebhookSecret,
+ 'manual_webhook_secret_gitea' => $this->giteaManualWebhookSecret,
+ ]);
$this->dispatch('success', 'Secret Saved.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
-
- public function mount()
- {
- $this->deploywebhook = generateDeployWebhook($this->resource);
- $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
- $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
- $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
- $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
- }
-
- public function render()
- {
- return view('livewire.project.shared.webhooks');
- }
}
diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php
index d5d660017..2335519c7 100644
--- a/app/Livewire/Project/Show.php
+++ b/app/Livewire/Project/Show.php
@@ -2,24 +2,46 @@
namespace App\Livewire\Project;
+use App\Models\Environment;
use App\Models\Project;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Show extends Component
{
public Project $project;
- public function mount()
- {
- $projectUuid = request()->route('project_uuid');
- $teamId = currentTeam()->id;
+ #[Validate(['required', 'string', 'min:3'])]
+ public string $name;
- $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
- if (! $project) {
- return redirect()->route('dashboard');
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
+
+ public function mount(string $project_uuid)
+ {
+ try {
+ $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $this->validate();
+ $environment = Environment::create([
+ 'name' => $this->name,
+ 'project_id' => $this->project->id,
+ ]);
+
+ return redirect()->route('project.resource.index', [
+ 'project_uuid' => $this->project->uuid,
+ 'environment_name' => $environment->name,
+ ]);
+ } catch (\Throwable $e) {
+ handleError($e, $this);
}
- $project->load(['environments']);
- $this->project = $project;
}
public function render()
diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php
deleted file mode 100644
index fc7f1eefc..000000000
--- a/app/Livewire/RunCommand.php
+++ /dev/null
@@ -1,42 +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 = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
- $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 c485a6a3a..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
@@ -10,6 +11,16 @@ class ApiTokens extends Component
public $tokens = [];
+ public bool $viewSensitiveData = false;
+
+ public bool $readOnly = true;
+
+ public bool $rootAccess = false;
+
+ public array $permissions = ['read-only'];
+
+ public $isApiEnabled;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -17,7 +28,52 @@ class ApiTokens extends Component
public function mount()
{
- $this->tokens = auth()->user()->tokens;
+ $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
+ $this->tokens = auth()->user()->tokens->sortByDesc('created_at');
+ }
+
+ public function updatedViewSensitiveData()
+ {
+ 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']);
+ }
+ $this->makeSureOneIsSelected();
+ }
+
+ public function updatedReadOnly()
+ {
+ 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']);
+ }
+ $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;
+ }
}
public function addNewToken()
@@ -26,7 +82,7 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
- $token = auth()->user()->createToken($this->description);
+ $token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
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..b9195b543 100644
--- a/app/Livewire/Security/PrivateKey/Show.php
+++ b/app/Livewire/Security/PrivateKey/Show.php
@@ -28,26 +28,28 @@ 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);
+ } catch (\Throwable) {
+ 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/Advanced.php b/app/Livewire/Server/Advanced.php
new file mode 100644
index 000000000..0852abebf
--- /dev/null
+++ b/app/Livewire/Server/Advanced.php
@@ -0,0 +1,115 @@
+server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ $this->parameters = get_route_parameters();
+ $this->syncData();
+ } catch (\Throwable) {
+ return redirect()->route('server.show');
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->server->settings->concurrent_builds = $this->concurrentBuilds;
+ $this->server->settings->dynamic_timeout = $this->dynamicTimeout;
+ $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
+ $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
+ $this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
+ $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold;
+ $this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
+ $this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
+ $this->server->settings->save();
+ } else {
+ $this->concurrentBuilds = $this->server->settings->concurrent_builds;
+ $this->dynamicTimeout = $this->server->settings->dynamic_timeout;
+ $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
+ $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency;
+ $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
+ $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
+ $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
+ $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
+ }
+ }
+
+ public function instantSave()
+ {
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'Server updated.');
+ // $this->dispatch('refreshServerShow');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ 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 submit()
+ {
+ try {
+ if (! validate_cron_expression($this->dockerCleanupFrequency)) {
+ $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
+ throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Server updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.server.advanced');
+ }
+}
diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php
new file mode 100644
index 000000000..d0db87f57
--- /dev/null
+++ b/app/Livewire/Server/Charts.php
@@ -0,0 +1,64 @@
+server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function pollData()
+ {
+ if ($this->poll || $this->interval <= 10) {
+ $this->loadData();
+ if ($this->interval > 10) {
+ $this->poll = false;
+ }
+ }
+ }
+
+ public function loadData()
+ {
+ try {
+ $cpuMetrics = $this->server->getCpuMetrics($this->interval);
+ $memoryMetrics = $this->server->getMemoryMetrics($this->interval);
+ $this->dispatch("refreshChartData-{$this->chartId}-cpu", [
+ 'seriesData' => $cpuMetrics,
+ ]);
+ $this->dispatch("refreshChartData-{$this->chartId}-memory", [
+ 'seriesData' => $memoryMetrics,
+ ]);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function setInterval()
+ {
+ if ($this->interval <= 10) {
+ $this->poll = true;
+ }
+ $this->loadData();
+ }
+}
diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php
new file mode 100644
index 000000000..f69fc8655
--- /dev/null
+++ b/app/Livewire/Server/CloudflareTunnels.php
@@ -0,0 +1,54 @@
+server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ if ($this->server->isLocalhost()) {
+ return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
+ }
+ $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave()
+ {
+ try {
+ $this->validate();
+ $this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
+ $this->server->settings->save();
+ $this->dispatch('success', 'Server updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function manualCloudflareConfig()
+ {
+ $this->isCloudflareTunnelsEnabled = true;
+ $this->server->settings->is_cloudflare_tunnel = true;
+ $this->server->settings->save();
+ $this->server->refresh();
+ $this->dispatch('success', 'Cloudflare Tunnels enabled.');
+ }
+
+ public function render()
+ {
+ return view('livewire.server.cloudflare-tunnels');
+ }
+}
diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php
index 7d2103e37..f58d7b6be 100644
--- a/app/Livewire/Server/ConfigureCloudflareTunnels.php
+++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php
@@ -21,7 +21,7 @@ class ConfigureCloudflareTunnels extends Component
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -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('serverInstalled');
+ $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..b9e3944b5 100644
--- a/app/Livewire/Server/Delete.php
+++ b/app/Livewire/Server/Delete.php
@@ -2,17 +2,38 @@
namespace App\Livewire\Server;
+use App\Actions\Server\DeleteServer;
+use App\Models\InstanceSettings;
+use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
{
use AuthorizesRequests;
- public $server;
+ public Server $server;
- public function delete()
+ public function mount(string $server_uuid)
{
+ try {
+ $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function delete($password)
+ {
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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()) {
@@ -21,6 +42,7 @@ class Delete extends Component
return;
}
$this->server->delete();
+ DeleteServer::dispatch($this->server);
return redirect()->route('server.index');
} catch (\Throwable $e) {
diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destination/Show.php
deleted file mode 100644
index 986e16cbf..000000000
--- a/app/Livewire/Server/Destination/Show.php
+++ /dev/null
@@ -1,31 +0,0 @@
-parameters = get_route_parameters();
- try {
- $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
- if (is_null($this->server)) {
- return redirect()->route('server.index');
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function render()
- {
- return view('livewire.server.destination.show');
- }
-}
diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php
new file mode 100644
index 000000000..dbab6e03f
--- /dev/null
+++ b/app/Livewire/Server/Destinations.php
@@ -0,0 +1,90 @@
+networks = collect();
+ $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ private function createNetworkAndAttachToProxy()
+ {
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
+ instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
+ }
+
+ public function add($name)
+ {
+ if ($this->server->isSwarm()) {
+ $found = $this->server->swarmDockers()->where('network', $name)->first();
+ if ($found) {
+ $this->dispatch('error', 'Network already added to this server.');
+
+ return;
+ } else {
+ SwarmDocker::create([
+ 'name' => $this->server->name.'-'.$name,
+ 'network' => $this->name,
+ 'server_id' => $this->server->id,
+ ]);
+ }
+ } else {
+ $found = $this->server->standaloneDockers()->where('network', $name)->first();
+ if ($found) {
+ $this->dispatch('error', 'Network already added to this server.');
+
+ return;
+ } else {
+ StandaloneDocker::create([
+ 'name' => $this->server->name.'-'.$name,
+ 'network' => $name,
+ 'server_id' => $this->server->id,
+ ]);
+ }
+ $this->createNetworkAndAttachToProxy();
+ }
+ }
+
+ public function scan()
+ {
+ if ($this->server->isSwarm()) {
+ $alreadyAddedNetworks = $this->server->swarmDockers;
+ } else {
+ $alreadyAddedNetworks = $this->server->standaloneDockers;
+ }
+ $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
+ $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
+ return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';
+ })->filter(function ($network) use ($alreadyAddedNetworks) {
+ return ! $alreadyAddedNetworks->contains('network', $network['Name']);
+ });
+ if ($this->networks->count() === 0) {
+ $this->dispatch('success', 'No new destinations found on this server.');
+
+ return;
+ }
+ $this->dispatch('success', 'Scan done.');
+ }
+
+ public function render()
+ {
+ return view('livewire.server.destinations');
+ }
+}
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
deleted file mode 100644
index 263ff6367..000000000
--- a/app/Livewire/Server/Form.php
+++ /dev/null
@@ -1,141 +0,0 @@
- '$refresh'];
-
- protected $rules = [
- 'server.name' => 'required',
- 'server.description' => 'nullable',
- 'server.ip' => 'required',
- 'server.user' => 'required',
- 'server.port' => 'required',
- 'server.settings.is_cloudflare_tunnel' => 'required|boolean',
- 'server.settings.is_reachable' => 'required',
- 'server.settings.is_swarm_manager' => 'required|boolean',
- 'server.settings.is_swarm_worker' => 'required|boolean',
- 'server.settings.is_build_server' => 'required|boolean',
- 'server.settings.concurrent_builds' => 'required|integer|min:1',
- 'server.settings.dynamic_timeout' => 'required|integer|min:1',
- 'wildcard_domain' => 'nullable|url',
- ];
-
- protected $validationAttributes = [
- 'server.name' => 'Name',
- 'server.description' => 'Description',
- 'server.ip' => 'IP address/Domain',
- 'server.user' => 'User',
- 'server.port' => 'Port',
- 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
- 'server.settings.is_reachable' => 'Is reachable',
- 'server.settings.is_swarm_manager' => 'Swarm Manager',
- 'server.settings.is_swarm_worker' => 'Swarm Worker',
- 'server.settings.is_build_server' => 'Build Server',
- 'server.settings.concurrent_builds' => 'Concurrent Builds',
- 'server.settings.dynamic_timeout' => 'Dynamic Timeout',
-
- ];
-
- public function mount()
- {
- $this->wildcard_domain = $this->server->settings->wildcard_domain;
- $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
- }
-
- public function serverInstalled()
- {
- $this->server->refresh();
- $this->server->settings->refresh();
- }
-
- public function updatedServerSettingsIsBuildServer()
- {
- $this->dispatch('serverInstalled');
- $this->dispatch('serverRefresh');
- $this->dispatch('proxyStatusUpdated');
- }
-
- public function instantSave()
- {
- try {
- refresh_server_connection($this->server->privateKey);
- $this->validateServer(false);
- $this->server->settings->save();
- $this->dispatch('success', 'Server updated.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function revalidate()
- {
- $this->revalidate = true;
- }
-
- public function checkLocalhostConnection()
- {
- $this->submit();
- ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
- if ($uptime) {
- $this->dispatch('success', 'Server is reachable.');
- $this->server->settings->is_reachable = true;
- $this->server->settings->is_usable = true;
- $this->server->settings->save();
- $this->dispatch('proxyStatusUpdated');
- } else {
- $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection. Check this documentation for further help. Error: '.$error);
-
- return;
- }
- }
-
- public function validateServer($install = true)
- {
- $this->dispatch('init', $install);
- }
-
- 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.');
-
- return;
- }
- refresh_server_connection($this->server->privateKey);
- $this->server->settings->wildcard_domain = $this->wildcard_domain;
- $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
- $this->server->settings->save();
- $this->server->save();
- $this->dispatch('success', 'Server updated.');
- }
-}
diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php
index 3d7b34de1..6599149c4 100644
--- a/app/Livewire/Server/LogDrains.php
+++ b/app/Livewire/Server/LogDrains.php
@@ -2,83 +2,132 @@
namespace App\Livewire\Server;
-use App\Actions\Server\InstallLogDrain;
+use App\Actions\Server\StartLogDrain;
+use App\Actions\Server\StopLogDrain;
use App\Models\Server;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class LogDrains extends Component
{
public Server $server;
- public $parameters = [];
+ #[Validate(['boolean'])]
+ public bool $isLogDrainNewRelicEnabled = false;
- protected $rules = [
- 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
- 'server.settings.logdrain_newrelic_license_key' => 'required|string',
- 'server.settings.logdrain_newrelic_base_uri' => 'required|string',
- 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
- 'server.settings.logdrain_highlight_project_id' => 'required|string',
- 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
- 'server.settings.logdrain_axiom_dataset_name' => 'required|string',
- 'server.settings.logdrain_axiom_api_key' => 'required|string',
- 'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
- 'server.settings.logdrain_custom_config' => 'required|string',
- 'server.settings.logdrain_custom_config_parser' => 'nullable',
- ];
+ #[Validate(['boolean'])]
+ public bool $isLogDrainCustomEnabled = false;
- protected $validationAttributes = [
- 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
- 'server.settings.logdrain_newrelic_license_key' => 'New Relic license key',
- 'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI',
- 'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain',
- 'server.settings.logdrain_highlight_project_id' => 'Highlight project ID',
- 'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
- 'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
- 'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
- 'server.settings.is_logdrain_custom_enabled' => 'Custom log drain',
- 'server.settings.logdrain_custom_config' => 'Custom log drain configuration',
- 'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser',
- ];
+ #[Validate(['boolean'])]
+ public bool $isLogDrainAxiomEnabled = false;
- public function mount()
+ #[Validate(['string', 'nullable'])]
+ public ?string $logDrainNewRelicLicenseKey = null;
+
+ #[Validate(['url', 'nullable'])]
+ public ?string $logDrainNewRelicBaseUri = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $logDrainAxiomDatasetName = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $logDrainAxiomApiKey = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $logDrainCustomConfig = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $logDrainCustomConfigParser = null;
+
+ public function mount(string $server_uuid)
{
- $this->parameters = get_route_parameters();
try {
- $server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
- if (is_null($server)) {
- return redirect()->route('server.index');
- }
- $this->server = $server;
+ $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function configureLogDrain()
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->customValidation();
+ $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled;
+ $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled;
+ $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled;
+
+ $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey;
+ $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri;
+ $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName;
+ $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey;
+ $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig;
+ $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser;
+
+ $this->server->settings->save();
+ } else {
+ $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled;
+ $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled;
+ $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled;
+
+ $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key;
+ $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri;
+ $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name;
+ $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key;
+ $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config;
+ $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser;
+ }
+ }
+
+ public function customValidation()
+ {
+ if ($this->isLogDrainNewRelicEnabled) {
+ try {
+ $this->validate([
+ 'logDrainNewRelicLicenseKey' => ['required'],
+ 'logDrainNewRelicBaseUri' => ['required', 'url'],
+ ]);
+ } catch (\Throwable $e) {
+ $this->isLogDrainNewRelicEnabled = false;
+
+ throw $e;
+ }
+ } elseif ($this->isLogDrainAxiomEnabled) {
+ try {
+ $this->validate([
+ 'logDrainAxiomDatasetName' => ['required'],
+ 'logDrainAxiomApiKey' => ['required'],
+ ]);
+ } catch (\Throwable $e) {
+ $this->isLogDrainAxiomEnabled = false;
+
+ throw $e;
+ }
+ } elseif ($this->isLogDrainCustomEnabled) {
+ try {
+ $this->validate([
+ 'logDrainCustomConfig' => ['required'],
+ 'logDrainCustomConfigParser' => ['string', 'nullable'],
+ ]);
+ } catch (\Throwable $e) {
+ $this->isLogDrainCustomEnabled = false;
+
+ throw $e;
+ }
+ }
+ }
+
+ public function instantSave()
{
try {
- InstallLogDrain::run($this->server);
- if (! $this->server->isLogDrainEnabled()) {
- $this->dispatch('serverRefresh');
+ $this->syncData(true);
+ if ($this->server->isLogDrainEnabled()) {
+ StartLogDrain::run($this->server);
+ $this->dispatch('success', 'Log drain service started.');
+ } else {
+ StopLogDrain::run($this->server);
$this->dispatch('success', 'Log drain service stopped.');
-
- return;
}
- $this->dispatch('serverRefresh');
- $this->dispatch('success', 'Log drain service started.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function instantSave(string $type)
- {
- try {
- $ok = $this->submit($type);
- if (! $ok) {
- return;
- }
- $this->configureLogDrain();
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -87,76 +136,10 @@ class LogDrains extends Component
public function submit(string $type)
{
try {
- $this->resetErrorBag();
- if ($type === 'newrelic') {
- $this->validate([
- 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
- 'server.settings.logdrain_newrelic_license_key' => 'required|string',
- 'server.settings.logdrain_newrelic_base_uri' => 'required|string',
- ]);
- $this->server->settings->update([
- 'is_logdrain_highlight_enabled' => false,
- 'is_logdrain_axiom_enabled' => false,
- 'is_logdrain_custom_enabled' => false,
- ]);
- } elseif ($type === 'highlight') {
- $this->validate([
- 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
- 'server.settings.logdrain_highlight_project_id' => 'required|string',
- ]);
- $this->server->settings->update([
- 'is_logdrain_newrelic_enabled' => false,
- 'is_logdrain_axiom_enabled' => false,
- 'is_logdrain_custom_enabled' => false,
- ]);
- } elseif ($type === 'axiom') {
- $this->validate([
- 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
- 'server.settings.logdrain_axiom_dataset_name' => 'required|string',
- 'server.settings.logdrain_axiom_api_key' => 'required|string',
- ]);
- $this->server->settings->update([
- 'is_logdrain_newrelic_enabled' => false,
- 'is_logdrain_highlight_enabled' => false,
- 'is_logdrain_custom_enabled' => false,
- ]);
- } elseif ($type === 'custom') {
- $this->validate([
- 'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
- 'server.settings.logdrain_custom_config' => 'required|string',
- 'server.settings.logdrain_custom_config_parser' => 'nullable',
- ]);
- $this->server->settings->update([
- 'is_logdrain_newrelic_enabled' => false,
- 'is_logdrain_highlight_enabled' => false,
- 'is_logdrain_axiom_enabled' => false,
- ]);
- }
- $this->server->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Settings saved.');
-
- return true;
} catch (\Throwable $e) {
- if ($type === 'newrelic') {
- $this->server->settings->update([
- 'is_logdrain_newrelic_enabled' => false,
- ]);
- } elseif ($type === 'highlight') {
- $this->server->settings->update([
- 'is_logdrain_highlight_enabled' => false,
- ]);
- } elseif ($type === 'axiom') {
- $this->server->settings->update([
- 'is_logdrain_axiom_enabled' => false,
- ]);
- } elseif ($type === 'custom') {
- $this->server->settings->update([
- 'is_logdrain_custom_enabled' => false,
- ]);
- }
- handleError($e, $this);
-
- return false;
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php
index 0aad33b1c..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_V2->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;
@@ -124,7 +122,6 @@ class ByIp extends Component
}
$server->settings->is_build_server = $this->is_build_server;
$server->settings->save();
- $server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {
diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php
index 0ad820428..64aa1884b 100644
--- a/app/Livewire/Server/PrivateKey/Show.php
+++ b/app/Livewire/Server/PrivateKey/Show.php
@@ -8,26 +8,63 @@ use Livewire\Component;
class Show extends Component
{
- public ?Server $server = null;
+ public Server $server;
public $privateKeys = [];
public $parameters = [];
- public function mount()
+ public function mount(string $server_uuid)
{
- $this->parameters = get_route_parameters();
try {
- $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
- if (is_null($this->server)) {
- return redirect()->route('server.index');
- }
+ $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function setPrivateKey($privateKeyId)
+ {
+ $ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId);
+ if (is_null($ownedPrivateKey)) {
+ $this->dispatch('error', 'You are not allowed to use this private key.');
+
+ return;
+ }
+
+ $originalPrivateKeyId = $this->server->getOriginal('private_key_id');
+ try {
+ $this->server->update(['private_key_id' => $privateKeyId]);
+ ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true);
+ if ($uptime) {
+ $this->dispatch('success', 'Private key updated successfully.');
+ } else {
+ throw new \Exception($error);
+ }
+ } catch (\Exception $e) {
+ $this->server->update(['private_key_id' => $originalPrivateKeyId]);
+ $this->server->validateConnection();
+ $this->dispatch('error', $e->getMessage());
+ }
+ }
+
+ public function checkConnection()
+ {
+ try {
+ ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
+ if ($uptime) {
+ $this->dispatch('success', 'Server is reachable.');
+ } else {
+ $this->dispatch('error', 'Server is not reachable. Check this documentation for further help. Error: '.$error);
+
+ return;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function render()
{
return view('livewire.server.private-key.show');
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index 8d1ece1c6..94ea3509a 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -6,7 +6,6 @@ use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
-use Illuminate\Support\Str;
use Livewire\Component;
class Proxy extends Component
@@ -21,6 +20,10 @@ class Proxy extends Component
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
+ protected $rules = [
+ 'server.settings.generate_exact_labels' => 'required|boolean',
+ ];
+
public function mount()
{
$this->selectedProxy = $this->server->proxyType();
@@ -32,24 +35,36 @@ class Proxy extends Component
$this->dispatch('refresh')->self();
}
- public function change_proxy()
+ public function changeProxy()
{
$this->server->proxy = null;
$this->server->save();
+ $this->dispatch('proxyChanged');
}
- public function select_proxy($proxy_type)
+ public function selectProxy($proxy_type)
{
$this->server->proxy->set('status', 'exited');
$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');
}
+ public function instantSave()
+ {
+ try {
+ $this->validate();
+ $this->server->settings->save();
+ $this->dispatch('success', 'Settings saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function submit()
{
try {
@@ -79,12 +94,11 @@ class Proxy extends Component
{
try {
$this->proxy_settings = CheckConfiguration::run($this->server);
- if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) {
+ if (str($this->proxy_settings)->contains('--api.dashboard=true') && str($this->proxy_settings)->contains('--api.insecure=true')) {
$this->dispatch('traefikDashboardAvailable', true);
} else {
$this->dispatch('traefikDashboardAvailable', false);
}
-
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index 6d3f00dc8..8fcff85d6 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -6,6 +6,9 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
+use Carbon\Carbon;
+use Illuminate\Process\InvokedProcess;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -29,6 +32,7 @@ class Deploy extends Component
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
+ 'proxyChanged' => 'proxyStatusUpdated',
];
}
@@ -50,7 +54,7 @@ class Deploy extends Component
public function proxyStarted()
{
CheckProxy::run($this->server, true);
- $this->dispatch('success', 'Proxy started.');
+ $this->dispatch('proxyStatusUpdated');
}
public function proxyStatusUpdated()
@@ -61,7 +65,7 @@ class Deploy extends Component
public function restart()
{
try {
- $this->stop();
+ $this->stop(forceStop: false);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -84,31 +88,53 @@ class Deploy extends Component
try {
$this->server->proxy->force_stop = false;
$this->server->save();
- $activity = StartProxy::run($this->server);
+ $activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function stop()
+ 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 = Carbon::now()->getTimestamp();
+ while ($process->running()) {
+ if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName);
+ break;
+ }
+ usleep(100000);
}
- $this->server->proxy->status = 'exited';
- $this->server->proxy->force_stop = true;
- $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/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php
index c858481db..6277a24bd 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurations.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php
@@ -21,7 +21,6 @@ class DynamicConfigurations extends Component
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
'loadDynamicConfigurations',
- 'refresh' => '$refresh',
];
}
@@ -42,7 +41,7 @@ class DynamicConfigurations extends Component
$contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server);
}
$this->contents = $contents;
- $this->dispatch('refresh');
+ $this->dispatch('$refresh');
}
public function mount()
diff --git a/app/Livewire/Server/Proxy/Modal.php b/app/Livewire/Server/Proxy/Modal.php
deleted file mode 100644
index 5679944d0..000000000
--- a/app/Livewire/Server/Proxy/Modal.php
+++ /dev/null
@@ -1,16 +0,0 @@
-dispatch('proxyStatusUpdated');
- }
-}
diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php
index e5de6eda0..2155f1e82 100644
--- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php
+++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Server\Proxy;
+use App\Enums\ProxyTypes;
use App\Models\Server;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@@ -45,7 +46,7 @@ class NewDynamicConfiguration extends Component
return redirect()->route('server.index');
}
$proxy_type = $this->server->proxyType();
- if ($proxy_type === 'TRAEFIK_V2') {
+ if ($proxy_type === ProxyTypes::TRAEFIK->value) {
if (! str($this->fileName)->endsWith('.yaml') && ! str($this->fileName)->endsWith('.yml')) {
$this->fileName = "{$this->fileName}.yaml";
}
@@ -69,7 +70,7 @@ class NewDynamicConfiguration extends Component
return;
}
}
- if ($proxy_type === 'TRAEFIK_V2') {
+ if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$yaml = Yaml::parse($this->value);
$yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml;
diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php
index cef909a45..5ecb56a69 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()
{
@@ -22,10 +22,7 @@ class Show extends Component
{
$this->parameters = get_route_parameters();
try {
- $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
- if (is_null($this->server)) {
- return redirect()->route('server.index');
- }
+ $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php
index 8dd4dd8e6..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;
@@ -16,7 +16,10 @@ class Status extends Component
public int $numberOfPolls = 0;
- protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling'];
+ protected $listeners = [
+ 'proxyStatusUpdated',
+ 'startProxyPolling',
+ ];
public function startProxyPolling()
{
@@ -41,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/Resources.php b/app/Livewire/Server/Resources.php
index 800344ac3..f549b43cb 100644
--- a/app/Livewire/Server/Resources.php
+++ b/app/Livewire/Server/Resources.php
@@ -15,7 +15,9 @@ class Resources extends Component
public $parameters = [];
- public Collection $unmanagedContainers;
+ public Collection $containers;
+
+ public $activeTab = 'managed';
public function getListeners()
{
@@ -50,14 +52,29 @@ class Resources extends Component
public function refreshStatus()
{
$this->server->refresh();
- $this->loadUnmanagedContainers();
+ if ($this->activeTab === 'managed') {
+ $this->loadManagedContainers();
+ } else {
+ $this->loadUnmanagedContainers();
+ }
$this->dispatch('success', 'Resource statuses refreshed.');
}
+ public function loadManagedContainers()
+ {
+ try {
+ $this->activeTab = 'managed';
+ $this->containers = $this->server->refresh()->definedResources();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function loadUnmanagedContainers()
{
+ $this->activeTab = 'unmanaged';
try {
- $this->unmanagedContainers = $this->server->loadUnmanagedContainers();
+ $this->containers = $this->server->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -65,13 +82,14 @@ class Resources extends Component
public function mount()
{
- $this->unmanagedContainers = collect();
+ $this->containers = collect();
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
+ $this->loadManagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 7ebf90115..bb9188f1c 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -2,36 +2,247 @@
namespace App\Livewire\Server;
+use App\Actions\Server\StartSentinel;
+use App\Actions\Server\StopSentinel;
use App\Models\Server;
-use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Show extends Component
{
- use AuthorizesRequests;
+ public Server $server;
- public ?Server $server = null;
+ #[Validate(['required'])]
+ public string $name;
- public $parameters = [];
+ #[Validate(['nullable'])]
+ public ?string $description = null;
- protected $listeners = ['serverInstalled' => '$refresh'];
+ #[Validate(['required'])]
+ public string $ip;
- public function mount()
+ #[Validate(['required'])]
+ public string $user;
+
+ #[Validate(['required'])]
+ public string $port;
+
+ #[Validate(['nullable'])]
+ public ?string $validationLogs = null;
+
+ #[Validate(['nullable', 'url'])]
+ public ?string $wildcardDomain = null;
+
+ #[Validate(['required'])]
+ public bool $isReachable;
+
+ #[Validate(['required'])]
+ public bool $isUsable;
+
+ #[Validate(['required'])]
+ public bool $isSwarmManager;
+
+ #[Validate(['required'])]
+ public bool $isSwarmWorker;
+
+ #[Validate(['required'])]
+ public bool $isBuildServer;
+
+ #[Validate(['required'])]
+ public bool $isMetricsEnabled;
+
+ #[Validate(['required'])]
+ public string $sentinelToken;
+
+ #[Validate(['nullable'])]
+ public ?string $sentinelUpdatedAt = null;
+
+ #[Validate(['required', 'integer', 'min:1'])]
+ public int $sentinelMetricsRefreshRateSeconds;
+
+ #[Validate(['required', 'integer', 'min:1'])]
+ public int $sentinelMetricsHistoryDays;
+
+ #[Validate(['required', 'integer', 'min:10'])]
+ public int $sentinelPushIntervalSeconds;
+
+ #[Validate(['nullable', 'url'])]
+ public ?string $sentinelCustomUrl = null;
+
+ #[Validate(['required'])]
+ public bool $isSentinelEnabled;
+
+ #[Validate(['required'])]
+ public bool $isSentinelDebugEnabled;
+
+ #[Validate(['required'])]
+ public string $serverTimezone;
+
+ #[Locked]
+ public array $timezones;
+
+ public function getListeners()
+ {
+ $teamId = auth()->user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
+ 'refreshServerShow' => 'refresh',
+ ];
+ }
+
+ public function mount(string $server_uuid)
{
- $this->parameters = get_route_parameters();
try {
- $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
- if (is_null($this->server)) {
- return redirect()->route('server.index');
- }
+ $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
+ $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->server->name = $this->name;
+ $this->server->description = $this->description;
+ $this->server->ip = $this->ip;
+ $this->server->user = $this->user;
+ $this->server->port = $this->port;
+ $this->server->validation_logs = $this->validationLogs;
+ $this->server->save();
+
+ $this->server->settings->is_swarm_manager = $this->isSwarmManager;
+ $this->server->settings->wildcard_domain = $this->wildcardDomain;
+ $this->server->settings->is_swarm_worker = $this->isSwarmWorker;
+ $this->server->settings->is_build_server = $this->isBuildServer;
+ $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled;
+ $this->server->settings->sentinel_token = $this->sentinelToken;
+ $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds;
+ $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
+ $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
+ $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
+ $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
+ $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
+ $this->server->settings->server_timezone = $this->serverTimezone;
+ $this->server->settings->save();
+ } else {
+ $this->name = $this->server->name;
+ $this->description = $this->server->description;
+ $this->ip = $this->server->ip;
+ $this->user = $this->server->user;
+ $this->port = $this->server->port;
+
+ $this->wildcardDomain = $this->server->settings->wildcard_domain;
+ $this->isReachable = $this->server->settings->is_reachable;
+ $this->isUsable = $this->server->settings->is_usable;
+ $this->isSwarmManager = $this->server->settings->is_swarm_manager;
+ $this->isSwarmWorker = $this->server->settings->is_swarm_worker;
+ $this->isBuildServer = $this->server->settings->is_build_server;
+ $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled;
+ $this->sentinelToken = $this->server->settings->sentinel_token;
+ $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds;
+ $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
+ $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
+ $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
+ $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled;
+ $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
+ $this->sentinelUpdatedAt = $this->server->settings->updated_at;
+ $this->serverTimezone = $this->server->settings->server_timezone;
+ }
+ }
+
+ public function refresh()
+ {
+ $this->syncData();
+ $this->dispatch('$refresh');
+ }
+
+ public function validateServer($install = true)
+ {
+ try {
+ $this->validationLogs = $this->server->validation_logs = null;
+ $this->server->save();
+ $this->dispatch('init', $install);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function checkLocalhostConnection()
+ {
+ $this->syncData(true);
+ ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
+ if ($uptime) {
+ $this->dispatch('success', 'Server is reachable.');
+ $this->server->settings->is_reachable = $this->isReachable = true;
+ $this->server->settings->is_usable = $this->isUsable = true;
+ $this->server->settings->save();
+ $this->dispatch('proxyStatusUpdated');
+ } else {
+ $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection. Check this documentation for further help. Error: '.$error);
+
+ return;
+ }
+ }
+
+ public function restartSentinel()
+ {
+ $this->server->restartSentinel();
+ $this->dispatch('success', 'Sentinel restarted.');
+ }
+
+ public function updatedIsSentinelDebugEnabled($value)
+ {
+ $this->submit();
+ $this->restartSentinel();
+ }
+
+ public function updatedIsMetricsEnabled($value)
+ {
+ $this->submit();
+ $this->restartSentinel();
+ }
+
+ public function updatedIsSentinelEnabled($value)
+ {
+ if ($value === true) {
+ StartSentinel::run($this->server, true);
+ } else {
+ $this->isMetricsEnabled = false;
+ $this->isSentinelDebugEnabled = false;
+ StopSentinel::dispatch($this->server);
+ }
+ $this->submit();
+
+ }
+
+ public function regenerateSentinelToken()
+ {
+ try {
+ $this->server->settings->generateSentinelToken();
+ $this->dispatch('success', 'Token regenerated & Sentinel restarted.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave()
+ {
+ $this->submit();
+ }
+
public function submit()
{
- $this->dispatch('serverRefresh', false);
+ try {
+ $this->syncData(true);
+ $this->dispatch('success', 'Server updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php
deleted file mode 100644
index 578a08967..000000000
--- a/app/Livewire/Server/ShowPrivateKey.php
+++ /dev/null
@@ -1,59 +0,0 @@
-server->private_key_id;
- refresh_server_connection($this->server->privateKey);
- $this->server->update([
- 'private_key_id' => $newPrivateKeyId,
- ]);
- $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);
- }
- }
-
- public function checkConnection()
- {
- try {
- ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
- if ($uptime) {
- $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.');
-
- return;
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function mount()
- {
- $this->parameters = get_route_parameters();
- }
-}
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index bd33937e0..8c5bc23ed 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -87,7 +87,10 @@ class ValidateAndInstall extends Component
{
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) {
- $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help. Error: '.$error;
+ $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help. Error: '.$error.'
';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
return;
}
@@ -99,6 +102,9 @@ class ValidateAndInstall extends Component
$this->supported_os_type = $this->server->validateOS();
if (! $this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: documentation .';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
return;
}
@@ -113,6 +119,9 @@ class ValidateAndInstall extends Component
if ($this->install) {
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: documentation .';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
return;
} else {
@@ -126,6 +135,9 @@ class ValidateAndInstall extends Component
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation .';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
return;
}
@@ -143,10 +155,14 @@ class ValidateAndInstall extends Component
} else {
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
+ $this->dispatch('refreshBoardingIndex');
$this->dispatch('success', 'Server validated.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation .';
+ $this->server->update([
+ 'validation_logs' => $this->error,
+ ]);
return;
}
diff --git a/app/Livewire/Settings/Backup.php b/app/Livewire/Settings/Backup.php
deleted file mode 100644
index 08ad04b2d..000000000
--- a/app/Livewire/Settings/Backup.php
+++ /dev/null
@@ -1,97 +0,0 @@
- 'required',
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.postgres_user' => 'required',
- 'database.postgres_password' => 'required',
-
- ];
-
- protected $validationAttributes = [
- 'database.uuid' => 'uuid',
- 'database.name' => 'name',
- 'database.description' => 'description',
- 'database.postgres_user' => 'postgres user',
- 'database.postgres_password' => 'postgres password',
- ];
-
- public function mount()
- {
- $this->backup = $this->database?->scheduledBackups->first() ?? null;
- $this->executions = $this->backup?->executions ?? [];
- }
-
- public function add_coolify_database()
- {
- try {
- $server = Server::findOrFail(0);
- $out = instant_remote_process(['docker inspect coolify-db'], $server);
- $envs = format_docker_envs_to_json($out);
- $postgres_password = $envs['POSTGRES_PASSWORD'];
- $postgres_user = $envs['POSTGRES_USER'];
- $postgres_db = $envs['POSTGRES_DB'];
- $this->database = StandalonePostgresql::create([
- 'id' => 0,
- 'name' => 'coolify-db',
- 'description' => 'Coolify database',
- 'postgres_user' => $postgres_user,
- 'postgres_password' => $postgres_password,
- 'postgres_db' => $postgres_db,
- 'status' => 'running',
- 'destination_type' => 'App\Models\StandaloneDocker',
- 'destination_id' => 0,
- ]);
- $this->backup = ScheduledDatabaseBackup::create([
- 'id' => 0,
- 'enabled' => true,
- 'save_s3' => false,
- 'frequency' => '0 0 * * *',
- 'database_id' => $this->database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
- 'team_id' => currentTeam()->id,
- ]);
- $this->database->refresh();
- $this->backup->refresh();
- $this->s3s = S3Storage::whereTeamId(0)->get();
- } catch (\Exception $e) {
- return handleError($e, $this);
- }
- }
-
- public function backup_now()
- {
- dispatch(new DatabaseBackupJob(
- backup: $this->backup
- ));
- $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
- }
-
- public function submit()
- {
- $this->dispatch('success', 'Backup updated.');
- }
-}
diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php
deleted file mode 100644
index 4dfa16e30..000000000
--- a/app/Livewire/Settings/Configuration.php
+++ /dev/null
@@ -1,105 +0,0 @@
- 'nullable',
- 'settings.resale_license' => 'nullable',
- 'settings.public_port_min' => 'required',
- 'settings.public_port_max' => 'required',
- 'settings.custom_dns_servers' => 'nullable',
- ];
-
- protected $validationAttributes = [
- 'settings.fqdn' => 'FQDN',
- 'settings.resale_license' => 'Resale License',
- 'settings.public_port_min' => 'Public port min',
- 'settings.public_port_max' => 'Public port max',
- 'settings.custom_dns_servers' => 'Custom DNS servers',
- ];
-
- public function mount()
- {
- $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;
- // $this->next_channel = $this->settings->next_channel;
- $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
- }
-
- public function instantSave()
- {
- $this->settings->do_not_track = $this->do_not_track;
- $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
- $this->settings->is_registration_enabled = $this->is_registration_enabled;
- $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
- // if ($this->next_channel) {
- // $this->settings->next_channel = false;
- // $this->next_channel = false;
- // } else {
- // $this->settings->next_channel = $this->next_channel;
- // }
- $this->settings->save();
- $this->dispatch('success', 'Settings updated!');
- }
-
- public function submit()
- {
- try {
- $error_show = false;
- $this->server = Server::findOrFail(0);
- $this->resetErrorBag();
- if ($this->settings->public_port_min > $this->settings->public_port_max) {
- $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
-
- return;
- }
- $this->validate();
-
- if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) {
- if (! validate_dns_entry($this->settings->fqdn, $this->server)) {
- $this->dispatch('error', "Validating DNS failed. Make sure you have added the DNS records correctly. {$this->settings->fqdn}->{$this->server->ip} Check this documentation for further help.");
- $error_show = true;
- }
- }
- if ($this->settings->fqdn) {
- check_domain_usage(domain: $this->settings->fqdn);
- }
- $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
- $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
- return str($dns)->trim()->lower();
- });
- $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
- $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
-
- $this->settings->save();
- $this->server->setupDynamicProxyConfiguration();
- if (! $error_show) {
- $this->dispatch('success', 'Instance settings updated successfully!');
- }
- } catch (\Exception $e) {
- return handleError($e, $this);
- }
- }
-}
diff --git a/app/Livewire/Settings/Email.php b/app/Livewire/Settings/Email.php
deleted file mode 100644
index bd7f8201e..000000000
--- a/app/Livewire/Settings/Email.php
+++ /dev/null
@@ -1,127 +0,0 @@
- 'nullable|boolean',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_enabled' => 'nullable|boolean',
- 'settings.resend_api_key' => 'nullable',
-
- ];
-
- protected $validationAttributes = [
- 'settings.smtp_from_address' => 'From Address',
- 'settings.smtp_from_name' => 'From Name',
- 'settings.smtp_recipients' => 'Recipients',
- 'settings.smtp_host' => 'Host',
- 'settings.smtp_port' => 'Port',
- 'settings.smtp_encryption' => 'Encryption',
- 'settings.smtp_username' => 'Username',
- 'settings.smtp_password' => 'Password',
- 'settings.smtp_timeout' => 'Timeout',
- 'settings.resend_api_key' => 'Resend API Key',
- ];
-
- public function mount()
- {
- $this->emails = auth()->user()->email;
- }
-
- public function submitFromFields()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- ]);
- $this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function submitResend()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.resend_api_key' => 'required',
- ]);
- $this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->settings->resend_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function instantSaveResend()
- {
- try {
- $this->settings->smtp_enabled = false;
- $this->submitResend();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function instantSave()
- {
- try {
- $this->settings->resend_enabled = false;
- $this->submit();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function submit()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'settings.smtp_from_address' => 'required|email',
- 'settings.smtp_from_name' => 'required',
- 'settings.smtp_host' => 'required',
- 'settings.smtp_port' => 'required|numeric',
- 'settings.smtp_encryption' => 'nullable',
- 'settings.smtp_username' => 'nullable',
- 'settings.smtp_password' => 'nullable',
- 'settings.smtp_timeout' => 'nullable',
- ]);
- $this->settings->save();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function sendTestNotification()
- {
- $this->settings?->notify(new Test($this->emails));
- $this->dispatch('success', 'Test email sent.');
- }
-}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index f6f918933..2991b8ae8 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -2,41 +2,225 @@
namespace App\Livewire\Settings;
+use App\Jobs\CheckForUpdatesJob;
use App\Models\InstanceSettings;
-use App\Models\S3Storage;
-use App\Models\StandalonePostgresql;
+use App\Models\Server;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
- public StandalonePostgresql $database;
+ protected Server $server;
- public $s3s;
+ #[Locked]
+ public $timezones;
- public function mount()
- {
- if (isInstanceAdmin()) {
- $settings = InstanceSettings::get();
- $database = StandalonePostgresql::whereName('coolify-db')->first();
- $s3s = S3Storage::whereTeamId(0)->get() ?? [];
- if ($database) {
- if ($database->status !== 'running') {
- $database->status = 'running';
- $database->save();
- }
- $this->database = $database;
- }
- $this->settings = $settings;
- $this->s3s = $s3s;
- } else {
- return redirect()->route('dashboard');
- }
- }
+ #[Validate('boolean')]
+ public bool $is_auto_update_enabled;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $fqdn = null;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $resale_license = null;
+
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_min;
+
+ #[Validate('required|integer|min:1025|max:65535')]
+ public int $public_port_max;
+
+ #[Validate('nullable|string')]
+ public ?string $custom_dns_servers = null;
+
+ #[Validate('nullable|string|max:255')]
+ public ?string $instance_name = null;
+
+ #[Validate('nullable|string')]
+ public ?string $allowed_ips = null;
+
+ #[Validate('nullable|string')]
+ public ?string $public_ipv4 = null;
+
+ #[Validate('nullable|string')]
+ public ?string $public_ipv6 = null;
+
+ #[Validate('string')]
+ public string $auto_update_frequency;
+
+ #[Validate('string')]
+ public string $update_check_frequency;
+
+ #[Validate('required|string|timezone')]
+ public string $instance_timezone;
+
+ #[Validate('boolean')]
+ public bool $do_not_track;
+
+ #[Validate('boolean')]
+ public bool $is_registration_enabled;
+
+ #[Validate('boolean')]
+ public bool $is_dns_validation_enabled;
+
+ #[Validate('boolean')]
+ public bool $is_api_enabled;
+
+ #[Validate('boolean')]
+ public bool $disable_two_step_confirmation;
public function render()
{
return view('livewire.settings.index');
}
+
+ public function mount()
+ {
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ } else {
+ $this->settings = instanceSettings();
+ $this->fqdn = $this->settings->fqdn;
+ $this->resale_license = $this->settings->resale_license;
+ $this->public_port_min = $this->settings->public_port_min;
+ $this->public_port_max = $this->settings->public_port_max;
+ $this->custom_dns_servers = $this->settings->custom_dns_servers;
+ $this->instance_name = $this->settings->instance_name;
+ $this->allowed_ips = $this->settings->allowed_ips;
+ $this->public_ipv4 = $this->settings->public_ipv4;
+ $this->public_ipv6 = $this->settings->public_ipv6;
+ $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;
+ $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
+ $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();
+ $this->instance_timezone = $this->settings->instance_timezone;
+ $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
+ }
+ }
+
+ public function instantSave($isSave = true)
+ {
+ $this->settings->fqdn = $this->fqdn;
+ $this->settings->resale_license = $this->resale_license;
+ $this->settings->public_port_min = $this->public_port_min;
+ $this->settings->public_port_max = $this->public_port_max;
+ $this->settings->custom_dns_servers = $this->custom_dns_servers;
+ $this->settings->instance_name = $this->instance_name;
+ $this->settings->allowed_ips = $this->allowed_ips;
+ $this->settings->public_ipv4 = $this->public_ipv4;
+ $this->settings->public_ipv6 = $this->public_ipv6;
+ $this->settings->do_not_track = $this->do_not_track;
+ $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
+ $this->settings->is_registration_enabled = $this->is_registration_enabled;
+ $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
+ $this->settings->is_api_enabled = $this->is_api_enabled;
+ $this->settings->auto_update_frequency = $this->auto_update_frequency;
+ $this->settings->update_check_frequency = $this->update_check_frequency;
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
+ $this->settings->instance_timezone = $this->instance_timezone;
+ if ($isSave) {
+ $this->settings->save();
+ $this->dispatch('success', 'Settings updated!');
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $error_show = false;
+ $this->server = Server::findOrFail(0);
+ $this->resetErrorBag();
+ if ($this->settings->public_port_min > $this->settings->public_port_max) {
+ $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
+
+ return;
+ }
+ $this->validate();
+
+ if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) {
+ $this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.');
+ if (empty($this->auto_update_frequency)) {
+ $this->auto_update_frequency = '0 0 * * *';
+ }
+
+ return;
+ }
+
+ if (! validate_cron_expression($this->update_check_frequency)) {
+ $this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.');
+ if (empty($this->update_check_frequency)) {
+ $this->update_check_frequency = '0 * * * *';
+ }
+
+ return;
+ }
+
+ if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) {
+ if (! validate_dns_entry($this->settings->fqdn, $this->server)) {
+ $this->dispatch('error', "Validating DNS failed. Make sure you have added the DNS records correctly. {$this->settings->fqdn}->{$this->server->ip} Check this documentation for further help.");
+ $error_show = true;
+ }
+ }
+ if ($this->settings->fqdn) {
+ check_domain_usage(domain: $this->settings->fqdn);
+ }
+ $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
+ $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
+ return str($dns)->trim()->lower();
+ });
+ $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
+ $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
+
+ $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim();
+ $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) {
+ return str($ip)->trim();
+ });
+ $this->settings->allowed_ips = $this->settings->allowed_ips->unique();
+ $this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
+
+ $this->instantSave(isSave: false);
+
+ $this->settings->save();
+ $this->server->setupDynamicProxyConfiguration();
+ if (! $error_show) {
+ $this->dispatch('success', 'Instance settings updated successfully!');
+ }
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function checkManually()
+ {
+ CheckForUpdatesJob::dispatchSync();
+ $this->dispatch('updateAvailable');
+ $settings = instanceSettings();
+ if ($settings->new_version_available) {
+ $this->dispatch('success', 'New version available!');
+ } else {
+ $this->dispatch('success', 'No new version available.');
+ }
+ }
+
+ public function toggleTwoStepConfirmation($password)
+ {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
+ $this->settings->save();
+ $this->dispatch('success', 'Two step confirmation has been disabled.');
+ }
}
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index 212bc95be..79f8269f3 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -28,8 +28,11 @@ class License extends Component
if (! isCloud()) {
abort(404);
}
+ if (! isInstanceAdmin()) {
+ return redirect()->route('home');
+ }
$this->instance_id = config('app.id');
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
}
public function render()
@@ -47,7 +50,6 @@ class License extends Component
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. Error: '.$e->getMessage());
- ray($e->getMessage());
return redirect()->route('settings.license');
}
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
new file mode 100644
index 000000000..1b0599ffe
--- /dev/null
+++ b/app/Livewire/SettingsBackup.php
@@ -0,0 +1,125 @@
+route('dashboard');
+ } else {
+ $settings = instanceSettings();
+ $this->database = StandalonePostgresql::whereName('coolify-db')->first();
+ $s3s = S3Storage::whereTeamId(0)->get() ?? [];
+ if ($this->database) {
+ $this->uuid = $this->database->uuid;
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->postgres_user = $this->database->postgres_user;
+ $this->postgres_password = $this->database->postgres_password;
+
+ if ($this->database->status !== 'running') {
+ $this->database->status = 'running';
+ $this->database->save();
+ }
+ $this->backup = $this->database->scheduledBackups->first();
+ $this->executions = $this->backup->executions;
+ }
+ $this->settings = $settings;
+ $this->s3s = $s3s;
+ }
+ }
+
+ public function addCoolifyDatabase()
+ {
+ try {
+ $server = Server::findOrFail(0);
+ $out = instant_remote_process(['docker inspect coolify-db'], $server);
+ $envs = format_docker_envs_to_json($out);
+ $postgres_password = $envs['POSTGRES_PASSWORD'];
+ $postgres_user = $envs['POSTGRES_USER'];
+ $postgres_db = $envs['POSTGRES_DB'];
+ $this->database = StandalonePostgresql::create([
+ 'id' => 0,
+ 'name' => 'coolify-db',
+ 'description' => 'Coolify database',
+ 'postgres_user' => $postgres_user,
+ 'postgres_password' => $postgres_password,
+ 'postgres_db' => $postgres_db,
+ 'status' => 'running',
+ 'destination_type' => \App\Models\StandaloneDocker::class,
+ 'destination_id' => 0,
+ ]);
+ $this->backup = ScheduledDatabaseBackup::create([
+ 'id' => 0,
+ 'enabled' => true,
+ 'save_s3' => false,
+ 'frequency' => '0 0 * * *',
+ 'database_id' => $this->database->id,
+ 'database_type' => \App\Models\StandalonePostgresql::class,
+ 'team_id' => currentTeam()->id,
+ ]);
+ $this->database->refresh();
+ $this->backup->refresh();
+ $this->s3s = S3Storage::whereTeamId(0)->get();
+
+ $this->uuid = $this->database->uuid;
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->postgres_user = $this->database->postgres_user;
+ $this->postgres_password = $this->database->postgres_password;
+ $this->executions = $this->backup->executions;
+
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function submit()
+ {
+ $this->database->update([
+ 'name' => $this->name,
+ 'description' => $this->description,
+ 'postgres_user' => $this->postgres_user,
+ 'postgres_password' => $this->postgres_password,
+ ]);
+ $this->dispatch('success', 'Backup updated.');
+ }
+}
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
new file mode 100644
index 000000000..61f720b3a
--- /dev/null
+++ b/app/Livewire/SettingsEmail.php
@@ -0,0 +1,117 @@
+route('dashboard');
+ }
+ $this->settings = instanceSettings();
+ $this->syncData();
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->settings->smtp_enabled = $this->smtpEnabled;
+ $this->settings->smtp_host = $this->smtpHost;
+ $this->settings->smtp_port = $this->smtpPort;
+ $this->settings->smtp_encryption = $this->smtpEncryption;
+ $this->settings->smtp_username = $this->smtpUsername;
+ $this->settings->smtp_password = $this->smtpPassword;
+ $this->settings->smtp_timeout = $this->smtpTimeout;
+ $this->settings->smtp_from_address = $this->smtpFromAddress;
+ $this->settings->smtp_from_name = $this->smtpFromName;
+
+ $this->settings->resend_enabled = $this->resendEnabled;
+ $this->settings->resend_api_key = $this->resendApiKey;
+ $this->settings->save();
+ } else {
+ $this->smtpEnabled = $this->settings->smtp_enabled;
+ $this->smtpHost = $this->settings->smtp_host;
+ $this->smtpPort = $this->settings->smtp_port;
+ $this->smtpEncryption = $this->settings->smtp_encryption;
+ $this->smtpUsername = $this->settings->smtp_username;
+ $this->smtpPassword = $this->settings->smtp_password;
+ $this->smtpTimeout = $this->settings->smtp_timeout;
+ $this->smtpFromAddress = $this->settings->smtp_from_address;
+ $this->smtpFromName = $this->settings->smtp_from_name;
+
+ $this->resendEnabled = $this->settings->resend_enabled;
+ $this->resendApiKey = $this->settings->resend_api_key;
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->dispatch('success', 'Settings saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave(string $type)
+ {
+ try {
+ if ($type === 'SMTP') {
+ $this->resendEnabled = false;
+ } else {
+ $this->smtpEnabled = false;
+ }
+ $this->syncData(true);
+ if ($this->smtpEnabled || $this->resendEnabled) {
+ $this->dispatch('success', "{$type} enabled.");
+ } else {
+ $this->dispatch('success', "{$type} disabled.");
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+}
diff --git a/app/Livewire/Settings/Auth.php b/app/Livewire/SettingsOauth.php
similarity index 89%
rename from app/Livewire/Settings/Auth.php
rename to app/Livewire/SettingsOauth.php
index 783b163e0..17b3b89a3 100644
--- a/app/Livewire/Settings/Auth.php
+++ b/app/Livewire/SettingsOauth.php
@@ -1,11 +1,11 @@
route('home');
+ }
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
$carry[$setting->provider] = $setting;
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 ee28f8847..07cef54f9 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -4,8 +4,6 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
-use App\Models\InstanceSettings;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Change extends Component
@@ -94,51 +92,53 @@ class Change extends Component
// }
public function mount()
{
- $github_app_uuid = request()->github_app_uuid;
- $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
- if (! $this->github_app) {
- return redirect()->route('source.all');
- }
- $this->applications = $this->github_app->applications;
- $settings = InstanceSettings::get();
- $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
+ try {
+ $github_app_uuid = request()->github_app_uuid;
+ $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
- $this->name = str($this->github_app->name)->kebab();
- $this->fqdn = $settings->fqdn;
+ $this->applications = $this->github_app->applications;
+ $settings = instanceSettings();
+ $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
- if ($settings->public_ipv4) {
- $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
- }
- if ($settings->public_ipv6) {
- $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
- }
- if ($this->github_app->installation_id && session('from')) {
- $source_id = data_get(session('from'), 'source_id');
- if (! $source_id || $this->github_app->id !== $source_id) {
- session()->forget('from');
- } else {
- $parameters = data_get(session('from'), 'parameters');
- $back = data_get(session('from'), 'back');
- $environment_name = data_get($parameters, 'environment_name');
- $project_uuid = data_get($parameters, 'project_uuid');
- $type = data_get($parameters, 'type');
- $destination = data_get($parameters, 'destination');
- session()->forget('from');
+ $this->name = str($this->github_app->name)->kebab();
+ $this->fqdn = $settings->fqdn;
- return redirect()->route($back, [
- 'environment_name' => $environment_name,
- 'project_uuid' => $project_uuid,
- 'type' => $type,
- 'destination' => $destination,
- ]);
+ if ($settings->public_ipv4) {
+ $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
}
- }
- $this->parameters = get_route_parameters();
- if (isCloud() && ! isDev()) {
- $this->webhook_endpoint = config('app.url');
- } else {
- $this->webhook_endpoint = $this->ipv4;
- $this->is_system_wide = $this->github_app->is_system_wide;
+ if ($settings->public_ipv6) {
+ $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
+ }
+ if ($this->github_app->installation_id && session('from')) {
+ $source_id = data_get(session('from'), 'source_id');
+ if (! $source_id || $this->github_app->id !== $source_id) {
+ session()->forget('from');
+ } else {
+ $parameters = data_get(session('from'), 'parameters');
+ $back = data_get(session('from'), 'back');
+ $environment_name = data_get($parameters, 'environment_name');
+ $project_uuid = data_get($parameters, 'project_uuid');
+ $type = data_get($parameters, 'type');
+ $destination = data_get($parameters, 'destination');
+ session()->forget('from');
+
+ return redirect()->route($back, [
+ 'environment_name' => $environment_name,
+ 'project_uuid' => $project_uuid,
+ 'type' => $type,
+ 'destination' => $destination,
+ ]);
+ }
+ }
+ $this->parameters = get_route_parameters();
+ if (isCloud() && ! isDev()) {
+ $this->webhook_endpoint = config('app.url');
+ } else {
+ $this->webhook_endpoint = $this->ipv4;
+ $this->is_system_wide = $this->github_app->is_system_wide;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e..136d3525e 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
- $this->name = generate_random_name();
+ $this->name = substr(generate_random_name(), 0, 30);
}
public function createGitHubApp()
diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php
index 1ccc3997c..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('.');
}
}
@@ -59,7 +61,7 @@ class Create extends Component
{
try {
$this->validate();
- $this->storage = new S3Storage();
+ $this->storage = new S3Storage;
$this->storage->name = $this->name;
$this->storage->description = $this->description ?? null;
$this->storage->region = $this->region;
diff --git a/app/Livewire/Subscription/Actions.php b/app/Livewire/Subscription/Actions.php
index db1f565a6..1388d3244 100644
--- a/app/Livewire/Subscription/Actions.php
+++ b/app/Livewire/Subscription/Actions.php
@@ -3,7 +3,6 @@
namespace App\Livewire\Subscription;
use App\Models\Team;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Actions extends Component
@@ -15,70 +14,6 @@ class Actions extends Component
$this->server_limits = Team::serverLimit();
}
- public function cancel()
- {
- try {
- $subscription_id = currentTeam()->subscription->lemon_subscription_id;
- if (! $subscription_id) {
- throw new \Exception('No subscription found');
- }
- $response = Http::withHeaders([
- 'Accept' => 'application/vnd.api+json',
- 'Content-Type' => 'application/vnd.api+json',
- 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'),
- ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id);
- $json = $response->json();
- if ($response->failed()) {
- $error = data_get($json, 'errors.0.status');
- if ($error === '404') {
- throw new \Exception('Subscription not found.');
- }
- throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
- } else {
- $this->dispatch('success', 'Subscription cancelled successfully. Reloading in 5s.');
- $this->dispatch('reloadWindow', 5000);
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
- public function resume()
- {
- try {
- $subscription_id = currentTeam()->subscription->lemon_subscription_id;
- if (! $subscription_id) {
- throw new \Exception('No subscription found');
- }
- $response = Http::withHeaders([
- 'Accept' => 'application/vnd.api+json',
- 'Content-Type' => 'application/vnd.api+json',
- 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'),
- ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id, [
- 'data' => [
- 'type' => 'subscriptions',
- 'id' => $subscription_id,
- 'attributes' => [
- 'cancelled' => false,
- ],
- ],
- ]);
- $json = $response->json();
- if ($response->failed()) {
- $error = data_get($json, 'errors.0.status');
- if ($error === '404') {
- throw new \Exception('Subscription not found.');
- }
- throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
- } else {
- $this->dispatch('success', 'Subscription resumed successfully. Reloading in 5s.');
- $this->dispatch('reloadWindow', 5000);
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
public function stripeCustomerPortal()
{
$session = getStripeCustomerPortalSession(currentTeam());
diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php
index c072352fe..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 = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php
index 9bc11d862..6b2d3fb36 100644
--- a/app/Livewire/Subscription/PricingPlans.php
+++ b/app/Livewire/Subscription/PricingPlans.php
@@ -2,55 +2,23 @@
namespace App\Livewire\Subscription;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Stripe\Checkout\Session;
use Stripe\Stripe;
class PricingPlans extends Component
{
- public bool $isTrial = false;
-
- public function mount()
- {
- $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended');
- if (config('constants.limits.trial_period') == 0) {
- $this->isTrial = false;
- }
- }
-
public function subscribeStripe($type)
{
- $team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
- switch ($type) {
- case 'basic-monthly':
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- case 'basic-yearly':
- $priceId = config('subscription.stripe_price_id_basic_yearly');
- break;
- case 'pro-monthly':
- $priceId = config('subscription.stripe_price_id_pro_monthly');
- break;
- case 'pro-yearly':
- $priceId = config('subscription.stripe_price_id_pro_yearly');
- break;
- case 'ultimate-monthly':
- $priceId = config('subscription.stripe_price_id_ultimate_monthly');
- break;
- case 'ultimate-yearly':
- $priceId = config('subscription.stripe_price_id_ultimate_yearly');
- break;
- case 'dynamic-monthly':
- $priceId = config('subscription.stripe_price_id_dynamic_monthly');
- break;
- case 'dynamic-yearly':
- $priceId = config('subscription.stripe_price_id_dynamic_yearly');
- break;
- default:
- $priceId = config('subscription.stripe_price_id_basic_monthly');
- break;
- }
+
+ $priceId = match ($type) {
+ 'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'),
+ 'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'),
+ default => config('subscription.stripe_price_id_dynamic_monthly'),
+ };
+
if (! $priceId) {
$this->dispatch('error', 'Price ID not found! Please contact the administrator.');
@@ -59,10 +27,14 @@ class PricingPlans extends Component
$payload = [
'allow_promotion_codes' => true,
'billing_address_collection' => 'required',
- 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id,
+ 'client_reference_id' => Auth::id().':'.currentTeam()->id,
'line_items' => [[
'price' => $priceId,
- 'quantity' => 1,
+ 'adjustable_quantity' => [
+ 'enabled' => true,
+ 'minimum' => 2,
+ ],
+ 'quantity' => 2,
]],
'tax_id_collection' => [
'enabled' => true,
@@ -70,39 +42,18 @@ class PricingPlans extends Component
'automatic_tax' => [
'enabled' => true,
],
-
+ 'subscription_data' => [
+ 'metadata' => [
+ 'user_id' => Auth::id(),
+ 'team_id' => currentTeam()->id,
+ ],
+ ],
+ 'payment_method_collection' => 'if_required',
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
- if (str($type)->contains('ultimate')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 10,
- ];
- $payload['line_items'][0]['quantity'] = 10;
- }
- if (str($type)->contains('dynamic')) {
- $payload['line_items'][0]['adjustable_quantity'] = [
- 'enabled' => true,
- 'minimum' => 2,
- ];
- $payload['line_items'][0]['quantity'] = 2;
- }
- if (! data_get($team, 'subscription.stripe_trial_already_ended')) {
- if (config('constants.limits.trial_period') > 0) {
- $payload['subscription_data'] = [
- 'trial_period_days' => config('constants.limits.trial_period'),
- 'trial_settings' => [
- 'end_behavior' => [
- 'missing_payment_method' => 'cancel',
- ],
- ],
- ];
- }
- $payload['payment_method_collection'] = 'if_required';
- }
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;
@@ -110,7 +61,7 @@ class PricingPlans extends Component
'name' => 'auto',
];
} else {
- $payload['customer_email'] = auth()->user()->email;
+ $payload['customer_email'] = Auth::user()->email;
}
$session = Session::create($payload);
diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php
index 270aa176a..e4afa5b60 100644
--- a/app/Livewire/Tags/Deployments.php
+++ b/app/Livewire/Tags/Deployments.php
@@ -7,19 +7,19 @@ use Livewire\Component;
class Deployments extends Component
{
- public $deployments_per_tag_per_server = [];
+ public $deploymentsPerTagPerServer = [];
- public $resource_ids = [];
+ public $resourceIds = [];
public function render()
{
return view('livewire.tags.deployments');
}
- public function get_deployments()
+ public function getDeployments()
{
try {
- $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([
+ $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([
'id',
'application_id',
'application_name',
@@ -29,7 +29,7 @@ class Deployments extends Component
'server_id',
'status',
])->sortBy('id')->groupBy('server_name')->toArray();
- $this->dispatch('deployments', $this->deployments_per_tag_per_server);
+ $this->dispatch('deployments', $this->deploymentsPerTagPerServer);
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php
deleted file mode 100644
index 91e15835f..000000000
--- a/app/Livewire/Tags/Index.php
+++ /dev/null
@@ -1,79 +0,0 @@
- 'update_deployments'];
-
- public function update_deployments($deployments)
- {
- $this->deployments_per_tag_per_server = $deployments;
- }
-
- public function tag_updated()
- {
- if ($this->tag == '') {
- return;
- }
- $tag = $this->tags->where('name', $this->tag)->first();
- if (! $tag) {
- $this->dispatch('error', "Tag ({$this->tag}) not found.");
- $this->tag = '';
-
- return;
- }
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- }
-
- public function redeploy_all()
- {
- try {
- $this->applications->each(function ($resource) {
- $deploy = new Deploy();
- $deploy->deploy_resource($resource);
- });
- $this->services->each(function ($resource) {
- $deploy = new Deploy();
- $deploy->deploy_resource($resource);
- });
- $this->dispatch('success', 'Mass deployment started.');
- } catch (\Exception $e) {
- return handleError($e, $this);
- }
- }
-
- public function mount()
- {
- $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
- if ($this->tag) {
- $this->tag_updated();
- }
- }
-
- public function render()
- {
- return view('livewire.tags.index');
- }
-}
diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php
index f4ecc67a0..fc5b13374 100644
--- a/app/Livewire/Tags/Show.php
+++ b/app/Livewire/Tags/Show.php
@@ -2,44 +2,60 @@
namespace App\Livewire\Tags;
-use App\Http\Controllers\Api\Deploy;
+use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
+use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Title;
use Livewire\Component;
+#[Title('Tags | Coolify')]
class Show extends Component
{
- public $tags;
+ #[Locked]
+ public ?string $tagName = null;
- public Tag $tag;
+ #[Locked]
+ public ?Collection $tags = null;
- public $applications;
+ #[Locked]
+ public ?Tag $tag = null;
- public $services;
+ #[Locked]
+ public ?Collection $applications = null;
- public $webhook = null;
+ #[Locked]
+ public ?Collection $services = null;
- public $deployments_per_tag_per_server = [];
+ #[Locked]
+ public ?string $webhook = null;
+
+ #[Locked]
+ public ?array $deploymentsPerTagPerServer = null;
public function mount()
{
- $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
- $tag = $this->tags->where('name', request()->tag_name)->first();
- if (! $tag) {
- return redirect()->route('tags.index');
+ try {
+ $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
+ if (str($this->tagName)->isNotEmpty()) {
+ $tag = $this->tags->where('name', $this->tagName)->first();
+ $this->webhook = generateTagDeployWebhook($tag->name);
+ $this->applications = $tag->applications()->get();
+ $this->services = $tag->services()->get();
+ $this->tag = $tag;
+ $this->getDeployments();
+ }
+ } catch (\Exception $e) {
+ return handleError($e, $this);
}
- $this->webhook = generatTagDeployWebhook($tag->name);
- $this->applications = $tag->applications()->get();
- $this->services = $tag->services()->get();
- $this->tag = $tag;
- $this->get_deployments();
}
- public function get_deployments()
+ public function getDeployments()
{
try {
$resource_ids = $this->applications->pluck('id');
- $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
+ $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([
'id',
'application_id',
'application_name',
@@ -54,16 +70,16 @@ class Show extends Component
}
}
- public function redeploy_all()
+ public function redeployAll()
{
try {
$message = collect([]);
$this->applications->each(function ($resource) use ($message) {
- $deploy = new Deploy();
+ $deploy = new DeployController;
$message->push($deploy->deploy_resource($resource));
});
$this->services->each(function ($resource) use ($message) {
- $deploy = new Deploy();
+ $deploy = new DeployController;
$message->push($deploy->deploy_resource($resource));
});
$this->dispatch('success', 'Mass deployment started.');
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 97d4fcdbf..cfb47d9d8 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -2,8 +2,11 @@
namespace App\Livewire\Team;
+use App\Models\InstanceSettings;
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
@@ -56,54 +59,54 @@ class AdminView extends Component
foreach ($servers as $server) {
$resources = $server->definedResources();
foreach ($resources as $resource) {
- ray('Deleting resource: '.$resource->name);
$resource->forceDelete();
}
- ray('Deleting server: '.$server->name);
$server->forceDelete();
}
$projects = $team->projects;
foreach ($projects as $project) {
- ray('Deleting project: '.$project->name);
$project->forceDelete();
}
$team->members()->detach($user->id);
- ray('Deleting team: '.$team->name);
$team->delete();
}
- public function delete($id)
+ public function delete($id, $password)
{
+ if (! isInstanceAdmin()) {
+ return redirect()->route('dashboard');
+ }
+ if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
+ 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');
}
$user = User::find($id);
$teams = $user->teams;
foreach ($teams as $team) {
- ray($team->name);
$user_alone_in_team = $team->members->count() === 1;
if ($team->id === 0) {
if ($user_alone_in_team) {
- ray('user is alone in the root team, do nothing');
-
return $this->dispatch('error', 'User is alone in the root team, cannot delete');
}
}
if ($user_alone_in_team) {
- ray('user is alone in the team');
$this->finalizeDeletion($user, $team);
continue;
}
- ray('user is not alone in the team');
if ($user->isOwner()) {
$found_other_owner_or_admin = $team->members->filter(function ($member) {
return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
})->where('id', '!=', $user->id)->first();
if ($found_other_owner_or_admin) {
- ray('found other owner or admin');
$team->members()->detach($user->id);
continue;
@@ -112,24 +115,19 @@ class AdminView extends Component
return $member->pivot->role === 'member';
})->first();
if ($found_other_member_who_is_not_owner) {
- ray('found other member who is not owner');
$found_other_member_who_is_not_owner->pivot->role = 'owner';
$found_other_member_who_is_not_owner->pivot->save();
$team->members()->detach($user->id);
} else {
- // This should never happen as if the user is the only member in the team, the team should be deleted already.
- ray('found no other member who is not owner');
$this->finalizeDeletion($user, $team);
}
continue;
}
} else {
- ray('user is not owner');
$team->members()->detach($user->id);
}
}
- ray('Deleting user: '.$user->name);
$user->delete();
$this->getUsers();
}
diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php
index 992833da5..f805d6122 100644
--- a/app/Livewire/Team/Create.php
+++ b/app/Livewire/Team/Create.php
@@ -3,28 +3,21 @@
namespace App\Livewire\Team;
use App\Models\Team;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Create extends Component
{
+ #[Validate(['required', 'min:3', 'max:255'])]
public string $name = '';
+ #[Validate(['nullable', 'min:3', 'max:255'])]
public ?string $description = null;
- protected $rules = [
- 'name' => 'required|min:3|max:255',
- 'description' => 'nullable|min:3|max:255',
- ];
-
- protected $validationAttributes = [
- 'name' => 'name',
- 'description' => 'description',
- ];
-
public function submit()
{
- $this->validate();
try {
+ $this->validate();
$team = Team::create([
'name' => $this->name,
'description' => $this->description,
diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php
index 45600dbfe..0972e7364 100644
--- a/app/Livewire/Team/Index.php
+++ b/app/Livewire/Team/Index.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\TeamInvitation;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
@@ -55,7 +56,7 @@ class Index extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === auth()->user()->id) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php
index 6a32a1d16..93432efc8 100644
--- a/app/Livewire/Team/Invitations.php
+++ b/app/Livewire/Team/Invitations.php
@@ -13,17 +13,18 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id)
{
- $initiation_found = TeamInvitation::find($invitation_id);
- if (! $initiation_found) {
+ try {
+ $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
+ $initiation_found->delete();
+ $this->refreshInvitations();
+ $this->dispatch('success', 'Invitation revoked.');
+ } catch (\Exception) {
return $this->dispatch('error', 'Invitation not found.');
}
- $initiation_found->delete();
- $this->refreshInvitations();
- $this->dispatch('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
- $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
+ $this->invitations = TeamInvitation::ownedByCurrentTeam()->get();
}
}
diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php
index cc69e6650..25f8a1ff5 100644
--- a/app/Livewire/Team/InviteLink.php
+++ b/app/Livewire/Team/InviteLink.php
@@ -41,6 +41,9 @@ class InviteLink extends Component
{
try {
$this->validate();
+ if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
+ throw new \Exception('Admins cannot invite owners.');
+ }
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
@@ -52,7 +55,7 @@ class InviteLink extends Component
if (is_null($user)) {
$password = Str::password();
$user = User::create([
- 'name' => Str::of($this->email)->before('@'),
+ 'name' => str($this->email)->before('@'),
'email' => $this->email,
'password' => Hash::make($password),
'force_password_reset' => true,
@@ -79,7 +82,7 @@ class InviteLink extends Component
'via' => $sendEmail ? 'email' : 'link',
]);
if ($sendEmail) {
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->view('emails.invitation-link', [
'team' => currentTeam()->name,
'invitation_link' => $link,
diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php
index 680cb901b..890d640a0 100644
--- a/app/Livewire/Team/Member.php
+++ b/app/Livewire/Team/Member.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
+use App\Enums\Role;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
@@ -12,29 +13,66 @@ class Member extends Component
public function makeAdmin()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeOwner()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::OWNER)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function makeReadonly()
{
- $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]);
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
}
public function remove()
{
- $this->member->teams()->detach(currentTeam());
- Cache::forget("team:{$this->member->id}");
- Cache::remember('team:'.$this->member->id, 3600, function () {
- return $this->member->teams()->first();
- });
- $this->dispatch('reloadWindow');
+ try {
+ if (Role::from(auth()->user()->role())->lt(Role::ADMIN)
+ || Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
+ throw new \Exception('You are not authorized to perform this action.');
+ }
+ $this->member->teams()->detach(currentTeam());
+ Cache::forget("team:{$this->member->id}");
+ Cache::remember('team:'.$this->member->id, 3600, function () {
+ return $this->member->teams()->first();
+ });
+ $this->dispatch('reloadWindow');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ }
+ }
+
+ private function getMemberRole()
+ {
+ return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role;
}
}
diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php
new file mode 100644
index 000000000..a24a237c5
--- /dev/null
+++ b/app/Livewire/Terminal/Index.php
@@ -0,0 +1,88 @@
+user()->isAdmin()) {
+ abort(403);
+ }
+ $this->servers = Server::isReachable()->get();
+ }
+
+ public function loadContainers()
+ {
+ try {
+ $this->containers = $this->getAllActiveContainers();
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ } finally {
+ $this->isLoadingContainers = false;
+ }
+ }
+
+ 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 7ad7e9523..e50085c64 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -3,6 +3,7 @@
namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
+use App\Models\InstanceSettings;
use Livewire\Component;
class Upgrade extends Component
@@ -15,13 +16,18 @@ class Upgrade extends Component
public string $latestVersion = '';
+ protected $listeners = ['updateAvailable' => 'checkUpdate'];
+
public function checkUpdate()
{
- $this->latestVersion = get_latest_version_of_coolify();
- $currentVersion = config('version');
- version_compare($currentVersion, $this->latestVersion, '<') ? $this->isUpgradeAvailable = true : $this->isUpgradeAvailable = false;
- if (isDev()) {
- $this->isUpgradeAvailable = true;
+ try {
+ $this->latestVersion = get_latest_version_of_coolify();
+ $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
+ if (isDev()) {
+ $this->isUpgradeAvailable = true;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php
index d1f79c835..fab3265b6 100644
--- a/app/Livewire/VerifyEmail.php
+++ b/app/Livewire/VerifyEmail.php
@@ -15,10 +15,7 @@ class VerifyEmail extends Component
$this->rateLimit(1, 300);
auth()->user()->sendVerificationEmail();
$this->dispatch('success', 'Email verification link sent!');
-
} catch (\Exception $e) {
- ray($e);
-
return handleError($e, $this);
}
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index e3d05bc2c..dd7c446b5 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -6,50 +6,159 @@ 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\Facades\Validator;
use Illuminate\Support\Str;
+use OpenApi\Attributes as OA;
use RuntimeException;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
+#[OA\Schema(
+ description: 'Application model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer', 'description' => 'The application identifier in the database.'],
+ 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'The application description.'],
+ 'repository_project_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'The repository project identifier.'],
+ 'uuid' => ['type' => 'string', 'description' => 'The application UUID.'],
+ 'name' => ['type' => 'string', 'description' => 'The application name.'],
+ 'fqdn' => ['type' => 'string', 'nullable' => true, 'description' => 'The application domains.'],
+ 'config_hash' => ['type' => 'string', 'description' => 'Configuration hash.'],
+ 'git_repository' => ['type' => 'string', 'description' => 'Git repository URL.'],
+ 'git_branch' => ['type' => 'string', 'description' => 'Git branch.'],
+ 'git_commit_sha' => ['type' => 'string', 'description' => 'Git commit SHA.'],
+ 'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'],
+ 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'],
+ 'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']],
+ 'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'],
+ 'install_command' => ['type' => 'string', 'description' => 'Install command.'],
+ 'build_command' => ['type' => 'string', 'description' => 'Build command.'],
+ 'start_command' => ['type' => 'string', 'description' => 'Start command.'],
+ 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
+ 'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
+ 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
+ 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
+ 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
+ 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
+ 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
+ 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
+ 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
+ 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
+ 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
+ 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
+ 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
+ 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
+ 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
+ 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
+ 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
+ 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
+ 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
+ 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
+ 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
+ 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
+ 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
+ 'status' => ['type' => 'string', 'description' => 'Application status.'],
+ 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
+ 'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
+ 'destination_id' => ['type' => 'integer', 'description' => 'Destination identifier.'],
+ 'source_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Source identifier.'],
+ 'private_key_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Private key identifier.'],
+ 'environment_id' => ['type' => 'integer', 'description' => 'Environment identifier.'],
+ 'dockerfile' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile content. Used for dockerfile build pack.'],
+ 'dockerfile_location' => ['type' => 'string', 'description' => 'Dockerfile location.'],
+ 'custom_labels' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom labels.'],
+ 'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile target build.'],
+ 'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitHub.'],
+ 'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitLab.'],
+ 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Bitbucket.'],
+ 'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Gitea.'],
+ 'docker_compose_location' => ['type' => 'string', 'description' => 'Docker compose location.'],
+ 'docker_compose' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose content. Used for docker compose build pack.'],
+ 'docker_compose_raw' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose raw content.'],
+ 'docker_compose_domains' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose domains.'],
+ 'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom start command.'],
+ 'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom build command.'],
+ 'swarm_replicas' => ['type' => 'integer', 'nullable' => true, 'description' => 'Swarm replicas. Only used for swarm deployments.'],
+ 'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true, 'description' => 'Swarm placement constraints. Only used for swarm deployments.'],
+ 'custom_docker_run_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom docker run options.'],
+ 'post_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command.'],
+ 'post_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command container.'],
+ 'pre_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command.'],
+ 'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command container.'],
+ 'watch_paths' => ['type' => 'string', 'nullable' => true, 'description' => 'Watch paths.'],
+ 'custom_healthcheck_found' => ['type' => 'boolean', 'description' => 'Custom healthcheck found.'],
+ 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
+ 'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was created.'],
+ 'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'],
+ 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
+ 'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
+ 'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'],
+ ]
+)]
+
class Application extends BaseModel
{
use SoftDeletes;
+ private static $parserVersion = '4';
+
protected $guarded = [];
+ protected $appends = ['server_status'];
+
protected static function booted()
{
static::saving(function ($application) {
- if ($application->fqdn == '') {
- $application->fqdn = null;
+ $payload = [];
+ if ($application->isDirty('fqdn')) {
+ if ($application->fqdn === '') {
+ $application->fqdn = null;
+ }
+ $payload['fqdn'] = $application->fqdn;
+ }
+ if ($application->isDirty('install_command')) {
+ $payload['install_command'] = str($application->install_command)->trim();
+ }
+ if ($application->isDirty('build_command')) {
+ $payload['build_command'] = str($application->build_command)->trim();
+ }
+ if ($application->isDirty('start_command')) {
+ $payload['start_command'] = str($application->start_command)->trim();
+ }
+ if ($application->isDirty('base_directory')) {
+ $payload['base_directory'] = str($application->base_directory)->trim();
+ }
+ if ($application->isDirty('publish_directory')) {
+ $payload['publish_directory'] = str($application->publish_directory)->trim();
+ }
+ if ($application->isDirty('status')) {
+ $payload['last_online_at'] = now();
+ }
+ if ($application->isDirty('custom_nginx_configuration')) {
+ if ($application->custom_nginx_configuration === '') {
+ $payload['custom_nginx_configuration'] = null;
+ }
+ }
+ if (count($payload) > 0) {
+ $application->forceFill($payload);
}
- $application->forceFill([
- 'fqdn' => $application->fqdn,
- 'install_command' => Str::of($application->install_command)->trim(),
- 'build_command' => Str::of($application->build_command)->trim(),
- 'start_command' => Str::of($application->start_command)->trim(),
- 'base_directory' => Str::of($application->base_directory)->trim(),
- 'publish_directory' => Str::of($application->publish_directory)->trim(),
- ]);
});
static::created(function ($application) {
ApplicationSetting::create([
'application_id' => $application->id,
]);
+ $application->compose_parsing_version = self::$parserVersion;
+ $application->save();
});
- static::deleting(function ($application) {
+ static::forceDeleting(function ($application) {
$application->update(['fqdn' => null]);
$application->settings()->delete();
- $storages = $application->persistentStorages()->get();
- $server = data_get($application, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
$application->environment_variables_preview()->delete();
@@ -57,19 +166,108 @@ class Application extends BaseModel
$task->delete();
}
$application->tags()->detach();
+ $application->previews()->delete();
+ foreach ($application->deployment_queue as $deployment) {
+ $deployment->delete();
+ }
});
}
+ public static function ownedByCurrentTeamAPI(int $teamId)
+ {
+ return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->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);
}
}
+ public function delete_volumes(?Collection $persistentStorages)
+ {
+ if ($this->build_pack === 'dockercompose') {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
+ } else {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $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 additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -131,12 +329,24 @@ class Application extends BaseModel
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
- return route('project.application.scheduled-tasks', [
+ $route = route('project.application.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
+ $settings = instanceSettings();
+ if (data_get($settings, 'fqdn')) {
+ $url = Url::fromString($route);
+ $url = $url->withPort(null);
+ $fqdn = data_get($settings, 'fqdn');
+ $fqdn = str_replace(['http://', 'https://'], '', $fqdn);
+ $url = $url->withHost($fqdn);
+
+ return $url->__toString();
+ }
+
+ return $route;
}
return null;
@@ -174,12 +384,20 @@ class Application extends BaseModel
return Attribute::make(
get: function () {
if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
+ if (str($this->git_repository)->contains('bitbucket')) {
+ return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
+ }
+
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ if (str($this->git_repository)->contains('bitbucket')) {
+ return "https://{$git_repository}/src/{$this->git_branch}";
+ }
+
return "https://{$git_repository}/tree/{$this->git_branch}";
}
@@ -228,18 +446,13 @@ class Application extends BaseModel
public function gitCommitLink($link): string
{
- if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
+ if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) {
if (str($this->source->html_url)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
}
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
}
- if (strpos($this->git_repository, 'git@') === 0) {
- $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
-
- return "https://{$git_repository}/commit/{$link}";
- }
if (str($this->git_repository)->contains('bitbucket')) {
$git_repository = str_replace('.git', '', $this->git_repository);
$url = Url::fromString($git_repository);
@@ -248,6 +461,14 @@ class Application extends BaseModel
return $url->__toString();
}
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ if (data_get($this, 'source.html_url')) {
+ return "{$this->source->html_url}/{$git_repository}/commit/{$link}";
+ }
+
+ return "{$git_repository}/commit/{$link}";
+ }
return $this->git_repository;
}
@@ -286,23 +507,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(
@@ -327,6 +531,11 @@ class Application extends BaseModel
);
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->startsWith('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
@@ -337,6 +546,28 @@ class Application extends BaseModel
return $this->getRawOriginal('status');
}
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ if ($this->additional_servers->count() === 0) {
+ return $this->destination->server->isFunctional();
+ } else {
+ $additional_servers_status = $this->additional_servers->pluck('pivot.status');
+ $main_server_status = $this->destination->server->isFunctional();
+ foreach ($additional_servers_status as $status) {
+ $server_status = str($status)->before(':')->value();
+ if ($server_status !== 'running') {
+ return false;
+ }
+ }
+
+ return $main_server_status;
+ }
+ }
+ );
+ }
+
public function status(): Attribute
{
return Attribute::make(
@@ -407,6 +638,14 @@ class Application extends BaseModel
);
}
+ public function customNginxConfiguration(): Attribute
+ {
+ return Attribute::make(
+ set: fn ($value) => base64_encode($value),
+ get: fn ($value) => base64_decode($value),
+ );
+ }
+
public function portsExposesArray(): Attribute
{
return Attribute::make(
@@ -510,6 +749,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();
@@ -532,7 +776,7 @@ class Application extends BaseModel
public function get_last_successful_deployment()
{
- return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
+ return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
}
public function get_last_days_deployments()
@@ -632,7 +876,7 @@ class Application extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
- $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
+ $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration);
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
} else {
@@ -662,21 +906,7 @@ class Application extends BaseModel
public function customRepository()
{
- preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
- $port = 22;
- if (count($matches) === 1) {
- $port = $matches[0];
- $gitHost = str($this->git_repository)->before(':');
- $gitRepo = str($this->git_repository)->after('/');
- $repository = "$gitHost:$gitRepo";
- } else {
- $repository = $this->git_repository;
- }
-
- return [
- 'repository' => $repository,
- 'port' => $port,
- ];
+ return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source);
}
public function generateBaseDir(string $uuid)
@@ -684,6 +914,11 @@ class Application extends BaseModel
return "/artifacts/{$uuid}";
}
+ public function dirOnServer()
+ {
+ return application_configuration_dir()."/{$this->uuid}";
+ }
+
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
@@ -838,7 +1073,7 @@ class Application extends BaseModel
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
- if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
@@ -921,7 +1156,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
- } elseif ($git_type === 'github') {
+ } elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@@ -965,7 +1200,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
- } elseif ($git_type === 'github') {
+ } elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@@ -997,7 +1232,7 @@ class Application extends BaseModel
}
}
- public function parseRawCompose()
+ public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1005,6 +1240,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', []));
@@ -1014,9 +1250,9 @@ class Application extends BaseModel
$type = null;
$source = null;
if (is_string($volume)) {
- $source = Str::of($volume)->before(':');
+ $source = str($volume)->before(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
- $type = Str::of('bind');
+ $type = str('bind');
}
} elseif (is_array($volume)) {
$type = data_get_str($volume, 'type');
@@ -1057,9 +1293,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([]);
@@ -1072,16 +1310,11 @@ class Application extends BaseModel
if ($isInit && $this->docker_compose_raw) {
return;
}
- $uuid = new Cuid2();
+ $uuid = new Cuid2;
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
- // $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$workdir$composeFile"]);
- // if ($composeFile !== $prComposeFile) {
- // $fileList->push(".$prComposeFile");
- // }
-
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
@@ -1097,46 +1330,54 @@ class Application extends BaseModel
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
- $composeFileContent = instant_remote_process($commands, $this->destination->server, false);
- if (! $composeFileContent) {
+ try {
+ $composeFileContent = instant_remote_process($commands, $this->destination->server);
+ } catch (\Exception $e) {
+ if (str($e->getMessage())->contains('No such file')) {
+ 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.");
+ }
+ if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
+ if ($this->deploymentType() === 'deploy_key') {
+ throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
+ }
+ throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
+ }
+ throw new \RuntimeException($e->getMessage());
+ } finally {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
- 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.");
- } else {
+ }
+ if ($composeFileContent) {
$this->docker_compose_raw = $composeFileContent;
$this->save();
- }
-
- $commands = collect([
- "rm -rf /tmp/{$uuid}",
- ]);
- instant_remote_process($commands, $this->destination->server, false);
- $parsedServices = $this->parseCompose();
- if ($this->docker_compose_domains) {
- $json = collect(json_decode($this->docker_compose_domains));
- $names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
- $jsonNames = $json->keys()->toArray();
- $diff = array_diff($jsonNames, $names);
- $json = $json->filter(function ($value, $key) use ($diff) {
- return ! in_array($key, $diff);
- });
- if ($json) {
- $this->docker_compose_domains = json_encode($json);
- } else {
- $this->docker_compose_domains = null;
+ $parsedServices = $this->parse();
+ if ($this->docker_compose_domains) {
+ $json = collect(json_decode($this->docker_compose_domains));
+ $names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
+ $jsonNames = $json->keys()->toArray();
+ $diff = array_diff($jsonNames, $names);
+ $json = $json->filter(function ($value, $key) use ($diff) {
+ return ! in_array($key, $diff);
+ });
+ if ($json) {
+ $this->docker_compose_domains = json_encode($json);
+ } else {
+ $this->docker_compose_domains = null;
+ }
+ $this->save();
}
- $this->save();
- }
- return [
- 'parsedServices' => $parsedServices,
- 'initialDockerComposeLocation' => $this->docker_compose_location,
- 'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
- ];
+ return [
+ 'parsedServices' => $parsedServices,
+ 'initialDockerComposeLocation' => $this->docker_compose_location,
+ ];
+ } 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)
@@ -1146,13 +1387,11 @@ class Application extends BaseModel
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
- ray('custom_labels is not base64 encoded');
$this->custom_labels = str($customLabels)->replace(',', "\n");
$this->custom_labels = base64_encode($customLabels);
}
$customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
- ray('custom_labels contains non-ascii characters');
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
}
$this->custom_labels = base64_encode($customLabels);
@@ -1277,7 +1516,7 @@ class Application extends BaseModel
$template = $this->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
- $random = new Cuid2(7);
+ $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
@@ -1288,4 +1527,126 @@ class Application extends BaseModel
return $preview;
}
+
+ public static function getDomainsByUuid(string $uuid): array
+ {
+ $application = self::where('uuid', $uuid)->first();
+
+ if ($application) {
+ return $application->fqdns;
+ }
+
+ return [];
+ }
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function generateConfig($is_json = false)
+ {
+ $config = collect([]);
+ if ($this->build_pack = 'nixpacks') {
+ $config = collect([
+ 'build_pack' => 'nixpacks',
+ 'docker_registry_image_name' => $this->docker_registry_image_name,
+ 'docker_registry_image_tag' => $this->docker_registry_image_tag,
+ 'install_command' => $this->install_command,
+ 'build_command' => $this->build_command,
+ 'start_command' => $this->start_command,
+ 'base_directory' => $this->base_directory,
+ 'publish_directory' => $this->publish_directory,
+ 'custom_docker_run_options' => $this->custom_docker_run_options,
+ 'ports_exposes' => $this->ports_exposes,
+ 'ports_mappings' => $this->ports_mapping,
+ 'settings' => collect([
+ 'is_static' => $this->settings->is_static,
+ ]),
+ ]);
+ }
+ $config = $config->filter(function ($value) {
+ return str($value)->isNotEmpty();
+ });
+ if ($is_json) {
+ return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ }
+
+ return $config;
+ }
+
+ public function setConfig($config)
+ {
+ $validator = Validator::make(['config' => $config], [
+ 'config' => 'required|json',
+ ]);
+ if ($validator->fails()) {
+ throw new \Exception('Invalid JSON format');
+ }
+ $config = json_decode($config, true);
+
+ $deepValidator = Validator::make(['config' => $config], [
+ 'config.build_pack' => 'required|string',
+ 'config.base_directory' => 'required|string',
+ 'config.publish_directory' => 'required|string',
+ 'config.ports_exposes' => 'required|string',
+ 'config.settings.is_static' => 'required|boolean',
+ ]);
+ if ($deepValidator->fails()) {
+ throw new \Exception('Invalid data');
+ }
+ $config = $deepValidator->validated()['config'];
+
+ try {
+ $settings = data_get($config, 'settings', []);
+ data_forget($config, 'settings');
+ $this->update($config);
+ $this->settings()->update($settings);
+ } catch (\Exception $e) {
+ throw new \Exception('Failed to update application settings');
+ }
+ }
}
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index b1c595046..c261c30c6 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -2,13 +2,58 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
+use OpenApi\Attributes as OA;
+#[OA\Schema(
+ description: 'Project model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'application_id' => ['type' => 'string'],
+ 'deployment_uuid' => ['type' => 'string'],
+ 'pull_request_id' => ['type' => 'integer'],
+ 'force_rebuild' => ['type' => 'boolean'],
+ 'commit' => ['type' => 'string'],
+ 'status' => ['type' => 'string'],
+ 'is_webhook' => ['type' => 'boolean'],
+ 'is_api' => ['type' => 'boolean'],
+ 'created_at' => ['type' => 'string'],
+ 'updated_at' => ['type' => 'string'],
+ 'logs' => ['type' => 'string'],
+ 'current_process_id' => ['type' => 'string'],
+ 'restart_only' => ['type' => 'boolean'],
+ 'git_type' => ['type' => 'string'],
+ 'server_id' => ['type' => 'integer'],
+ 'application_name' => ['type' => 'string'],
+ 'server_name' => ['type' => 'string'],
+ 'deployment_url' => ['type' => 'string'],
+ 'destination_id' => ['type' => 'string'],
+ 'only_this_server' => ['type' => 'boolean'],
+ 'rollback' => ['type' => 'boolean'],
+ 'commit_message' => ['type' => 'string'],
+ ],
+)]
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 3bdd24014..bf2bf05bf 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();
@@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel
});
}
});
+ static::saving(function ($preview) {
+ if ($preview->isDirty('status')) {
+ $preview->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
@@ -35,6 +40,11 @@ class ApplicationPreview extends BaseModel
return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail();
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->startsWith('running');
+ }
+
public function application()
{
return $this->belongsTo(Application::class);
@@ -49,7 +59,7 @@ class ApplicationPreview extends BaseModel
$template = $this->application->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
- $random = new Cuid2(7);
+ $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php
index 7e028a6b5..17201ea6e 100644
--- a/app/Models/BaseModel.php
+++ b/app/Models/BaseModel.php
@@ -14,7 +14,7 @@ abstract class BaseModel extends Model
static::creating(function (Model $model) {
// Generate a UUID if one isn't set
if (! $model->uuid) {
- $model->uuid = (string) new Cuid2(7);
+ $model->uuid = (string) new Cuid2;
}
});
}
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index e84b6989b..71e8bbd21 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -4,7 +4,20 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
+use OpenApi\Attributes as OA;
+#[OA\Schema(
+ description: 'Environment model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'name' => ['type' => 'string'],
+ 'project_id' => ['type' => 'integer'],
+ 'created_at' => ['type' => 'string'],
+ 'updated_at' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ ]
+)]
class Environment extends Model
{
protected $guarded = [];
@@ -14,10 +27,8 @@ class Environment extends Model
static::deleting(function ($environment) {
$shared_variables = $environment->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting environment shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
-
});
}
@@ -27,6 +38,9 @@ class Environment extends Model
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mysqls()->count() == 0 &&
+ $this->keydbs()->count() == 0 &&
+ $this->dragonflies()->count() == 0 &&
+ $this->clickhouses()->count() == 0 &&
$this->mariadbs()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;
@@ -109,7 +123,7 @@ class Environment extends Model
protected function name(): Attribute
{
return Attribute::make(
- set: fn (string $value) => strtolower($value),
+ set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(),
);
}
}
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index ff63bca5a..08f23d7ab 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -5,9 +5,32 @@ namespace App\Models;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Str;
-use Symfony\Component\Yaml\Yaml;
+use OpenApi\Attributes as OA;
+use Visus\Cuid2\Cuid2;
+#[OA\Schema(
+ description: 'Environment Variable model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'uuid' => ['type' => 'string'],
+ 'application_id' => ['type' => 'integer'],
+ 'service_id' => ['type' => 'integer'],
+ 'database_id' => ['type' => 'integer'],
+ 'is_build_time' => ['type' => 'boolean'],
+ 'is_literal' => ['type' => 'boolean'],
+ 'is_multiline' => ['type' => 'boolean'],
+ 'is_preview' => ['type' => 'boolean'],
+ 'is_shared' => ['type' => 'boolean'],
+ 'is_shown_once' => ['type' => 'boolean'],
+ 'key' => ['type' => 'string'],
+ 'value' => ['type' => 'string'],
+ 'real_value' => ['type' => 'string'],
+ 'version' => ['type' => 'string'],
+ 'created_at' => ['type' => 'string'],
+ 'updated_at' => ['type' => 'string'],
+ ]
+)]
class EnvironmentVariable extends Model
{
protected $guarded = [];
@@ -21,10 +44,15 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
- protected $appends = ['real_value', 'is_shared'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
+ static::creating(function (Model $model) {
+ if (! $model->uuid) {
+ $model->uuid = (string) new Cuid2;
+ }
+ });
static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->application_id && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
@@ -46,6 +74,9 @@ class EnvironmentVariable extends Model
'version' => config('version'),
]);
});
+ static::saving(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->updateIsShared();
+ });
}
public function service()
@@ -68,8 +99,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;
@@ -84,69 +129,14 @@ 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
+ protected function isReallyRequired(): 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;
- }
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
);
}
@@ -166,32 +156,38 @@ class EnvironmentVariable extends Model
private function get_real_environment_variables(?string $environment_variable = null, $resource = null)
{
- if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) {
+ if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) {
return null;
}
$environment_variable = trim($environment_variable);
- $type = str($environment_variable)->after('{{')->before('.')->value;
- if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) {
- $variable = Str::after($environment_variable, "{$type}.");
- $variable = Str::before($variable, '}}');
- $variable = Str::of($variable)->trim()->value;
+ $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)) {
- return $variable;
+ continue;
}
- if ($type === 'environment') {
+ $variable = str($sharedEnv)->match('/\.(.*)/');
+ if ($type->value() === 'environment') {
$id = $resource->environment->id;
- } elseif ($type === 'project') {
+ } elseif ($type->value() === 'project') {
$id = $resource->environment->project->id;
- } else {
+ } elseif ($type->value() === 'team') {
$id = $resource->team()->id;
}
+ if (is_null($id)) {
+ continue;
+ }
$environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first();
if ($environment_variable_found) {
- return $environment_variable_found;
+ $environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $environment_variable_found->value);
}
}
- return $environment_variable;
+ return str($environment_variable)->value();
}
private function get_environment_variables(?string $environment_variable = null): ?string
@@ -205,7 +201,7 @@ class EnvironmentVariable extends Model
private function set_environment_variables(?string $environment_variable = null): ?string
{
- if (is_null($environment_variable) && $environment_variable == '') {
+ if (is_null($environment_variable) && $environment_variable === '') {
return null;
}
$environment_variable = trim($environment_variable);
@@ -220,7 +216,14 @@ class EnvironmentVariable extends Model
protected function key(): Attribute
{
return Attribute::make(
- set: fn (string $value) => Str::of($value)->trim(),
+ set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
+
+ protected function updateIsShared(): void
+ {
+ $type = str($this->value)->after('{{')->before('.')->value;
+ $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
+ $this->is_shared = $isShared;
+ }
}
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index daf902daf..0b0e93b12 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -20,6 +20,22 @@ class GithubApp extends BaseModel
'webhook_secret',
];
+ protected static function booted(): void
+ {
+ static::deleting(function (GithubApp $github_app) {
+ $applications_count = Application::where('source_id', $github_app->id)->count();
+ if ($applications_count > 0) {
+ throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
+ }
+ $github_app->privateKey()->delete();
+ });
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return GithubApp::whereTeamId(currentTeam()->id);
+ }
+
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@@ -30,15 +46,9 @@ class GithubApp extends BaseModel
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
}
- protected static function booted(): void
+ public function team()
{
- static::deleting(function (GithubApp $github_app) {
- $applications_count = Application::where('source_id', $github_app->id)->count();
- if ($applications_count > 0) {
- throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
- }
- $github_app->privateKey()->delete();
- });
+ return $this->belongsTo(Team::class);
}
public function applications()
@@ -55,7 +65,7 @@ class GithubApp extends BaseModel
{
return Attribute::make(
get: function () {
- if ($this->getMorphClass() === 'App\Models\GithubApp') {
+ if ($this->getMorphClass() === \App\Models\GithubApp::class) {
return 'github';
}
},
diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php
index a789a7e65..2112a4a66 100644
--- a/app/Models/GitlabApp.php
+++ b/app/Models/GitlabApp.php
@@ -9,6 +9,11 @@ class GitlabApp extends BaseModel
'app_secret',
];
+ public static function ownedByCurrentTeam()
+ {
+ return GitlabApp::whereTeamId(currentTeam()->id);
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'source');
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 452c5ca22..eeb803925 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -17,8 +18,26 @@ class InstanceSettings extends Model implements SendsEmail
protected $casts = [
'resale_license' => 'encrypted',
'smtp_password' => 'encrypted',
+ 'allowed_ip_ranges' => 'array',
+ 'is_auto_update_enabled' => 'boolean',
+ 'auto_update_frequency' => 'string',
+ 'update_check_frequency' => 'string',
+ 'sentinel_token' => 'encrypted',
];
+ protected static function booted(): void
+ {
+ static::updated(function ($settings) {
+ if ($settings->isDirty('helper_version')) {
+ Server::chunkById(100, function ($servers) {
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+ });
+ }
+ });
+ }
+
public function fqdn(): Attribute
{
return Attribute::make(
@@ -33,6 +52,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);
@@ -47,4 +90,27 @@ class InstanceSettings extends Model implements SendsEmail
return explode(',', $recipients);
}
+
+ public function getTitleDisplayName(): string
+ {
+ $instanceName = $this->instance_name;
+ if (! $instanceName) {
+ return '';
+ }
+
+ return "[{$instanceName}]";
+ }
+
+ // public function helperVersion(): Attribute
+ // {
+ // return Attribute::make(
+ // get: function ($value) {
+ // if (isDev()) {
+ // return 'latest';
+ // }
+
+ // return $value;
+ // }
+ // );
+ // }
}
diff --git a/app/Models/Kubernetes.php b/app/Models/Kubernetes.php
index 2ad7a2110..174cb5bc8 100644
--- a/app/Models/Kubernetes.php
+++ b/app/Models/Kubernetes.php
@@ -2,6 +2,4 @@
namespace App\Models;
-class Kubernetes extends BaseModel
-{
-}
+class Kubernetes extends BaseModel {}
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index 62ee4c45c..2c223be77 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Events\FileStorageChanged;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
@@ -23,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();
@@ -33,20 +35,56 @@ class LocalFileVolume extends BaseModel
$workdir = $this->resource->workdir();
$server = $this->resource->destination->server;
}
- $commands = collect([
- "cd $workdir",
- ]);
- $fs_path = data_get($this, 'fs_path');
- if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
- $commands->push("rm -rf $fs_path");
+ $commands = collect([]);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
}
- ray($commands);
+ $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();
+ }
+ }
- return instant_remote_process($commands, $server);
+ 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 $path > /dev/null 2>&1 || true");
+ } elseif ($isDir === 'OK') {
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
+ $commands->push("rmdir $path > /dev/null 2>&1 || true");
+ }
+ }
+ if ($commands->count() > 0) {
+ return instant_remote_process($commands, $server);
+ }
}
public function saveStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -55,13 +93,10 @@ class LocalFileVolume extends BaseModel
$workdir = $this->resource->workdir();
$server = $this->resource->destination->server;
}
- $commands = collect([
- "mkdir -p $workdir > /dev/null 2>&1 || true",
- "cd $workdir",
- ]);
- $is_directory = $this->is_directory;
- if ($is_directory) {
+ $commands = collect([]);
+ if ($this->is_directory) {
$commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true");
+ $commands->push("cd $workdir");
}
if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) {
$parent_dir = str($this->fs_path)->beforeLast('/');
@@ -69,35 +104,50 @@ 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);
+ $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) {
- 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 (! $fileVolume->is_directory && $isDir == 'NOK') {
+ if ($isDir === 'NOK' && ! $this->is_directory) {
+ $chmod = data_get($this, 'chmod');
+ $chown = data_get($this, 'chown');
if ($content) {
$content = base64_encode($content);
- $chmod = $fileVolume->chmod;
- $chown = $fileVolume->chown;
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
- $commands->push("chmod +x $path");
- if ($chown) {
- $commands->push("chown $chown $path");
- }
- if ($chmod) {
- $commands->push("chmod $chmod $path");
- }
+ } else {
+ $commands->push("touch $path");
}
- } elseif ($isDir == 'NOK' && $fileVolume->is_directory) {
+ $commands->push("chmod +x $path");
+ if ($chown) {
+ $commands->push("chown $chown $path");
+ }
+ if ($chmod) {
+ $commands->push("chmod $chmod $path");
+ }
+ } elseif ($isDir === 'NOK' && $this->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index e48b8b405..68e476365 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -4,7 +4,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Str;
class LocalPersistentVolume extends Model
{
@@ -33,14 +32,14 @@ class LocalPersistentVolume extends Model
protected function name(): Attribute
{
return Attribute::make(
- set: fn (string $value) => Str::of($value)->trim()->value,
+ set: fn (string $value) => str($value)->trim()->value,
);
}
protected function mountPath(): Attribute
{
return Attribute::make(
- set: fn (string $value) => Str::of($value)->trim()->start('/')->value
+ set: fn (string $value) => str($value)->trim()->start('/')->value
);
}
@@ -49,7 +48,7 @@ class LocalPersistentVolume extends Model
return Attribute::make(
set: function (?string $value) {
if ($value) {
- return Str::of($value)->trim()->start('/')->value;
+ return str($value)->trim()->start('/')->value;
} else {
return $value;
}
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index 187dfca58..065746ede 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -2,41 +2,167 @@
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;
+#[OA\Schema(
+ description: 'Private Key model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'uuid' => ['type' => 'string'],
+ 'name' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ 'private_key' => ['type' => 'string', 'format' => 'private-key'],
+ 'is_git_related' => ['type' => 'boolean'],
+ 'team_id' => ['type' => 'integer'],
+ 'created_at' => ['type' => 'string'],
+ 'updated_at' => ['type' => 'string'],
+ ],
+)]
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) {
+ $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()
@@ -58,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 acc98e341..f27e6c208 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -2,13 +2,33 @@
namespace App\Models;
+use OpenApi\Attributes as OA;
+
+#[OA\Schema(
+ description: 'Project model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'uuid' => ['type' => 'string'],
+ 'name' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ 'environments' => new OA\Property(
+ property: 'environments',
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Environment'),
+ description: 'The environments of the project.'
+ ),
+ ]
+)]
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()
@@ -27,7 +47,6 @@ class Project extends BaseModel
$project->settings()->delete();
$shared_variables = $project->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting project shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
});
@@ -103,13 +122,36 @@ class Project extends BaseModel
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
- public function resource_count()
+ public function isEmpty()
{
- return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
+ return $this->applications()->count() == 0 &&
+ $this->redis()->count() == 0 &&
+ $this->postgresqls()->count() == 0 &&
+ $this->mysqls()->count() == 0 &&
+ $this->keydbs()->count() == 0 &&
+ $this->dragonflies()->count() == 0 &&
+ $this->clickhouses()->count() == 0 &&
+ $this->mariadbs()->count() == 0 &&
+ $this->mongodbs()->count() == 0 &&
+ $this->services()->count() == 0;
}
public function databases()
{
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 getDefaultEnvironmentAttribute()
+ {
+ $default = $this->environments()->where('name', 'production')->first();
+ if ($default) {
+ return $default->name;
+ }
+ $default = $this->environments()->get();
+ if ($default->count() > 0) {
+ return $default->sortBy('created_at')->first()->name;
+ }
+
+ return null;
+ }
}
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index 278ee5995..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 {
@@ -50,7 +60,7 @@ class S3Storage extends BaseModel
} catch (\Throwable $e) {
$this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject('Coolify: S3 Storage Connection Error');
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
$users = collect([]);
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index edd840e7d..473fc7b4b 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,22 @@ 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 instanceof ServiceDatabase) {
+ $destination = data_get($this->database->service, 'destination');
+ $server = data_get($destination, 'server');
+ } else {
+ $destination = data_get($this->database, 'destination');
+ $server = data_get($destination, 'server');
+ }
+ if ($server) {
+ return $server;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 1cb805e8e..264a04d1f 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -26,6 +26,26 @@ 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) {
+ return $this->application->destination->server;
+ }
+ } elseif ($this->service) {
+ if ($this->service->destination && $this->service->destination->server) {
+ return $this->service->destination->server;
+ }
+ } elseif ($this->database) {
+ if ($this->database->destination && $this->database->destination->server) {
+ return $this->database->destination->server;
+ }
+ }
+
+ return null;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index b1419dc0e..64192c71f 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -3,25 +3,51 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
+use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
-use App\Jobs\PullSentinelImageJob;
-use App\Notifications\Server\Revived;
+use App\Jobs\CheckAndStartSentinelJob;
+use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
+use OpenApi\Attributes as OA;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
+#[OA\Schema(
+ description: 'Server model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer'],
+ 'uuid' => ['type' => 'string'],
+ 'name' => ['type' => 'string'],
+ 'description' => ['type' => 'string'],
+ 'ip' => ['type' => 'string'],
+ 'user' => ['type' => 'string'],
+ 'port' => ['type' => 'integer'],
+ 'proxy' => ['type' => 'object'],
+ 'high_disk_usage_notification_sent' => ['type' => 'boolean'],
+ 'unreachable_notification_sent' => ['type' => 'boolean'],
+ 'unreachable_count' => ['type' => 'integer'],
+ 'validation_logs' => ['type' => 'string'],
+ 'log_drain_notification_sent' => ['type' => 'boolean'],
+ 'swarm_cluster' => ['type' => 'string'],
+ 'delete_unused_volumes' => ['type' => 'boolean'],
+ 'delete_unused_networks' => ['type' => 'boolean'],
+ ]
+)]
+
class Server extends BaseModel
{
- use SchemalessAttributesTrait;
+ use SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -30,19 +56,56 @@ class Server extends BaseModel
static::saving(function ($server) {
$payload = [];
if ($server->user) {
- $payload['user'] = Str::of($server->user)->trim();
+ $payload['user'] = str($server->user)->trim();
}
if ($server->ip) {
- $payload['ip'] = Str::of($server->ip)->trim();
+ $payload['ip'] = str($server->ip)->trim();
}
$server->forceFill($payload);
});
+ static::saved(function ($server) {
+ if ($server->privateKey?->isDirty()) {
+ refresh_server_connection($server->privateKey);
+ }
+ });
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
+ if ($server->id === 0) {
+ if ($server->isSwarm()) {
+ SwarmDocker::create([
+ 'id' => 0,
+ 'name' => 'coolify',
+ 'network' => 'coolify-overlay',
+ 'server_id' => $server->id,
+ ]);
+ } else {
+ StandaloneDocker::create([
+ 'id' => 0,
+ 'name' => 'coolify',
+ 'network' => 'coolify',
+ 'server_id' => $server->id,
+ ]);
+ }
+ } else {
+ if ($server->isSwarm()) {
+ SwarmDocker::create([
+ 'name' => 'coolify-overlay',
+ 'network' => 'coolify-overlay',
+ 'server_id' => $server->id,
+ ]);
+ } else {
+ StandaloneDocker::create([
+ 'name' => 'coolify',
+ 'network' => 'coolify',
+ 'server_id' => $server->id,
+ ]);
+ }
+ }
});
- static::deleting(function ($server) {
+
+ static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -50,18 +113,38 @@ class Server extends BaseModel
});
}
- public $casts = [
+ protected $casts = [
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
+ 'delete_unused_volumes' => 'boolean',
+ 'delete_unused_networks' => 'boolean',
+ 'unreachable_notification_sent' => 'boolean',
+ 'is_build_server' => 'boolean',
+ 'force_disabled' => 'boolean',
];
protected $schemalessAttributes = [
'proxy',
];
+ protected $fillable = [
+ 'name',
+ 'ip',
+ 'port',
+ 'user',
+ 'description',
+ 'private_key_id',
+ 'team_id',
+ ];
+
protected $guarded = [];
+ public function type()
+ {
+ return 'server';
+ }
+
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -94,39 +177,9 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class);
}
- public function addInitialNetwork()
+ public function proxySet()
{
- if ($this->id === 0) {
- if ($this->isSwarm()) {
- SwarmDocker::create([
- 'id' => 0,
- 'name' => 'coolify',
- 'network' => 'coolify-overlay',
- 'server_id' => $this->id,
- ]);
- } else {
- StandaloneDocker::create([
- 'id' => 0,
- 'name' => 'coolify',
- 'network' => 'coolify',
- 'server_id' => $this->id,
- ]);
- }
- } else {
- if ($this->isSwarm()) {
- SwarmDocker::create([
- 'name' => 'coolify-overlay',
- 'network' => 'coolify-overlay',
- 'server_id' => $this->id,
- ]);
- } else {
- StandaloneDocker::create([
- 'name' => 'coolify',
- 'network' => 'coolify',
- 'server_id' => $this->id,
- ]);
- }
- }
+ return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
public function setupDefault404Redirect()
@@ -134,13 +187,13 @@ class Server extends BaseModel
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
$redirect_url = $this->proxy->redirect_url;
- if ($proxy_type === 'TRAEFIK_V2') {
+ 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
}';
@@ -164,7 +217,7 @@ respond 404
return;
}
- if ($proxy_type === 'TRAEFIK_V2') {
+ if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
@@ -174,10 +227,13 @@ respond 404
1 => 'https',
],
'service' => 'noop',
- 'rule' => 'HostRegexp(`{catchall:.*}`)',
+ 'rule' => 'HostRegexp(`.+`)',
+ 'tls' => [
+ 'certResolver' => 'letsencrypt',
+ ],
'priority' => 1,
'middlewares' => [
- 0 => 'redirect-regexp@file',
+ 0 => 'redirect-regexp',
],
],
],
@@ -210,7 +266,7 @@ respond 404
$conf;
$base64 = base64_encode($conf);
- } elseif ($proxy_type === 'CADDY') {
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
@@ -226,9 +282,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();
}
@@ -236,11 +289,11 @@ respond 404
public function setupDynamicProxyConfiguration()
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$dynamic_config_path = $this->proxyPath().'/dynamic';
- if ($this->proxyType() === 'TRAEFIK_V2') {
+ if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml";
- if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
+ if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -278,6 +331,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' => [
@@ -298,6 +358,15 @@ respond 404
],
],
],
+ 'coolify-terminal' => [
+ 'loadBalancer' => [
+ 'servers' => [
+ 0 => [
+ 'url' => 'http://coolify-realtime:6002',
+ ],
+ ],
+ ],
+ ],
],
],
];
@@ -327,6 +396,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 =
@@ -340,13 +419,13 @@ respond 404
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
- if (config('app.env') == 'local') {
+ if (config('app.env') === 'local') {
// ray($yaml);
}
}
} elseif ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
- if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
+ if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -360,6 +439,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);
@@ -386,12 +468,20 @@ $schema://$host {
// TODO: should use /traefik for already exisiting configurations?
// 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_V2->value) {
- $proxy_path = $proxy_path;
+ if ($proxyType === ProxyTypes::TRAEFIK->value) {
+ // 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;
@@ -399,15 +489,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_V2->value;
- // $this->proxy->status = ProxyStatus::EXITED->value;
- // $this->save();
- // }
return data_get($this->proxy, 'type');
}
@@ -426,20 +507,6 @@ $schema://$host {
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
}
- public function skipServer()
- {
- if ($this->ip === '1.2.3.4') {
- // ray('skipping 1.2.3.4');
- return true;
- }
- if ($this->settings->force_disabled === true) {
- // ray('force_disabled');
- return true;
- }
-
- return false;
- }
-
public function isForceDisabled()
{
return $this->settings->force_disabled;
@@ -447,124 +514,112 @@ $schema://$host {
public function forceEnableServer()
{
- $this->settings->update([
- 'force_disabled' => false,
- ]);
+ $this->settings->force_disabled = false;
+ $this->settings->save();
}
public function forceDisableServer()
{
- $this->settings->update([
- 'force_disabled' => true,
- ]);
+ $this->settings->force_disabled = true;
+ $this->settings->save();
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
+ public function sentinelHeartbeat(bool $isReset = false)
+ {
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
+ }
+
+ /**
+ * Get the wait time for Sentinel to push before performing an SSH check.
+ *
+ * @return int The wait time in seconds.
+ */
+ public function waitBeforeDoingSshCheck(): int
+ {
+ $wait = $this->settings->sentinel_push_interval_seconds * 3;
+ if ($wait < 120) {
+ $wait = 120;
+ }
+
+ return $wait;
+ }
+
+ public function isSentinelLive()
+ {
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
+ }
+
+ public function isSentinelEnabled()
+ {
+ return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
+ }
+
+ public function isMetricsEnabled()
+ {
+ return $this->settings->is_metrics_enabled;
+ }
+
+ public function isServerApiEnabled()
+ {
+ return $this->settings->is_sentinel_enabled;
+ }
+
public function checkSentinel()
{
- ray("Checking sentinel on server: {$this->name}");
- if ($this->is_metrics_enabled) {
- $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
- $sentinel_found = json_decode($sentinel_found, true);
- $status = data_get($sentinel_found, '0.State.Status', 'exited');
- if ($status !== 'running') {
- ray('Sentinel is not running, starting it...');
- PullSentinelImageJob::dispatch($this);
- } else {
- ray('Sentinel is running');
+ CheckAndStartSentinelJob::dispatch($this);
+ }
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ if ($this->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ if (str($cpu)->contains('error')) {
+ $error = json_decode($cpu, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
}
+ $cpu = json_decode($cpu, true);
+
+ return collect($cpu)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
}
}
- public function getMetrics()
+ public function getMemoryMetrics(int $mins = 5)
{
- if ($this->is_metrics_enabled) {
- $from = now()->subMinutes(5)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
- $cpu = str($cpu)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($cpu)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $value] = explode(',', trim($line));
+ if ($this->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ if (str($memory)->contains('error')) {
+ $error = json_decode($memory, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $memory = json_decode($memory, true);
+ $parsedCollection = collect($memory)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['usedPercent']];
+ });
- return [(int) $time, (float) $value];
- });
- })->toArray();
-
- return $parsedCollection;
+ return $parsedCollection->toArray();
}
}
- public function isServerReady(int $tries = 3)
+ public function getDiskUsage(): ?string
{
- if ($this->skipServer()) {
- return false;
- }
- $serverUptimeCheckNumber = $this->unreachable_count;
- if ($this->unreachable_count < $tries) {
- $serverUptimeCheckNumber = $this->unreachable_count + 1;
- }
- if ($this->unreachable_count > $tries) {
- $serverUptimeCheckNumber = $tries;
- }
-
- $serverUptimeCheckNumberMax = $tries;
-
- // ray('server: ' . $this->name);
- // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
- // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
-
- ['uptime' => $uptime] = $this->validateConnection();
- if ($uptime) {
- if ($this->unreachable_notification_sent === true) {
- $this->update(['unreachable_notification_sent' => false]);
- }
-
- return true;
- } else {
- if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
- // Reached max number of retries
- if ($this->unreachable_notification_sent === false) {
- ray('Server unreachable, sending notification...');
- // $this->team?->notify(new Unreachable($this));
- $this->update(['unreachable_notification_sent' => true]);
- }
- if ($this->settings->is_reachable === true) {
- $this->settings()->update([
- 'is_reachable' => false,
- ]);
- }
-
- foreach ($this->applications() as $application) {
- $application->update(['status' => 'exited']);
- }
- foreach ($this->databases() as $database) {
- $database->update(['status' => 'exited']);
- }
- foreach ($this->services()->get() as $service) {
- $apps = $service->applications()->get();
- $dbs = $service->databases()->get();
- foreach ($apps as $app) {
- $app->update(['status' => 'exited']);
- }
- foreach ($dbs as $db) {
- $db->update(['status' => 'exited']);
- }
- }
- } else {
- $this->update([
- 'unreachable_count' => $this->unreachable_count + 1,
- ]);
- }
-
- return false;
- }
- }
-
- public function getDiskUsage()
- {
- return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
+ return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
+ // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
@@ -591,7 +646,49 @@ $schema://$host {
return instant_remote_process(["docker start $id"], $this);
}
- public function getContainers(): Collection
+ public function getContainers()
+ {
+ $containers = collect([]);
+ $containerReplicates = collect([]);
+ if ($this->isSwarm()) {
+ $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
+ $containers = format_docker_command_output_to_json($containers);
+ $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false);
+ if ($containerReplicates) {
+ $containerReplicates = format_docker_command_output_to_json($containerReplicates);
+ foreach ($containerReplicates as $containerReplica) {
+ $name = data_get($containerReplica, 'Name');
+ $containers = $containers->map(function ($container) use ($name, $containerReplica) {
+ if (data_get($container, 'Spec.Name') === $name) {
+ $replicas = data_get($containerReplica, 'Replicas');
+ $running = str($replicas)->explode('/')[0];
+ $total = str($replicas)->explode('/')[1];
+ if ($running === $total) {
+ data_set($container, 'State.Status', 'running');
+ data_set($container, 'State.Health.Status', 'healthy');
+ } else {
+ data_set($container, 'State.Status', 'starting');
+ data_set($container, 'State.Health.Status', 'unhealthy');
+ }
+ }
+
+ return $container;
+ });
+ }
+ }
+ } else {
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
+ $containers = format_docker_command_output_to_json($containers);
+ $containerReplicates = collect([]);
+ }
+
+ return [
+ 'containers' => collect($containers) ?? collect([]),
+ 'containerReplicates' => collect($containerReplicates) ?? collect([]),
+ ];
+ }
+
+ public function getContainersWithSentinel(): Collection
{
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
@@ -604,24 +701,21 @@ $schema://$host {
$containers = data_get(json_decode($containers, true), 'containers', []);
return collect($containers);
- } else {
- if ($this->isSwarm()) {
- $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
- } else {
- $containers = instant_remote_process(['docker container ls -q'], $this, false);
- if (! $containers) {
- return collect([]);
- }
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false);
- }
- if (is_null($containers)) {
- return collect([]);
- }
-
- return format_docker_command_output_to_json($containers);
}
}
+ 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()) {
@@ -668,9 +762,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()
@@ -714,6 +808,33 @@ $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) {
+ return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
+ }
+ );
+ }
+
+ 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(
@@ -744,8 +865,6 @@ $schema://$host {
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
- // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
- // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
return $standalone_docker->concat($swarm_docker);
}
@@ -766,7 +885,7 @@ $schema://$host {
public function muxFilename()
{
- return "{$this->ip}_{$this->port}_{$this->user}";
+ return $this->uuid;
}
public function team()
@@ -776,20 +895,32 @@ $schema://$host {
public function isProxyShouldRun()
{
- if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
+ // TODO: Do we need "|| $this->proxy->force_stop" here?
+ if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
return true;
}
+ public function skipServer()
+ {
+ if ($this->ip === '1.2.3.4') {
+ return true;
+ }
+ if ($this->settings->force_disabled === true) {
+ return true;
+ }
+
+ return false;
+ }
+
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);
+ $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
+
+ if ($isFunctional === false) {
+ Storage::disk('ssh-mux')->delete($this->muxFilename());
}
return $isFunctional;
@@ -806,7 +937,7 @@ $schema://$host {
$releaseLines = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($releaseLines as $line) {
- $item = Str::of($line)->trim();
+ $item = str($line)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
@@ -841,36 +972,110 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection()
+ public function serverStatus(): bool
{
- config()->set('coolify.mux_enabled', false);
-
- $server = Server::find($this->id);
- if (! $server) {
- return ['uptime' => false, 'error' => 'Server not found.'];
+ if ($this->status() === false) {
+ return false;
}
- if ($server->skipServer()) {
+ if ($this->isFunctional() === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function status(): bool
+ {
+ ['uptime' => $uptime] = $this->validateConnection(false);
+ if ($uptime === false) {
+ foreach ($this->applications() as $application) {
+ $application->status = 'exited';
+ $application->save();
+ }
+ foreach ($this->databases() as $database) {
+ $database->status = 'exited';
+ $database->save();
+ }
+ foreach ($this->services() as $service) {
+ $apps = $service->applications()->get();
+ $dbs = $service->databases()->get();
+ foreach ($apps as $app) {
+ $app->status = 'exited';
+ $app->save();
+ }
+ foreach ($dbs as $db) {
+ $db->status = 'exited';
+ $db->save();
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function isReachableChanged()
+ {
+ $this->refresh();
+ $unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
+ $isReachable = (bool) $this->settings->is_reachable;
+ // If the server is reachable, send the reachable notification if it was sent before
+ if ($isReachable === true) {
+ if ($unreachableNotificationSent === true) {
+ $this->sendReachableNotification();
+ }
+ } else {
+ // If the server is unreachable, send the unreachable notification if it was not sent before
+ if ($unreachableNotificationSent === false) {
+ $this->sendUnreachableNotification();
+ }
+ }
+ }
+
+ public function sendReachableNotification()
+ {
+ $this->unreachable_notification_sent = false;
+ $this->save();
+ $this->refresh();
+ $this->team->notify(new Reachable($this));
+ }
+
+ public function sendUnreachableNotification()
+ {
+ $this->unreachable_notification_sent = true;
+ $this->save();
+ $this->refresh();
+ $this->team->notify(new Unreachable($this));
+ }
+
+ public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
+ {
+ config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
+
+ if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
- // EC2 does not have `uptime` command, lol
- instant_remote_process(['ls /'], $server);
- $server->settings()->update([
- 'is_reachable' => true,
- ]);
- $server->update([
- 'unreachable_count' => 0,
- ]);
- if (data_get($server, 'unreachable_notification_sent') === true) {
- // $server->team?->notify(new Revived($server));
- $server->update(['unreachable_notification_sent' => false]);
+ // Make sure the private key is stored
+ if ($this->privateKey) {
+ $this->privateKey->storeInFileSystem();
+ }
+ instant_remote_process(['ls /'], $this);
+ if ($this->settings->is_reachable === false) {
+ $this->settings->is_reachable = true;
+ $this->settings->save();
}
return ['uptime' => true, 'error' => null];
} catch (\Throwable $e) {
- $server->settings()->update([
- 'is_reachable' => false,
- ]);
+ if ($justCheckingNewKey) {
+ return ['uptime' => false, 'error' => 'This key is not valid for this server.'];
+ }
+ if ($this->settings->is_reachable === true) {
+ $this->settings->is_reachable = false;
+ $this->settings->save();
+ }
return ['uptime' => false, 'error' => $e->getMessage()];
}
@@ -878,9 +1083,7 @@ $schema://$host {
public function installDocker()
{
- $activity = InstallDocker::run($this);
-
- return $activity;
+ return InstallDocker::run($this);
}
public function validateDockerEngine($throwError = false)
@@ -991,4 +1194,61 @@ $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(':');
+ }
+
+ public function restartSentinel(bool $async = true)
+ {
+ try {
+ if ($async) {
+ StartSentinel::dispatch($this, true);
+ } else {
+ StartSentinel::run($this, true);
+ }
+ } catch (\Throwable $e) {
+ return handleError($e);
+ }
+ }
+
+ public function url()
+ {
+ return base_url().'/server/'.$this->uuid;
+ }
+
+ public function restartContainer(string $containerName)
+ {
+ return instant_remote_process(['docker restart '.$containerName], $this, false);
+ }
}
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index 9235848ee..fc2c5a0f4 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -2,14 +2,151 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Log;
+use OpenApi\Attributes as OA;
+#[OA\Schema(
+ description: 'Server Settings model',
+ type: 'object',
+ properties: [
+ 'id' => ['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'],
+ 'is_logdrain_axiom_enabled' => ['type' => 'boolean'],
+ 'is_logdrain_custom_enabled' => ['type' => 'boolean'],
+ 'is_logdrain_highlight_enabled' => ['type' => 'boolean'],
+ 'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
+ 'is_metrics_enabled' => ['type' => 'boolean'],
+ 'is_reachable' => ['type' => 'boolean'],
+ 'is_sentinel_enabled' => ['type' => 'boolean'],
+ 'is_swarm_manager' => ['type' => 'boolean'],
+ 'is_swarm_worker' => ['type' => 'boolean'],
+ 'is_usable' => ['type' => 'boolean'],
+ 'logdrain_axiom_api_key' => ['type' => 'string'],
+ 'logdrain_axiom_dataset_name' => ['type' => 'string'],
+ 'logdrain_custom_config' => ['type' => 'string'],
+ 'logdrain_custom_config_parser' => ['type' => 'string'],
+ 'logdrain_highlight_project_id' => ['type' => 'string'],
+ 'logdrain_newrelic_base_uri' => ['type' => 'string'],
+ 'logdrain_newrelic_license_key' => ['type' => 'string'],
+ 'sentinel_metrics_history_days' => ['type' => 'integer'],
+ 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
+ 'sentinel_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'],
+ 'updated_at' => ['type' => 'string'],
+ ]
+)]
class ServerSetting extends Model
{
protected $guarded = [];
+ protected $casts = [
+ 'force_docker_cleanup' => 'boolean',
+ 'docker_cleanup_threshold' => 'integer',
+ 'sentinel_token' => 'encrypted',
+ 'is_reachable' => 'boolean',
+ 'is_usable' => 'boolean',
+ ];
+
+ protected static function booted()
+ {
+ static::creating(function ($setting) {
+ try {
+ if (str($setting->sentinel_token)->isEmpty()) {
+ $setting->generateSentinelToken(save: false, ignoreEvent: true);
+ }
+ if (str($setting->sentinel_custom_url)->isEmpty()) {
+ $setting->generateSentinelUrl(save: false, ignoreEvent: true);
+ }
+ } catch (\Throwable $e) {
+ Log::error('Error creating server setting: '.$e->getMessage());
+ }
+ });
+ static::updated(function ($settings) {
+ if (
+ $settings->isDirty('sentinel_token') ||
+ $settings->isDirty('sentinel_custom_url') ||
+ $settings->isDirty('sentinel_metrics_refresh_rate_seconds') ||
+ $settings->isDirty('sentinel_metrics_history_days') ||
+ $settings->isDirty('sentinel_push_interval_seconds')
+ ) {
+ $settings->server->restartSentinel();
+ }
+ if ($settings->isDirty('is_reachable')) {
+ $settings->server->isReachableChanged();
+ }
+ });
+ }
+
+ public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
+ {
+ $data = [
+ 'server_uuid' => $this->server->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->sentinel_token = $encrypted;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $token;
+ }
+
+ public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
+ {
+ $domain = null;
+ $settings = InstanceSettings::get();
+ if ($this->server->isLocalhost()) {
+ $domain = 'http://host.docker.internal:8000';
+ } elseif ($settings->fqdn) {
+ $domain = $settings->fqdn;
+ } elseif ($settings->public_ipv4) {
+ $domain = 'http://'.$settings->public_ipv4.':8000';
+ } elseif ($settings->public_ipv6) {
+ $domain = 'http://'.$settings->public_ipv6.':8000';
+ }
+ $this->sentinel_custom_url = $domain;
+ if ($save) {
+ if ($ignoreEvent) {
+ $this->saveQuietly();
+ } else {
+ $this->save();
+ }
+ }
+
+ return $domain;
+ }
+
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 7851eb58a..0c9e081a1 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -2,17 +2,60 @@
namespace App\Models;
+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 Visus\Cuid2\Cuid2;
+#[OA\Schema(
+ description: 'Service model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the service. Only used for database identification.'],
+ 'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the service.'],
+ 'name' => ['type' => 'string', 'description' => 'The name of the service.'],
+ 'environment_id' => ['type' => 'integer', 'description' => 'The unique identifier of the environment where the service is attached to.'],
+ 'server_id' => ['type' => 'integer', 'description' => 'The unique identifier of the server where the service is running.'],
+ '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.'],
+ 'is_container_label_readonly_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label readonly.'],
+ 'config_hash' => ['type' => 'string', 'description' => 'The hash of the service configuration.'],
+ 'service_type' => ['type' => 'string', 'description' => 'The type of the service.'],
+ 'created_at' => ['type' => 'string', 'description' => 'The date and time when the service was created.'],
+ 'updated_at' => ['type' => 'string', 'description' => 'The date and time when the service was last updated.'],
+ 'deleted_at' => ['type' => 'string', 'description' => 'The date and time when the service was deleted.'],
+ ],
+)]
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();
@@ -51,6 +94,20 @@ class Service extends BaseModel
}
}
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->server->isFunctional();
+ }
+ );
+ }
+
+ public function isRunning()
+ {
+ return (bool) str($this->status())->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status())->contains('exited');
@@ -76,15 +133,86 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable');
}
+ public static function ownedByCurrentTeam()
+ {
+ return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
+ 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;
@@ -160,9 +288,196 @@ 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('castopod'):
+ $data = collect([]);
+ $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
+ if ($disable_https) {
+ $data = $data->merge([
+ 'Disable HTTPS' => [
+ 'key' => 'CP_DISABLE_HTTPS',
+ 'value' => data_get($disable_https, 'value'),
+ 'rules' => 'required',
+ 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
+ ],
+ ]);
+ }
+ $fields->put('Castopod', $data->toArray());
+ break;
+ 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' => data_get($password, 'key'),
+ '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' => data_get($email, 'key'),
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
+ if ($password) {
+ $data = $data->merge([
+ 'Admin Password' => [
+ 'key' => data_get($password, 'key'),
+ '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' => data_get($email, 'key'),
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ '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([
@@ -176,7 +491,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_TOLGEE',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -185,7 +500,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();
@@ -209,7 +524,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([
@@ -223,7 +538,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_UNLEASH',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -232,7 +547,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([
@@ -246,7 +561,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'GF_SECURITY_ADMIN_PASSWORD',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -255,7 +570,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();
@@ -281,7 +596,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();
@@ -305,7 +620,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();
@@ -358,7 +673,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();
@@ -384,7 +699,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) {
@@ -398,7 +713,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();
@@ -458,33 +773,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();
@@ -550,14 +840,128 @@ class Service extends BaseModel
$fields->put('Vaultwarden', $data);
break;
+ case $image->contains('gitlab/gitlab'):
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first();
+ $data = collect([]);
+ if ($password) {
+ $data = $data->merge([
+ 'Root Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $data = $data->merge([
+ 'Root User' => [
+ 'key' => 'GITLAB_ROOT_USER',
+ 'value' => 'root',
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+
+ $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' => data_get($admin_user, 'key'),
+ 'value' => data_get($admin_user, 'value', 'admin'),
+ 'readonly' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($admin_password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($admin_password, 'key'),
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ if ($admin_email) {
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => data_get($admin_email, 'key'),
+ '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'];
@@ -595,10 +999,10 @@ 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'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
+ case $image->contains('mysql'):
+ $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -645,11 +1049,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();
@@ -739,12 +1143,24 @@ class Service extends BaseModel
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
- return route('project.service.scheduled-tasks', [
+ $route = route('project.service.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'service_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
+ $settings = InstanceSettings::get();
+ if (data_get($settings, 'fqdn')) {
+ $url = Url::fromString($route);
+ $url = $url->withPort(null);
+ $fqdn = data_get($settings, 'fqdn');
+ $fqdn = str_replace(['http://', 'https://'], '', $fqdn);
+ $url = $url->withHost($fqdn);
+
+ return $url->__toString();
+ }
+
+ return $route;
}
return null;
@@ -818,12 +1234,12 @@ 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()
@@ -834,17 +1250,50 @@ class Service extends BaseModel
public function saveComposeConfigs()
{
$workdir = $this->workdir();
- $commands[] = "mkdir -p $workdir";
- $commands[] = "cd $workdir";
- $docker_compose_base64 = base64_encode($this->docker_compose);
- $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
- $envs = $this->environment_variables()->get();
+ 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";
$commands[] = 'rm -f .env || true';
- foreach ($envs as $env) {
- $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+
+ $envs_from_coolify = $this->environment_variables()->get();
+ $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->count() === 0) {
+ if ($sorted->count() === 0) {
$commands[] = 'touch .env';
}
instant_remote_process($commands, $this->server);
@@ -852,14 +1301,33 @@ 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()
{
- $networks = getTopLevelNetworks($this);
+ return getTopLevelNetworks($this);
+ }
- // ray($networks);
- return $networks;
+ protected function isDeployable(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ $envs = $this->environment_variables()->where('is_required', true)->get();
+ foreach ($envs as $env) {
+ if ($env->is_really_required) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ );
}
}
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 98c1cf4e7..5cafc9042 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function restart()
@@ -27,6 +32,26 @@ class ServiceApplication extends BaseModel
instant_remote_process(["docker restart {$container_id}"], $this->service->server);
}
+ public static function ownedByCurrentTeamAPI(int $teamId)
+ {
+ return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->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);
@@ -97,4 +122,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..5fdd52637 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -17,6 +17,21 @@ class ServiceDatabase extends BaseModel
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
+ static::saving(function ($service) {
+ if ($service->isDirty('status')) {
+ $service->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ public static function ownedByCurrentTeamAPI(int $teamId)
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
+ }
+
+ public static function ownedByCurrentTeam()
+ {
+ return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function restart()
@@ -25,6 +40,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 +130,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 c5e252c34..6d66c6854 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneClickhouse extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected $casts = [
'clickhouse_password' => 'encrypted',
];
@@ -29,19 +32,26 @@ class StandaloneClickhouse extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
- $database->scheduledBackups()->delete();
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -70,6 +80,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');
@@ -89,6 +104,17 @@ class StandaloneClickhouse extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -178,18 +204,36 @@ class StandaloneClickhouse extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-clickhouse';
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
- } else {
- return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}";
- }
+ return new Attribute(
+ get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -226,4 +270,53 @@ class StandaloneClickhouse extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 8c739d984..f7d83f0a3 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneDragonfly extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected $casts = [
'dragonfly_password' => 'encrypted',
];
@@ -29,19 +32,26 @@ class StandaloneDragonfly extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $database->scheduledBackups()->delete();
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -70,6 +80,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');
@@ -89,6 +104,17 @@ class StandaloneDragonfly extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -178,18 +204,36 @@ class StandaloneDragonfly extends BaseModel
);
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-dragonfly';
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
- } else {
- return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0";
- }
+ return new Attribute(
+ get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -226,4 +270,53 @@ class StandaloneDragonfly extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 5216681c9..083c743d9 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneKeydb extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
+
protected $casts = [
'keydb_password' => 'encrypted',
];
@@ -29,19 +32,26 @@ class StandaloneKeydb extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $database->scheduledBackups()->delete();
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -70,6 +80,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');
@@ -89,6 +104,17 @@ class StandaloneKeydb extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -178,18 +204,36 @@ class StandaloneKeydb extends BaseModel
);
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-keydb';
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
- } else {
- return "redis://{$this->keydb_password}@{$this->uuid}:6379/0";
- }
+ return new Attribute(
+ get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ 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 null;
+ }
+ );
}
public function environment()
@@ -226,4 +270,53 @@ class StandaloneKeydb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 33fd2cbc2..833dad6c4 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneMariadb extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected $casts = [
'mariadb_password' => 'encrypted',
];
@@ -29,19 +32,26 @@ class StandaloneMariadb extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
- $database->scheduledBackups()->delete();
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -70,6 +80,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');
@@ -89,6 +104,17 @@ class StandaloneMariadb extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -161,6 +187,13 @@ class StandaloneMariadb extends BaseModel
return data_get($this, 'is_log_drain_enabled', false);
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-mariadb';
@@ -183,13 +216,24 @@ class StandaloneMariadb extends BaseModel
);
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
- } else {
- return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
- }
+ return new Attribute(
+ get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -226,4 +270,53 @@ class StandaloneMariadb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 0cc52b3f7..dd8893180 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneMongodb extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected static function booted()
{
static::created(function ($database) {
@@ -33,19 +36,26 @@ class StandaloneMongodb extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
- $database->scheduledBackups()->delete();
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -74,6 +84,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');
@@ -93,6 +108,17 @@ class StandaloneMongodb extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -198,18 +224,36 @@ class StandaloneMongodb extends BaseModel
);
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-mongodb';
}
- public function get_db_url(bool $useInternal = false)
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
- } else {
- return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
- }
+ return new Attribute(
+ get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -246,4 +290,53 @@ class StandaloneMongodb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 174736f77..710fea1bc 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneMysql extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected $casts = [
'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted',
@@ -30,19 +33,26 @@ class StandaloneMysql extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
- $database->scheduledBackups()->delete();
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -71,6 +81,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');
@@ -90,6 +105,17 @@ class StandaloneMysql extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -157,6 +183,13 @@ class StandaloneMysql extends BaseModel
return null;
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-mysql';
@@ -184,13 +217,24 @@ class StandaloneMysql extends BaseModel
);
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
- } else {
- return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}";
- }
+ return new Attribute(
+ get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -227,4 +271,53 @@ class StandaloneMysql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index a5bf4dc2a..4a457a6cf 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandalonePostgresql extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected $casts = [
'init_scripts' => 'array',
'postgres_password' => 'encrypted',
@@ -30,19 +33,17 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
- $database->scheduledBackups()->delete();
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
}
public function workdir()
@@ -50,6 +51,15 @@ class StandalonePostgresql extends BaseModel
return database_configuration_dir()."/{$this->uuid}";
}
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
+ }
+
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
@@ -59,6 +69,17 @@ class StandalonePostgresql extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
@@ -85,6 +106,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');
@@ -179,18 +205,36 @@ class StandalonePostgresql extends BaseModel
return data_get($this, 'environment.project.team');
}
+ public function databaseType(): Attribute
+ {
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
public function type(): string
{
return 'standalone-postgresql';
}
- public function get_db_url(bool $useInternal = false): string
+ protected function internalDbUrl(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
- } else {
- return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
- }
+ return new Attribute(
+ get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}",
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
+ }
+
+ return null;
+ }
+ );
}
public function environment()
@@ -227,4 +271,53 @@ class StandalonePostgresql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ed379750e..826bb951c 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -13,6 +14,8 @@ class StandaloneRedis extends BaseModel
protected $guarded = [];
+ protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+
protected static function booted()
{
static::created(function ($database) {
@@ -25,19 +28,26 @@ class StandaloneRedis extends BaseModel
'is_readonly' => true,
]);
});
- static::deleting(function ($database) {
- $database->scheduledBackups()->delete();
- $storages = $database->persistentStorages()->get();
- $server = data_get($database, 'destination.server');
- if ($server) {
- foreach ($storages as $storage) {
- instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
- }
- }
+ static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
+ $database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
+ static::saving(function ($database) {
+ if ($database->isDirty('status')) {
+ $database->forceFill(['last_online_at' => now()]);
+ }
+ });
+ }
+
+ protected function serverStatus(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ return $this->destination->server->isFunctional();
+ }
+ );
}
public function isConfigurationChanged(bool $save = false)
@@ -66,6 +76,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');
@@ -85,6 +100,17 @@ class StandaloneRedis extends BaseModel
}
}
+ public function delete_volumes(Collection $persistentStorages)
+ {
+ if ($persistentStorages->count() === 0) {
+ return;
+ }
+ $server = data_get($this, 'destination.server');
+ foreach ($persistentStorages as $storage) {
+ instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
+ }
+ }
+
public function realStatus()
{
return $this->getRawOriginal('status');
@@ -179,13 +205,46 @@ class StandaloneRedis extends BaseModel
return 'standalone-redis';
}
- public function get_db_url(bool $useInternal = false): string
+ public function databaseType(): Attribute
{
- if ($this->is_public && ! $useInternal) {
- return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
- } else {
- return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
- }
+ return new Attribute(
+ get: fn () => $this->type(),
+ );
+ }
+
+ protected function internalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ }
+ );
+ }
+
+ protected function externalDbUrl(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ if ($this->is_public && $this->public_port) {
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ }
+
+ return null;
+ }
+ );
+ }
+
+ public function getRedisVersion()
+ {
+ $image_parts = explode(':', $this->image);
+
+ return $image_parts[1] ?? '0.0';
}
public function environment()
@@ -222,4 +281,82 @@ class StandaloneRedis extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getCpuMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error === 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
+ });
+
+ return $parsedCollection->toArray();
+ }
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
+
+ public function redisPassword(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
+ if (! $password) {
+ return null;
+ }
+
+ return $password->value;
+ },
+
+ );
+ }
+
+ public function redisUsername(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
+ if (! $username) {
+ return null;
+ }
+
+ return $username->value;
+ }
+ );
+ }
}
diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php
index 35dc43c0c..1bd84a664 100644
--- a/app/Models/Subscription.php
+++ b/app/Models/Subscription.php
@@ -15,22 +15,7 @@ class Subscription extends Model
public function type()
{
- if (isLemon()) {
- $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
- $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
- $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
-
- $subscription = $this->lemon_variant_id;
- if (in_array($subscription, $basic)) {
- return 'basic';
- }
- if (in_array($subscription, $pro)) {
- return 'pro';
- }
- if (in_array($subscription, $ultimate)) {
- return 'ultimate';
- }
- } elseif (isStripe()) {
+ if (isStripe()) {
if (! $this->stripe_plan_id) {
return 'zero';
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index fe5995a1b..8996b745c 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -7,7 +7,69 @@ use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
+use OpenApi\Attributes as OA;
+#[OA\Schema(
+ description: 'Team model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the team.'],
+ 'name' => ['type' => 'string', 'description' => 'The name of the team.'],
+ 'description' => ['type' => 'string', 'description' => 'The description of the team.'],
+ 'personal_team' => ['type' => 'boolean', 'description' => 'Whether the team is personal or not.'],
+ 'created_at' => ['type' => 'string', 'description' => 'The date and time the team was created.'],
+ 'updated_at' => ['type' => 'string', 'description' => 'The date and time the team was last updated.'],
+ 'smtp_enabled' => ['type' => 'boolean', 'description' => 'Whether SMTP is enabled or not.'],
+ 'smtp_from_address' => ['type' => 'string', 'description' => 'The email address to send emails from.'],
+ 'smtp_from_name' => ['type' => 'string', 'description' => 'The name to send emails from.'],
+ 'smtp_recipients' => ['type' => 'string', 'description' => 'The email addresses to send emails to.'],
+ 'smtp_host' => ['type' => 'string', 'description' => 'The SMTP host.'],
+ 'smtp_port' => ['type' => 'string', 'description' => 'The SMTP port.'],
+ 'smtp_encryption' => ['type' => 'string', 'description' => 'The SMTP encryption.'],
+ 'smtp_username' => ['type' => 'string', 'description' => 'The SMTP username.'],
+ 'smtp_password' => ['type' => 'string', 'description' => 'The SMTP password.'],
+ 'smtp_timeout' => ['type' => 'string', 'description' => 'The SMTP timeout.'],
+ 'smtp_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via SMTP.'],
+ 'smtp_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via SMTP.'],
+ 'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'],
+ 'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'],
+ 'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'],
+ 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'],
+ 'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'],
+ 'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'],
+ 'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'],
+ 'discord_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Discord.'],
+ 'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'],
+ 'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'],
+ 'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'],
+ 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'],
+ 'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
+ 'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'],
+ 'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'],
+ 'use_instance_email_settings' => ['type' => 'boolean', 'description' => 'Whether to use instance email settings or not.'],
+ 'telegram_enabled' => ['type' => 'boolean', 'description' => 'Whether Telegram is enabled or not.'],
+ 'telegram_token' => ['type' => 'string', 'description' => 'The Telegram token.'],
+ 'telegram_chat_id' => ['type' => 'string', 'description' => 'The Telegram chat ID.'],
+ 'telegram_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Telegram.'],
+ 'telegram_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Telegram.'],
+ 'telegram_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Telegram.'],
+ 'telegram_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Telegram.'],
+ 'telegram_notifications_test_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram test message thread ID.'],
+ 'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'],
+ 'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'],
+ 'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'],
+
+ 'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
+ 'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
+ 'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
+ 'members' => new OA\Property(
+ property: 'members',
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/User'),
+ description: 'The members of the team.'
+ ),
+ ]
+)]
class Team extends Model implements SendsDiscord, SendsEmail
{
use Notifiable;
@@ -31,27 +93,22 @@ class Team extends Model implements SendsDiscord, SendsEmail
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
- ray('Deleting key: '.$key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
- ray('Deleting source: '.$source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
- ray('Deleting tag: '.$tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
- ray('Deleting team shared variable: '.$shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
- ray('Deleting s3: '.$s3->name);
$s3->delete();
}
});
@@ -74,9 +131,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
$recipients = data_get($notification, 'emails', null);
if (is_null($recipients)) {
- $recipients = $this->members()->pluck('email')->toArray();
-
- return $recipients;
+ return $this->members()->pluck('email')->toArray();
}
return explode(',', $recipients);
@@ -105,8 +160,12 @@ class Team extends Model implements SendsDiscord, SendsEmail
if (currentTeam()->id === 0 && isDev()) {
return 9999999;
}
+ $team = Team::find(currentTeam()->id);
+ if (! $team) {
+ return 0;
+ }
- return Team::find(currentTeam()->id)->limits['serverLimit'];
+ return data_get($team, 'limits', 0);
}
public function limits(): Attribute
@@ -128,9 +187,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
- $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
- return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
+ return $serverLimit ?? 2;
}
);
@@ -190,9 +248,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
$sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
- $sources = $sources->merge($github_apps)->merge($gitlab_apps);
- return $sources;
+ return $sources->merge($github_apps)->merge($gitlab_apps);
}
public function s3s()
@@ -200,8 +257,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
return $this->hasMany(S3Storage::class)->where('is_usable', true);
}
- public function trialEnded()
+ public function subscriptionEnded()
{
+ $this->subscription->update([
+ 'stripe_subscription_id' => null,
+ 'stripe_plan_id' => null,
+ 'stripe_cancel_at_period_end' => false,
+ 'stripe_invoice_paid' => false,
+ 'stripe_trial_already_ended' => false,
+ ]);
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
@@ -210,16 +274,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
}
- public function trialEndedButSubscribed()
- {
- foreach ($this->servers as $server) {
- $server->settings()->update([
- 'is_usable' => true,
- 'is_reachable' => true,
- ]);
- }
- }
-
public function isAnyNotificationEnabled()
{
if (isCloud()) {
diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php
index c202710e2..bc1a90d58 100644
--- a/app/Models/TeamInvitation.php
+++ b/app/Models/TeamInvitation.php
@@ -20,11 +20,16 @@ class TeamInvitation extends Model
return $this->belongsTo(Team::class);
}
+ public static function ownedByCurrentTeam()
+ {
+ return TeamInvitation::whereTeamId(currentTeam()->id);
+ }
+
public function isValid()
{
$createdAt = $this->created_at;
- $diff = $createdAt->diffInMinutes(now());
- if ($diff <= config('constants.invitation.link.expiration')) {
+ $diff = $createdAt->diffInDays(now());
+ if ($diff <= config('constants.invitation.link.expiration_days')) {
return true;
} else {
$this->delete();
diff --git a/app/Models/User.php b/app/Models/User.php
index 1e120e951..25fb33d66 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
@@ -17,7 +18,23 @@ use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\NewAccessToken;
+use OpenApi\Attributes as OA;
+#[OA\Schema(
+ description: 'User model',
+ type: 'object',
+ properties: [
+ 'id' => ['type' => 'integer', 'description' => 'The user identifier in the database.'],
+ 'name' => ['type' => 'string', 'description' => 'The user name.'],
+ 'email' => ['type' => 'string', 'description' => 'The user email.'],
+ 'email_verified_at' => ['type' => 'string', 'description' => 'The date when the user email was verified.'],
+ 'created_at' => ['type' => 'string', 'description' => 'The date when the user was created.'],
+ 'updated_at' => ['type' => 'string', 'description' => 'The date when the user was updated.'],
+ 'two_factor_confirmed_at' => ['type' => 'string', 'description' => 'The date when the user two factor was confirmed.'],
+ 'force_password_reset' => ['type' => 'boolean', 'description' => 'The flag to force the user to reset the password.'],
+ 'marketing_emails' => ['type' => 'boolean', 'description' => 'The flag to receive marketing emails.'],
+ ],
+)]
class User extends Authenticatable implements SendsEmail
{
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
@@ -104,7 +121,7 @@ class User extends Authenticatable implements SendsEmail
public function sendVerificationEmail()
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$url = Url::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
@@ -142,7 +159,7 @@ class User extends Authenticatable implements SendsEmail
public function isAdminFromSession()
{
- if (auth()->user()->id === 0) {
+ if (Auth::id() === 0) {
return true;
}
$teams = $this->teams()->get();
@@ -162,8 +179,12 @@ class User extends Authenticatable implements SendsEmail
public function isInstanceAdmin()
{
- $found_root_team = auth()->user()->teams->filter(function ($team) {
+ $found_root_team = Auth::user()->teams->filter(function ($team) {
if ($team->id == 0) {
+ if (! Auth::user()->isAdmin()) {
+ return false;
+ }
+
return true;
}
@@ -175,9 +196,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
- return Cache::remember('team:'.auth()->user()->id, 3600, function () {
- if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) {
- return auth()->user()->teams[0];
+ return Cache::remember('team:'.Auth::id(), 3600, function () {
+ if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
+ return Auth::user()->teams[0];
}
return Team::find(session('currentTeam')->id);
@@ -186,7 +207,7 @@ class User extends Authenticatable implements SendsEmail
public function otherTeams()
{
- return auth()->user()->teams->filter(function ($team) {
+ return Auth::user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
@@ -196,7 +217,7 @@ class User extends Authenticatable implements SendsEmail
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
- $user = auth()->user()->teams->where('id', currentTeam()->id)->first();
+ $user = Auth::user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index 1858f31e0..242980e00 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -4,11 +4,11 @@ namespace App\Notifications\Application;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Str;
class DeploymentFailed extends Notification implements ShouldQueue
{
@@ -41,8 +41,8 @@ class DeploymentFailed extends Notification implements ShouldQueue
$this->project_uuid = data_get($application, 'environment.project.uuid');
$this->environment_name = data_get($application, 'environment.name');
$this->fqdn = data_get($application, 'fqdn');
- if (Str::of($this->fqdn)->explode(',')->count() > 1) {
- $this->fqdn = Str::of($this->fqdn)->explode(',')->first();
+ if (str($this->fqdn)->explode(',')->count() > 1) {
+ $this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
@@ -54,7 +54,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
@@ -73,14 +73,42 @@ class DeploymentFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
+ if ($this->fqdn) {
+ $message->addField('Domain', $this->fqdn, true);
+ }
} else {
- $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): ';
- $message .= '[View Deployment Logs]('.$this->deployment_url.')';
+ if ($this->fqdn) {
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
+ }
+ $message = new DiscordMessage(
+ title: ':cross_mark: Deployment failed',
+ description: $description,
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')');
}
return $message;
diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php
index 0cac6cbab..946a622ca 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -4,11 +4,11 @@ namespace App\Notifications\Application;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Str;
class DeploymentSuccess extends Notification implements ShouldQueue
{
@@ -41,8 +41,8 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$this->project_uuid = data_get($application, 'environment.project.uuid');
$this->environment_name = data_get($application, 'environment.name');
$this->fqdn = data_get($application, 'fqdn');
- if (Str::of($this->fqdn)->explode(',')->count() > 1) {
- $this->fqdn = Str::of($this->fqdn)->explode(',')->first();
+ if (str($this->fqdn)->explode(',')->count() > 1) {
+ $this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
@@ -52,7 +52,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
// TODO: Make batch notifications work with email
- $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']);
+ $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]);
}
return $channels;
@@ -60,7 +60,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
@@ -79,24 +79,39 @@ class DeploymentSuccess extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
if ($this->preview) {
- $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.'
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Preview deployment successful',
+ description: 'Pull request: '.$this->preview->pull_request_id,
+ color: DiscordMessage::successColor(),
+ );
-';
if ($this->preview->fqdn) {
- $message .= '[Open Application]('.$this->preview->fqdn.') | ';
+ $message->addField('Application', '[Link]('.$this->preview->fqdn.')');
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
- } else {
- $message = 'Coolify: New version successfully deployed of '.$this->application_name.'
-';
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+ $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
+ } else {
if ($this->fqdn) {
- $message .= '[Open Application]('.$this->fqdn.') | ';
+ $description = '[Open application]('.$this->fqdn.')';
+ } else {
+ $description = '';
}
- $message .= '[Deployment logs]('.$this->deployment_url.')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: New version successfully deployed',
+ description: $description,
+ color: DiscordMessage::successColor(),
+ );
+ $message->addField('Project', data_get($this->application, 'environment.project.name'), true);
+ $message->addField('Environment', $this->environment_name, true);
+ $message->addField('Name', $this->application_name, true);
+
+ $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')');
}
return $message;
diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php
index baf508895..852c6b526 100644
--- a/app/Notifications/Application/StatusChanged.php
+++ b/app/Notifications/Application/StatusChanged.php
@@ -3,11 +3,11 @@
namespace App\Notifications\Application;
use App\Models\Application;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-use Illuminate\Support\Str;
class StatusChanged extends Notification implements ShouldQueue
{
@@ -31,8 +31,8 @@ class StatusChanged extends Notification implements ShouldQueue
$this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->environment_name = data_get($resource, 'environment.name');
$this->fqdn = data_get($resource, 'fqdn', null);
- if (Str::of($this->fqdn)->explode(',')->count() > 1) {
- $this->fqdn = Str::of($this->fqdn)->explode(',')->first();
+ if (str($this->fqdn)->explode(',')->count() > 1) {
+ $this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}";
}
@@ -44,7 +44,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$fqdn = $this->fqdn;
$mail->subject("Coolify: {$this->resource_name} has been stopped");
$mail->view('emails.application-status-changes', [
@@ -56,14 +56,14 @@ class StatusChanged extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: '.$this->resource_name.' has been stopped.
-
-';
- $message .= '[Open Application in Coolify]('.$this->resource_url.')';
-
- return $message;
+ return new DiscordMessage(
+ title: ':cross_mark: Application stopped',
+ description: '[Open Application in Coolify]('.$this->resource_url.')',
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index f1706f138..86276fec9 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -12,11 +12,11 @@ class DiscordChannel
*/
public function send(SendsDiscord $notifiable, Notification $notification): void
{
- $message = $notification->toDiscord($notifiable);
+ $message = $notification->toDiscord();
$webhookUrl = $notifiable->routeNotificationForDiscord();
if (! $webhookUrl) {
return;
}
- dispatch(new SendMessageToDiscordJob($message, $webhookUrl));
+ dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high');
}
}
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 413d3de53..af9af978d 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -32,7 +32,6 @@ class EmailChannel
if ($error === 'No email settings found.') {
throw $e;
}
- ray($e->getMessage());
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recipients)) {
$message .= implode(', ', $recipients);
diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php
index b1a607651..b3d4e384b 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -18,29 +18,29 @@ class TelegramChannel
$topicsInstance = get_class($notification);
switch ($topicsInstance) {
- case 'App\Notifications\Test':
+ case \App\Notifications\Test::class:
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
- case 'App\Notifications\Application\StatusChanged':
- case 'App\Notifications\Container\ContainerRestarted':
- case 'App\Notifications\Container\ContainerStopped':
+ case \App\Notifications\Application\StatusChanged::class:
+ case \App\Notifications\Container\ContainerRestarted::class:
+ case \App\Notifications\Container\ContainerStopped::class:
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
- case 'App\Notifications\Application\DeploymentSuccess':
- case 'App\Notifications\Application\DeploymentFailed':
+ case \App\Notifications\Application\DeploymentSuccess::class:
+ case \App\Notifications\Application\DeploymentFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
- case 'App\Notifications\Database\BackupSuccess':
- case 'App\Notifications\Database\BackupFailed':
+ case \App\Notifications\Database\BackupSuccess::class:
+ case \App\Notifications\Database\BackupFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
- case 'App\Notifications\ScheduledTask\TaskFailed':
+ case \App\Notifications\ScheduledTask\TaskFailed::class:
$topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
break;
}
if (! $telegramToken || ! $chatId || ! $message) {
return;
}
- dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
+ dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high');
}
}
diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php
index 3d7b7c8d0..cc7d76ebf 100644
--- a/app/Notifications/Channels/TransactionalEmailChannel.php
+++ b/app/Notifications/Channels/TransactionalEmailChannel.php
@@ -2,7 +2,6 @@
namespace App\Notifications\Channels;
-use App\Models\InstanceSettings;
use App\Models\User;
use Exception;
use Illuminate\Mail\Message;
@@ -14,7 +13,7 @@ class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
- $settings = 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/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index a55f16a83..182a1f5fc 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -14,9 +15,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public string $name, public Server $server, public ?string $url = null)
- {
- }
+ public function __construct(public string $name, public Server $server, public ?string $url = null) {}
public function via(object $notifiable): array
{
@@ -25,7 +24,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
@@ -36,9 +35,17 @@ class ContainerRestarted extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':warning: Resource restarted',
+ description: "{$this->name} has been restarted automatically on {$this->server->name}.",
+ color: DiscordMessage::infoColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php
index d9dc57b98..33a55c65a 100644
--- a/app/Notifications/Container/ContainerStopped.php
+++ b/app/Notifications/Container/ContainerStopped.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Container;
use App\Models\Server;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -14,9 +15,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public string $name, public Server $server, public ?string $url = null)
- {
- }
+ public function __construct(public string $name, public Server $server, public ?string $url = null) {}
public function via(object $notifiable): array
{
@@ -25,7 +24,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: A resource has been stopped unexpectedly on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
@@ -36,9 +35,17 @@ class ContainerStopped extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Resource stopped',
+ description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Resource', '[Link]('.$this->url.')');
+ }
return $message;
}
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index c6403ab71..8e2733339 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -33,7 +34,7 @@ class BackupFailed extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
@@ -45,9 +46,19 @@ class BackupFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Database backup failed',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+ $message->addField('Output', $this->output);
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index f3a3d5943..5128c8ed6 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -3,6 +3,7 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -33,7 +34,7 @@ class BackupSuccess extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Backup successfully done for {$this->database->name}");
$mail->view('emails.backup-success', [
'name' => $this->name,
@@ -44,15 +45,22 @@ class BackupSuccess extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Database backup successful',
+ description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.",
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField('Frequency', $this->frequency, true);
+
+ return $message;
}
public function toTelegram(): array
{
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
- ray($message);
return [
'message' => $message,
diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php
deleted file mode 100644
index c74676eb7..000000000
--- a/app/Notifications/Database/DailyBackup.php
+++ /dev/null
@@ -1,52 +0,0 @@
-subject('Coolify: Daily backup statuses');
- $mail->view('emails.daily-backup', [
- 'databases' => $this->databases,
- ]);
-
- return $mail;
- }
-
- public function toDiscord(): string
- {
- return 'Coolify: Daily backup statuses';
- }
-
- public function toTelegram(): array
- {
- $message = 'Coolify: Daily backup statuses';
-
- return [
- 'message' => $message,
- ];
- }
-}
diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php
new file mode 100644
index 000000000..856753dca
--- /dev/null
+++ b/app/Notifications/Dto/DiscordMessage.php
@@ -0,0 +1,83 @@
+fields[] = [
+ 'name' => $name,
+ 'value' => $value,
+ 'inline' => $inline,
+ ];
+
+ return $this;
+ }
+
+ public function toPayload(): array
+ {
+ $footerText = 'Coolify v'.config('version');
+ if (isCloud()) {
+ $footerText = 'Coolify Cloud';
+ }
+ $payload = [
+ 'embeds' => [
+ [
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'color' => $this->color,
+ 'fields' => $this->addTimestampToFields($this->fields),
+ 'footer' => [
+ 'text' => $footerText,
+ ],
+ ],
+ ],
+ ];
+ if ($this->isCritical) {
+ $payload['content'] = '@here';
+ }
+
+ return $payload;
+ }
+
+ private function addTimestampToFields(array $fields): array
+ {
+ $fields[] = [
+ 'name' => 'Time',
+ 'value' => 'timestamp.':R>',
+ 'inline' => true,
+ ];
+
+ return $fields;
+ }
+}
diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php
index 6acd770f6..48e7d8340 100644
--- a/app/Notifications/Internal/GeneralNotification.php
+++ b/app/Notifications/Internal/GeneralNotification.php
@@ -4,6 +4,7 @@ namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -14,9 +15,7 @@ class GeneralNotification extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public string $message)
- {
- }
+ public function __construct(public string $message) {}
public function via(object $notifiable): array
{
@@ -34,9 +33,13 @@ class GeneralNotification extends Notification implements ShouldQueue
return $channels;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return $this->message;
+ return new DiscordMessage(
+ title: 'Coolify: General Notification',
+ description: $this->message,
+ color: DiscordMessage::infoColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
index 3a41fb687..c3501a8eb 100644
--- a/app/Notifications/ScheduledTask/TaskFailed.php
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -3,6 +3,7 @@
namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -29,13 +30,12 @@ class TaskFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
-
return setNotificationChannels($notifiable, 'scheduled_tasks');
}
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed.");
$mail->view('emails.scheduled-task-failed', [
'task' => $this->task,
@@ -46,9 +46,19 @@ class TaskFailed extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Scheduled task failed',
+ description: "Scheduled task ({$this->task->name}) failed.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ if ($this->url) {
+ $message->addField('Scheduled task', '[Link]('.$this->url.')');
+ }
+
+ return $message;
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php
index 0e445f035..7ea1b84c2 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -15,9 +16,7 @@ class DockerCleanup extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server, public string $message)
- {
- }
+ public function __construct(public Server $server, public string $message) {}
public function via(object $notifiable): array
{
@@ -46,16 +45,18 @@ 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;
// }
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server cleanup job done',
+ description: $this->message,
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index 960a7c79f..a26c803ee 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,9 +18,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server)
- {
- }
+ public function __construct(public Server $server) {}
public function via(object $notifiable): array
{
@@ -43,7 +42,7 @@ class ForceDisabled extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
$mail->view('emails.server-force-disabled', [
'name' => $this->server->name,
@@ -52,9 +51,15 @@ class ForceDisabled extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $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 = new DiscordMessage(
+ title: ':cross_mark: Server disabled',
+ description: "Server ({$this->server->name}) disabled because it is not paid!",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)');
return $message;
}
@@ -62,7 +67,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/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php
index 6a4b5d74b..65b65a10c 100644
--- a/app/Notifications/Server/ForceEnabled.php
+++ b/app/Notifications/Server/ForceEnabled.php
@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,9 +18,7 @@ class ForceEnabled extends Notification implements ShouldQueue
public $tries = 1;
- public function __construct(public Server $server)
- {
- }
+ public function __construct(public Server $server) {}
public function via(object $notifiable): array
{
@@ -43,7 +42,7 @@ class ForceEnabled extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
$mail->view('emails.server-force-enabled', [
'name' => $this->server->name,
@@ -52,11 +51,13 @@ class ForceEnabled extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server ({$this->server->name}) enabled again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ':white_check_mark: Server enabled',
+ description: "Server '{$this->server->name}' enabled again!",
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php
index 5f63ef8f1..e373abc03 100644
--- a/app/Notifications/Server/HighDiskUsage.php
+++ b/app/Notifications/Server/HighDiskUsage.php
@@ -3,9 +3,7 @@
namespace App\Notifications\Server;
use App\Models\Server;
-use App\Notifications\Channels\DiscordChannel;
-use App\Notifications\Channels\EmailChannel;
-use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,46 +15,39 @@ 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 $server_disk_usage_notification_threshold) {}
public function via(object $notifiable): array
{
- $channels = [];
- $isEmailEnabled = isEmailEnabled($notifiable);
- $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
- $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
- if ($isDiscordEnabled) {
- $channels[] = DiscordChannel::class;
- }
- if ($isEmailEnabled) {
- $channels[] = EmailChannel::class;
- }
- if ($isTelegramEnabled) {
- $channels[] = TelegramChannel::class;
- }
-
- return $channels;
+ return setNotificationChannels($notifiable, 'server_disk_usage');
}
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Server ({$this->server->name}) high disk usage detected!");
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
- 'threshold' => $this->cleanup_after_percentage,
+ 'threshold' => $this->server_disk_usage_notification_threshold,
]);
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $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 = new DiscordMessage(
+ title: ':cross_mark: High disk usage detected',
+ description: "Server '{$this->server->name}' high disk usage detected!",
+ color: DiscordMessage::errorColor(),
+ isCritical: true,
+ );
+
+ $message->addField('Disk usage', "{$this->disk_usage}%", true);
+ $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true);
+ $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true);
+ $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)');
return $message;
}
@@ -64,7 +55,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->server_disk_usage_notification_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/Server/Revived.php b/app/Notifications/Server/Reachable.php
similarity index 70%
rename from app/Notifications/Server/Revived.php
rename to app/Notifications/Server/Reachable.php
index e7d3baf3e..9b54501d9 100644
--- a/app/Notifications/Server/Revived.php
+++ b/app/Notifications/Server/Reachable.php
@@ -2,34 +2,37 @@
namespace App\Notifications\Server;
-use App\Actions\Docker\GetContainersStatus;
-use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
-class Revived extends Notification implements ShouldQueue
+class Reachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
+ protected bool $isRateLimited = false;
+
public function __construct(public Server $server)
{
- if ($this->server->unreachable_notification_sent === false) {
- return;
- }
- GetContainersStatus::dispatch($server);
- // dispatch(new ContainerStatusJob($server));
+ $this->isRateLimited = isEmailRateLimited(
+ limiterKey: 'server-reachable:'.$this->server->id,
+ );
}
public function via(object $notifiable): array
{
+ if ($this->isRateLimited) {
+ return [];
+ }
+
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -50,7 +53,7 @@ class Revived extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Server ({$this->server->name}) revived.");
$mail->view('emails.server-revived', [
'name' => $this->server->name,
@@ -59,11 +62,13 @@ class Revived extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
-
- return $message;
+ return new DiscordMessage(
+ title: ":white_check_mark: Server '{$this->server->name}' revived",
+ description: 'All automations & integrations are turned on again!',
+ color: DiscordMessage::successColor(),
+ );
}
public function toTelegram(): array
diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php
index 2dcfe28b8..5bc568e82 100644
--- a/app/Notifications/Server/Unreachable.php
+++ b/app/Notifications/Server/Unreachable.php
@@ -6,6 +6,7 @@ use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -17,13 +18,21 @@ class Unreachable extends Notification implements ShouldQueue
public $tries = 1;
+ protected bool $isRateLimited = false;
+
public function __construct(public Server $server)
{
-
+ $this->isRateLimited = isEmailRateLimited(
+ limiterKey: 'server-unreachable:'.$this->server->id,
+ );
}
public function via(object $notifiable): array
{
+ if ($this->isRateLimited) {
+ return [];
+ }
+
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -42,9 +51,9 @@ class Unreachable extends Notification implements ShouldQueue
return $channels;
}
- public function toMail(): MailMessage
+ public function toMail(): ?MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject("Coolify: Your server ({$this->server->name}) is unreachable.");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
@@ -53,14 +62,20 @@ class Unreachable extends Notification implements ShouldQueue
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): ?DiscordMessage
{
- $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.";
+ $message = new DiscordMessage(
+ title: ':cross_mark: Server unreachable',
+ description: "Your server '{$this->server->name}' is unreachable.",
+ color: DiscordMessage::errorColor(),
+ );
+
+ $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.');
return $message;
}
- public function toTelegram(): array
+ public function toTelegram(): ?array
{
return [
'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.",
diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php
index 925859aba..a43b1e153 100644
--- a/app/Notifications/Test.php
+++ b/app/Notifications/Test.php
@@ -2,10 +2,12 @@
namespace App\Notifications;
+use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Queue\Middleware\RateLimited;
class Test extends Notification implements ShouldQueue
{
@@ -13,29 +15,39 @@ class Test extends Notification implements ShouldQueue
public $tries = 5;
- public function __construct(public ?string $emails = null)
- {
- }
+ public function __construct(public ?string $emails = null) {}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'test');
}
+ public function middleware(object $notifiable, string $channel)
+ {
+ return match ($channel) {
+ \App\Notifications\Channels\EmailChannel::class => [new RateLimited('email')],
+ default => [],
+ };
+ }
+
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject('Coolify: Test Email');
$mail->view('emails.test');
return $mail;
}
- public function toDiscord(): string
+ public function toDiscord(): DiscordMessage
{
- $message = 'Coolify: This is a test Discord notification from Coolify.';
- $message .= "\n\n";
- $message .= '[Go to your dashboard]('.base_url().')';
+ $message = new DiscordMessage(
+ title: ':white_check_mark: Test Success',
+ description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:',
+ color: DiscordMessage::successColor(),
+ );
+
+ $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true);
return $message;
}
diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php
index a251b47ea..6da2a6fcc 100644
--- a/app/Notifications/TransactionalEmails/InvitationLink.php
+++ b/app/Notifications/TransactionalEmails/InvitationLink.php
@@ -22,16 +22,14 @@ class InvitationLink extends Notification implements ShouldQueue
return [TransactionalEmailChannel::class];
}
- public function __construct(public User $user)
- {
- }
+ public function __construct(public User $user) {}
public function toMail(): MailMessage
{
$invitation = TeamInvitation::whereEmail($this->user->email)->first();
$invitation_team = Team::find($invitation->team->id);
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject('Coolify: Invitation for '.$invitation_team->name);
$mail->view('emails.invitation-link', [
'team' => $invitation_team->name,
diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php
index 45243c4d5..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 = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->token = $token;
}
@@ -53,7 +53,7 @@ class ResetPassword extends Notification
protected function buildMailMessage($url)
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject('Coolify: Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]);
diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php
index ed30c1883..64883a58e 100644
--- a/app/Notifications/TransactionalEmails/Test.php
+++ b/app/Notifications/TransactionalEmails/Test.php
@@ -14,9 +14,7 @@ class Test extends Notification implements ShouldQueue
public $tries = 5;
- public function __construct(public string $emails)
- {
- }
+ public function __construct(public string $emails) {}
public function via(): array
{
@@ -25,7 +23,7 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
- $mail = new MailMessage();
+ $mail = new MailMessage;
$mail->subject('Coolify: Test Email');
$mail->view('emails.test');
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 1bce22c12..015434bd2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -5,17 +5,30 @@ namespace App\Providers;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
+ if ($this->app->environment('local')) {
+ $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
+ }
}
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
+
+ Password::defaults(function () {
+ $rule = Password::min(8);
+
+ return $this->app->isProduction()
+ ? $rule->mixedCase()->letters()->numbers()->symbols()
+ : $rule;
+ });
+
Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php
new file mode 100644
index 000000000..07e0e8709
--- /dev/null
+++ b/app/Providers/DuskServiceProvider.php
@@ -0,0 +1,21 @@
+visit('/login')
+ ->type('email', 'test@example.com')
+ ->type('password', 'password')
+ ->press('Login');
+ });
+ }
+}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index cd6ec7705..e8784bab3 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -6,7 +6,6 @@ use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
-use App\Models\InstanceSettings;
use App\Models\OauthSetting;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
@@ -45,19 +44,23 @@ class FortifyServiceProvider extends ServiceProvider
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () {
- $settings = InstanceSettings::get();
+ $isFirstUser = User::count() === 0;
+
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
return redirect()->route('login');
}
if (config('coolify.waitlist')) {
return redirect()->route('waitlist.index');
} else {
- return view('auth.register');
+ return view('auth.register', [
+ 'isFirstUser' => $isFirstUser,
+ ]);
}
});
Fortify::loginView(function () {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count();
if ($users == 0) {
@@ -72,7 +75,8 @@ class FortifyServiceProvider extends ServiceProvider
});
Fortify::authenticateUsing(function (Request $request) {
- $user = User::where('email', $request->email)->with('teams')->first();
+ $email = strtolower($request->email);
+ $user = User::where('email', $email)->with('teams')->first();
if (
$user &&
Hash::check($request->password, $user->password)
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 0c6422f0c..f8ccee9db 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -3,11 +3,11 @@
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
-use Illuminate\Support\Str;
trait ExecuteRemoteCommand
{
@@ -43,9 +43,9 @@ 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::of($output)->trim();
+ $output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n".$output;
}
diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php
index df0c1cb11..25643753d 100644
--- a/app/View/Components/Forms/Datalist.php
+++ b/app/View/Components/Forms/Datalist.php
@@ -30,7 +30,7 @@ class Datalist extends Component
public function render(): View|Closure|string
{
if (is_null($this->id)) {
- $this->id = new Cuid2(7);
+ $this->id = new Cuid2;
}
if (is_null($this->name)) {
$this->name = $this->id;
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index 36c07dae1..7283ef20f 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -22,13 +22,15 @@ class Input extends Component
public bool $allowToPeak = true,
public bool $isMultiline = false,
public string $defaultClass = 'input',
- ) {
- }
+ public string $autocomplete = 'off',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
+ ) {}
public function render(): View|Closure|string
{
if (is_null($this->id)) {
- $this->id = new Cuid2(7);
+ $this->id = new Cuid2;
}
if (is_null($this->name)) {
$this->name = $this->id;
diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php
index 21c147c2b..dd5ba66b7 100644
--- a/app/View/Components/Forms/Select.php
+++ b/app/View/Components/Forms/Select.php
@@ -30,7 +30,7 @@ class Select extends Component
public function render(): View|Closure|string
{
if (is_null($this->id)) {
- $this->id = new Cuid2(7);
+ $this->id = new Cuid2;
}
if (is_null($this->name)) {
$this->name = $this->id;
diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php
index bfdf03a31..6081c2a8a 100644
--- a/app/View/Components/Forms/Textarea.php
+++ b/app/View/Components/Forms/Textarea.php
@@ -19,6 +19,8 @@ class Textarea extends Component
public ?string $value = null,
public ?string $label = null,
public ?string $placeholder = null,
+ public ?string $monacoEditorLanguage = '',
+ public bool $useMonacoEditor = false,
public bool $required = false,
public bool $disabled = false,
public bool $readonly = false,
@@ -28,7 +30,9 @@ class Textarea extends Component
public bool $realtimeValidation = false,
public bool $allowToPeak = true,
public string $defaultClass = 'input scrollbar font-mono',
- public string $defaultClassInput = 'input'
+ public string $defaultClassInput = 'input',
+ public ?int $minlength = null,
+ public ?int $maxlength = null,
) {
//
}
@@ -39,7 +43,7 @@ class Textarea extends Component
public function render(): View|Closure|string
{
if (is_null($this->id)) {
- $this->id = new Cuid2(7);
+ $this->id = new Cuid2;
}
if (is_null($this->name)) {
$this->name = $this->id;
diff --git a/app/View/Components/ResourceView.php b/app/View/Components/ResourceView.php
index 5a11b159d..d1107465b 100644
--- a/app/View/Components/ResourceView.php
+++ b/app/View/Components/ResourceView.php
@@ -16,9 +16,7 @@ class ResourceView extends Component
public ?string $logo = null,
public ?string $documentation = null,
public bool $upgrade = false,
- ) {
-
- }
+ ) {}
/**
* Get the view / contents that represent the component.
diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php
deleted file mode 100644
index f968b6d0c..000000000
--- a/app/View/Components/Server/Sidebar.php
+++ /dev/null
@@ -1,27 +0,0 @@
-links = $this->links->merge($links);
} else {
if ($application->fqdn) {
- $fqdns = collect(Str::of($application->fqdn)->explode(','));
+ $fqdns = collect(str($application->fqdn)->explode(','));
$fqdns->map(function ($fqdn) {
$this->links->push(getFqdnWithoutPort($fqdn));
});
}
if ($application->ports) {
- $portsCollection = collect(Str::of($application->ports)->explode(','));
+ $portsCollection = collect(str($application->ports)->explode(','));
$portsCollection->map(function ($port) {
- if (Str::of($port)->contains(':')) {
- $hostPort = Str::of($port)->before(':');
+ if (str($port)->contains(':')) {
+ $hostPort = str($port)->before(':');
} else {
$hostPort = $port;
}
diff --git a/app/View/Components/Status/Index.php b/app/View/Components/Status/Index.php
index f8436a102..ada9eb682 100644
--- a/app/View/Components/Status/Index.php
+++ b/app/View/Components/Status/Index.php
@@ -14,8 +14,7 @@ class Index extends Component
public function __construct(
public $resource = null,
public bool $showRefreshButton = true,
- ) {
- }
+ ) {}
/**
* Get the view / contents that represent the component.
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index 999de45c2..875866e2f 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -1,12 +1,178 @@
user()->currentAccessToken();
return data_get($token, 'team_id');
}
-function invalid_token()
+function invalidTokenResponse()
{
- return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
+ return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
+}
+
+function serializeApiResponse($data)
+{
+ if ($data instanceof Collection) {
+ return $data->map(function ($d) {
+ $d = collect($d)->sortKeys();
+ $created_at = data_get($d, 'created_at');
+ $updated_at = data_get($d, 'updated_at');
+ if ($created_at) {
+ unset($d['created_at']);
+ $d['created_at'] = $created_at;
+ }
+ if ($updated_at) {
+ unset($d['updated_at']);
+ $d['updated_at'] = $updated_at;
+ }
+ if (data_get($d, 'name')) {
+ $d = $d->prepend($d['name'], 'name');
+ }
+ if (data_get($d, 'description')) {
+ $d = $d->prepend($d['description'], 'description');
+ }
+ if (data_get($d, 'uuid')) {
+ $d = $d->prepend($d['uuid'], 'uuid');
+ }
+
+ if (! is_null(data_get($d, 'id'))) {
+ $d = $d->prepend($d['id'], 'id');
+ }
+
+ return $d;
+ });
+ } else {
+ $d = collect($data)->sortKeys();
+ $created_at = data_get($d, 'created_at');
+ $updated_at = data_get($d, 'updated_at');
+ if ($created_at) {
+ unset($d['created_at']);
+ $d['created_at'] = $created_at;
+ }
+ if ($updated_at) {
+ unset($d['updated_at']);
+ $d['updated_at'] = $updated_at;
+ }
+ if (data_get($d, 'name')) {
+ $d = $d->prepend($d['name'], 'name');
+ }
+ if (data_get($d, 'description')) {
+ $d = $d->prepend($d['description'], 'description');
+ }
+ if (data_get($d, 'uuid')) {
+ $d = $d->prepend($d['uuid'], 'uuid');
+ }
+
+ if (! is_null(data_get($d, 'id'))) {
+ $d = $d->prepend($d['id'], 'id');
+ }
+
+ return $d;
+ }
+}
+
+function sharedDataApplications()
+{
+ return [
+ 'git_repository' => 'string',
+ 'git_branch' => 'string',
+ 'build_pack' => Rule::enum(BuildPackTypes::class),
+ 'is_static' => 'boolean',
+ 'static_image' => Rule::enum(StaticImageTypes::class),
+ 'domains' => 'string',
+ 'redirect' => Rule::enum(RedirectTypes::class),
+ 'git_commit_sha' => 'string',
+ 'docker_registry_image_name' => 'string|nullable',
+ 'docker_registry_image_tag' => 'string|nullable',
+ 'install_command' => 'string|nullable',
+ 'build_command' => 'string|nullable',
+ 'start_command' => 'string|nullable',
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
+ 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
+ 'base_directory' => 'string|nullable',
+ 'publish_directory' => 'string|nullable',
+ 'health_check_enabled' => 'boolean',
+ 'health_check_path' => 'string',
+ 'health_check_port' => 'string|nullable',
+ 'health_check_host' => 'string',
+ 'health_check_method' => 'string',
+ 'health_check_return_code' => 'numeric',
+ 'health_check_scheme' => 'string',
+ 'health_check_response_text' => 'string|nullable',
+ 'health_check_interval' => 'numeric',
+ 'health_check_timeout' => 'numeric',
+ 'health_check_retries' => 'numeric',
+ 'health_check_start_period' => 'numeric',
+ 'limits_memory' => 'string',
+ 'limits_memory_swap' => 'string',
+ 'limits_memory_swappiness' => 'numeric',
+ 'limits_memory_reservation' => 'string',
+ 'limits_cpus' => 'string',
+ 'limits_cpuset' => 'string|nullable',
+ 'limits_cpu_shares' => 'numeric',
+ 'custom_labels' => 'string|nullable',
+ 'custom_docker_run_options' => 'string|nullable',
+ 'post_deployment_command' => 'string|nullable',
+ 'post_deployment_command_container' => 'string',
+ 'pre_deployment_command' => 'string|nullable',
+ 'pre_deployment_command_container' => 'string',
+ 'manual_webhook_secret_github' => 'string|nullable',
+ 'manual_webhook_secret_gitlab' => 'string|nullable',
+ 'manual_webhook_secret_bitbucket' => 'string|nullable',
+ 'manual_webhook_secret_gitea' => 'string|nullable',
+ 'docker_compose_location' => 'string',
+ 'docker_compose' => 'string|nullable',
+ 'docker_compose_raw' => 'string|nullable',
+ 'docker_compose_domains' => 'array|nullable',
+ 'docker_compose_custom_start_command' => 'string|nullable',
+ 'docker_compose_custom_build_command' => 'string|nullable',
+ ];
+}
+
+function validateIncomingRequest(Request $request)
+{
+ // check if request is json
+ if (! $request->isJson()) {
+ return response()->json([
+ 'message' => 'Invalid request.',
+ 'error' => 'Content-Type must be application/json.',
+ ], 400);
+ }
+ // check if request is valid json
+ if (! json_decode($request->getContent())) {
+ return response()->json([
+ 'message' => 'Invalid request.',
+ 'error' => 'Invalid JSON.',
+ ], 400);
+ }
+ // check if valid json is empty
+ if (empty($request->json()->all())) {
+ return response()->json([
+ 'message' => 'Invalid request.',
+ 'error' => 'Empty JSON.',
+ ], 400);
+ }
+}
+
+function removeUnnecessaryFieldsFromRequest(Request $request)
+{
+ $request->offsetUnset('project_uuid');
+ $request->offsetUnset('environment_name');
+ $request->offsetUnset('destination_uuid');
+ $request->offsetUnset('server_uuid');
+ $request->offsetUnset('type');
+ $request->offsetUnset('domains');
+ $request->offsetUnset('instant_deploy');
+ $request->offsetUnset('github_app_uuid');
+ $request->offsetUnset('private_key_uuid');
+ $request->offsetUnset('use_build_server');
+ $request->offsetUnset('is_static');
}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index 376b0f2aa..eb331f8c2 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -8,7 +8,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
-function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)
+function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)
{
$application_id = $application->id;
$deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}");
@@ -35,6 +35,7 @@ function queue_application_deployment(Application $application, string $deployme
'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook,
+ 'is_api' => $is_api,
'restart_only' => $restart_only,
'commit' => $commit,
'rollback' => $rollback,
@@ -45,11 +46,11 @@ function queue_application_deployment(Application $application, string $deployme
if ($no_questions_asked) {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
- ));
+ ))->onQueue('high');
} elseif (next_queuable($server_id, $application_id)) {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
- ));
+ ))->onQueue('high');
}
}
function force_start_deployment(ApplicationDeploymentQueue $deployment)
@@ -60,12 +61,12 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment)
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
- ));
+ ))->onQueue('high');
}
function queue_next_deployment(Application $application)
{
$server_id = $application->destination->server_id;
- $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
+ $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first();
if ($next_found) {
$next_found->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
@@ -73,13 +74,13 @@ function queue_next_deployment(Application $application)
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
- ));
+ ))->onQueue('high');
}
}
function next_queuable(string $server_id, string $application_id): bool
{
- $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at');
+ $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
$same_application_deployments = $deployments->where('application_id', $application_id);
$in_progress = $same_application_deployments->filter(function ($value, $key) {
return $value->status === 'in_progress';
@@ -90,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;
@@ -98,3 +99,26 @@ function next_queuable(string $server_id, string $application_id): bool
return true;
}
+function next_after_cancel(?Server $server = null)
+{
+ if ($server) {
+ $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at');
+ if ($next_found->count() > 0) {
+ foreach ($next_found as $next) {
+ $server = Server::find($next->server_id);
+ $concurrent_builds = $server->settings->concurrent_builds;
+ $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
+ if ($inprogress_deployments->count() < $concurrent_builds) {
+ $next->update([
+ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ]);
+
+ dispatch(new ApplicationDeploymentJob(
+ application_deployment_queue_id: $next->id,
+ ))->onQueue('high');
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index e0272fa4c..303fcab8e 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -9,18 +9,27 @@ const VALID_CRON_STRINGS = [
'weekly' => '0 0 * * 0',
'monthly' => '0 0 1 * *',
'yearly' => '0 0 1 1 *',
+ '@hourly' => '0 * * * *',
+ '@daily' => '0 0 * * *',
+ '@weekly' => '0 0 * * 0',
+ '@monthly' => '0 0 1 * *',
+ '@yearly' => '0 0 1 1 *',
];
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',
@@ -28,10 +37,10 @@ const DATABASE_DOCKER_IMAGES = [
'neo4j',
'influxdb',
'clickhouse/clickhouse-server',
- 'supabase/postgres',
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
+ 'minio/minio',
'svhd/logto',
];
@@ -40,6 +49,8 @@ const SUPPORTED_OS = [
'ubuntu debian raspbian',
'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 dba8aa543..e12910f82 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -1,5 +1,6 @@
first();
+ $destination = StandaloneDocker::where('uuid', $destinationUuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $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;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandalonePostgresql::create([
- 'name' => generate_database_name('postgresql'),
- 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis
+function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneRedis;
+ $database->name = generate_database_name('redis');
+ $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneRedis::create([
- 'name' => generate_database_name('redis'),
- 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
]);
+
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
+ return $database;
}
-function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb
+function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneMongodb;
+ $database->name = generate_database_name('mongodb');
+ $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneMongodb::create([
- 'name' => generate_database_name('mongodb'),
- 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql
+function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneMysql;
+ $database->name = generate_database_name('mysql');
+ $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneMysql::create([
- 'name' => generate_database_name('mysql'),
- 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb
+function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneMariadb;
+ $database->name = generate_database_name('mariadb');
+ $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
- return StandaloneMariadb::create([
- 'name' => generate_database_name('mariadb'),
- 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
+
+ return $database;
}
-function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb
+function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneKeydb;
+ $database->name = generate_database_name('keydb');
+ $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneKeydb::create([
- 'name' => generate_database_name('keydb'),
- 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly
+function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneDragonfly;
+ $database->name = generate_database_name('dragonfly');
+ $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneDragonfly::create([
- 'name' => generate_database_name('dragonfly'),
- 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse
+function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
+ $database = new StandaloneClickhouse;
+ $database->name = generate_database_name('clickhouse');
+ $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $database->environment_id = $environment_id;
+ $database->destination_id = $destination->id;
+ $database->destination_type = $destination->getMorphClass();
+ if ($otherData) {
+ $database->fill($otherData);
+ }
+ $database->save();
- return StandaloneClickhouse::create([
- 'name' => generate_database_name('clickhouse'),
- 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
- 'environment_id' => $environment_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ]);
+ return $database;
}
-/**
- * Delete file locally on the filesystem.
- */
function delete_backup_locally(?string $filename, Server $server): void
{
if (empty($filename)) {
@@ -156,3 +201,17 @@ function delete_backup_locally(?string $filename, Server $server): void
}
instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false);
}
+
+function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool
+{
+ if ($id) {
+ $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->where('id', '!=', $id)->first();
+ } else {
+ $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->first();
+ }
+ if ($foundDatabase) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 91e553cf6..40eacf5c8 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -1,5 +1,6 @@
filter();
- return $containers;
+ return $containers->filter();
+ }
+
+ 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);
+
+ return $containers->filter();
}
return $containers;
@@ -48,9 +61,13 @@ function format_docker_command_output_to_json($rawOutput): Collection
$outputLines = collect($outputLines);
}
- return $outputLines
- ->reject(fn ($line) => empty($line))
- ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
+ try {
+ return $outputLines
+ ->reject(fn ($line) => empty($line))
+ ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
+ } catch (\Throwable) {
+ return collect([]);
+ }
}
function format_docker_labels_to_json(string|array $rawOutput): Collection
@@ -85,13 +102,13 @@ function format_docker_envs_to_json($rawOutput)
return [$env[0] => $env[1]];
});
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return collect([]);
}
}
function checkMinimumDockerEngineVersion($dockerVersion)
{
- $majorDockerVersion = Str::of($dockerVersion)->before('.')->value();
+ $majorDockerVersion = str($dockerVersion)->before('.')->value();
if ($majorDockerVersion <= 22) {
$dockerVersion = null;
}
@@ -115,6 +132,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];
}
@@ -135,6 +155,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) {
@@ -152,7 +174,7 @@ function get_port_from_dockerfile($dockerfile): ?int
$dockerfile_array = explode("\n", $dockerfile);
$found_exposed_port = null;
foreach ($dockerfile_array as $line) {
- $line_str = Str::of($line)->trim();
+ $line_str = str($line)->trim();
if ($line_str->startsWith('EXPOSE')) {
$found_exposed_port = $line_str->replace('EXPOSE', '')->trim();
break;
@@ -183,12 +205,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
+ if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'service.server');
$environment_variables = data_get($resource, 'service.environment_variables');
$type = $resource->serviceType();
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'destination.server');
$environment_variables = data_get($resource, 'environment_variables');
@@ -208,12 +230,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([
@@ -246,7 +268,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) {
@@ -255,7 +277,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_ingress_network={$network}");
}
foreach ($domains as $loop => $domain) {
- $loop = $loop;
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
@@ -265,6 +286,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");
@@ -298,43 +322,26 @@ 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];
+ }
+ if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
+ return explode(',', $matches[1]);
+ }
+
+ return null;
+ })->flatten()
+ ->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
if ($generate_unique_uuid) {
- $uuid = new Cuid2(7);
+ $uuid = new Cuid2;
}
$url = Url::fromString($domain);
@@ -352,8 +359,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$https_label = "https-{$loop}-{$uuid}-{$service_name}";
}
if (str($image)->contains('ghost')) {
- $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.regex=^{$path}/(.*)");
- $labels->push('traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1');
+ $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)");
+ $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.replacement=/$1");
+ $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.handler=rewrite");
+ $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.regexp=^{$path}/(.*)");
+ $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.replacement=/$1");
}
$to_www_name = "{$loop}-{$uuid}-to-www";
@@ -377,6 +387,7 @@ 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')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
@@ -385,12 +396,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');
}
@@ -402,6 +407,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}");
@@ -411,12 +419,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');
}
@@ -428,6 +430,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}");
@@ -458,17 +463,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
- $middlewares->push("{$https_label}-stripprefix");
+ $middlewares->push("{$http_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');
}
@@ -480,6 +479,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}");
@@ -489,12 +491,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');
}
@@ -506,13 +502,16 @@ 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}");
}
}
}
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
continue;
}
}
@@ -534,17 +533,96 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$labels = collect([]);
if ($pull_request_id === 0) {
if ($application->fqdn) {
- $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
+ $domains = str(data_get($application, 'fqdn'))->explode(',');
+ $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels;
+ if ($shouldGenerateLabelsExactly) {
+ switch ($application->destination->server->proxyType()) {
+ case ProxyTypes::TRAEFIK->value:
+ $labels = $labels->merge(fqdnLabelsForTraefik(
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ redirect_direction: $application->redirect
+ ));
+ break;
+ case ProxyTypes::CADDY->value:
+ $labels = $labels->merge(fqdnLabelsForCaddy(
+ network: $application->destination->network,
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ redirect_direction: $application->redirect
+ ));
+ break;
+ }
+ } else {
+ $labels = $labels->merge(fqdnLabelsForTraefik(
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ redirect_direction: $application->redirect
+ ));
+ $labels = $labels->merge(fqdnLabelsForCaddy(
+ network: $application->destination->network,
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled(),
+ redirect_direction: $application->redirect
+ ));
+ }
+ }
+ } else {
+ if (data_get($preview, 'fqdn')) {
+ $domains = str(data_get($preview, 'fqdn'))->explode(',');
+ } else {
+ $domains = collect([]);
+ }
+ $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels;
+ if ($shouldGenerateLabelsExactly) {
+ switch ($application->destination->server->proxyType()) {
+ case ProxyTypes::TRAEFIK->value:
+ $labels = $labels->merge(fqdnLabelsForTraefik(
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled()
+ ));
+ break;
+ case ProxyTypes::CADDY->value:
+ $labels = $labels->merge(fqdnLabelsForCaddy(
+ network: $application->destination->network,
+ uuid: $appUuid,
+ domains: $domains,
+ onlyPort: $onlyPort,
+ is_force_https_enabled: $application->isForceHttpsEnabled(),
+ is_gzip_enabled: $application->isGzipEnabled(),
+ is_stripprefix_enabled: $application->isStripprefixEnabled()
+ ));
+ break;
+ }
+ } else {
$labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ is_stripprefix_enabled: $application->isStripprefixEnabled()
));
- // Add Caddy labels
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
uuid: $appUuid,
@@ -552,35 +630,9 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled(),
- redirect_direction: $application->redirect
+ is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
- } else {
- if (data_get($preview, 'fqdn')) {
- $domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
- } else {
- $domains = collect([]);
- }
- $labels = $labels->merge(fqdnLabelsForTraefik(
- uuid: $appUuid,
- domains: $domains,
- onlyPort: $onlyPort,
- is_force_https_enabled: $application->isForceHttpsEnabled(),
- is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
- ));
- // Add Caddy labels
- $labels = $labels->merge(fqdnLabelsForCaddy(
- network: $application->destination->network,
- uuid: $appUuid,
- domains: $domains,
- onlyPort: $onlyPort,
- is_force_https_enabled: $application->isForceHttpsEnabled(),
- is_gzip_enabled: $application->isGzipEnabled(),
- is_stripprefix_enabled: $application->isStripprefixEnabled()
- ));
-
}
return $labels->all();
@@ -605,7 +657,7 @@ function isDatabaseImage(?string $image = null)
return false;
}
-function convert_docker_run_to_compose(?string $custom_docker_run_options = null)
+function convertDockerRunToCompose(?string $custom_docker_run_options = null)
{
$options = [];
$compose_options = collect([]);
@@ -617,21 +669,30 @@ 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',
+ '--gpus' => 'gpus',
]);
foreach ($matches as $match) {
$option = $match[1];
+ if ($option === '--gpus') {
+ $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/';
+ preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches);
+ $value = $device_matches[1] ?? 'all';
+ $options[$option][] = $value;
+ $options[$option] = array_unique($options[$option]);
+ }
if (isset($match[2]) && $match[2] !== '') {
$value = $match[2];
$options[$option][] = $value;
@@ -668,6 +729,32 @@ 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]);
+ }
+ } elseif ($option === '--gpus') {
+ $payload = [
+ 'driver' => 'nvidia',
+ 'capabilities' => ['gpu'],
+ ];
+ if (! is_null($value) && is_array($value) && count($value) > 0) {
+ if (str($value[0]) != 'all') {
+ if (str($value[0])->contains(',')) {
+ $payload['device_ids'] = str($value[0])->explode(',')->toArray();
+ } else {
+ $payload['device_ids'] = [$value[0]];
+ }
+ }
+ }
+ ray($payload);
+ $compose_options->put('deploy', [
+ 'resources' => [
+ 'reservations' => [
+ 'devices' => [$payload],
+ ],
+ ],
+ ]);
} else {
if ($list_options->contains($option)) {
if ($compose_options->has($mapping[$option])) {
@@ -689,6 +776,26 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null
return $compose_options->toArray();
}
+function generateCustomDockerRunOptionsForDatabases($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/github.php b/bootstrap/helpers/github.php
index d916dc9c8..529ac82b1 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -3,6 +3,7 @@
use App\Models\GithubApp;
use App\Models\GitlabApp;
use Carbon\Carbon;
+use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Lcobucci\JWT\Encoding\ChainedFormatter;
@@ -14,9 +15,9 @@ use Lcobucci\JWT\Token\Builder;
function generate_github_installation_token(GithubApp $source)
{
$signingKey = InMemory::plainText($source->privateKey->private_key);
- $algorithm = new Sha256();
- $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
- $now = new DateTimeImmutable();
+ $algorithm = new Sha256;
+ $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$issuedToken = $tokenBuilder
->issuedBy($source->app_id)
@@ -38,18 +39,17 @@ function generate_github_installation_token(GithubApp $source)
function generate_github_jwt_token(GithubApp $source)
{
$signingKey = InMemory::plainText($source->privateKey->private_key);
- $algorithm = new Sha256();
- $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
- $now = new DateTimeImmutable();
+ $algorithm = new Sha256;
+ $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
+ $now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
- $issuedToken = $tokenBuilder
+
+ return $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now->modify('-1 minute'))
->expiresAt($now->modify('+10 minutes'))
->getToken($algorithm, $signingKey)
->toString();
-
- return $issuedToken;
}
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
@@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
if (is_null($source)) {
throw new \Exception('Not implemented yet.');
}
- if ($source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($source->getMorphClass() === \App\Models\GithubApp::class) {
if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint);
} else {
@@ -85,7 +85,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
function get_installation_path(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
- $name = Str::of(Str::kebab($github->name));
+ $name = str(Str::kebab($github->name));
$installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps';
return "$github->html_url/$installation_path/$name/installations/new";
@@ -93,7 +93,7 @@ function get_installation_path(GithubApp $source)
function get_permissions_path(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
- $name = Str::of(Str::kebab($github->name));
+ $name = str(Str::kebab($github->name));
return "$github->html_url/settings/apps/$name/permissions";
}
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 2bf230c20..a8ef0fe5a 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -1,12 +1,29 @@
isFunctional()) {
+ return collect();
+ }
+ $proxyType = $server->proxyType();
+ if (is_null($proxyType) || $proxyType === 'NONE') {
+ return collect();
+ }
+ $networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false);
+
+ return collect($networks)->map(function ($network) {
+ return collect(json_decode($network))->keys();
+ })->flatten()->unique();
+}
+function collectDockerNetworksByServer(Server $server)
+{
+ $allNetworks = collect([]);
if ($server->isSwarm()) {
$networks = collect($server->swarmDockers)->map(function ($docker) {
return $docker['network'];
@@ -17,18 +34,28 @@ function connectProxyToNetworks(Server $server)
return $docker['network'];
});
}
+ $allNetworks = $allNetworks->merge($networks);
// Service networks
foreach ($server->services()->get() as $service) {
- $networks->push($service->networks());
+ if ($service->isRunning()) {
+ $networks->push($service->networks());
+ }
+ $allNetworks->push($service->networks());
}
// Docker compose based apps
$docker_compose_apps = $server->dockerComposeBasedApplications();
foreach ($docker_compose_apps as $app) {
- $networks->push($app->uuid);
+ if ($app->isRunning()) {
+ $networks->push($app->uuid);
+ }
+ $allNetworks->push($app->uuid);
}
// Docker compose based preview deployments
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
foreach ($docker_compose_previews as $preview) {
+ if (! $preview->isRunning()) {
+ continue;
+ }
$pullRequestId = $preview->pull_request_id;
$applicationId = $preview->application_id;
$application = Application::find($applicationId);
@@ -37,28 +64,48 @@ function connectProxyToNetworks(Server $server)
}
$network = "{$application->uuid}-{$pullRequestId}";
$networks->push($network);
+ $allNetworks->push($network);
}
$networks = collect($networks)->flatten()->unique();
+ $allNetworks = $allNetworks->flatten()->unique();
if ($server->isSwarm()) {
if ($networks->count() === 0) {
$networks = collect(['coolify-overlay']);
+ $allNetworks = collect(['coolify-overlay']);
}
+ } else {
+ if ($networks->count() === 0) {
+ $networks = collect(['coolify']);
+ $allNetworks = collect(['coolify']);
+ }
+ }
+
+ return [
+ 'networks' => $networks,
+ 'allNetworks' => $allNetworks,
+ ];
+}
+function connectProxyToNetworks(Server $server)
+{
+ ['networks' => $networks] = collectDockerNetworksByServer($server);
+ if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
return [
"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 {
- if ($networks->count() === 0) {
- $networks = collect(['coolify']);
- }
$commands = $networks->map(function ($network) {
return [
"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!'",
];
});
}
@@ -92,21 +139,21 @@ function generate_default_proxy_configuration(Server $server)
'external' => true,
];
});
- if ($proxy_type === 'TRAEFIK_V2') {
+ if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$labels = [
'traefik.enable=true',
'traefik.http.routers.traefik.entrypoints=http',
'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.10',
+ 'image' => 'traefik:v3.1',
'restart' => RESTART_MODE,
'extra_hosts' => [
'host.docker.internal:host-gateway',
@@ -115,6 +162,7 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
'8080:8080',
],
'healthcheck' => [
@@ -138,6 +186,7 @@ function generate_default_proxy_configuration(Server $server)
'--entryPoints.http.http2.maxConcurrentStreams=50',
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
+ '--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
'--providers.file.watch=true',
@@ -173,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' => [
@@ -191,13 +239,12 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
+ ],
+ 'labels' => [
+ 'coolify.managed=true',
+ 'coolify.proxy=true',
],
- // "healthcheck" => [
- // "test" => "wget -qO- http://localhost:80|| exit 1",
- // "interval" => "4s",
- // "timeout" => "2s",
- // "retries" => 5,
- // ],
'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..c7dd2cb83 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'),
associative: true,
flags: JSON_THROW_ON_ERROR
);
- } catch (\JsonException $exception) {
+ } catch (\JsonException) {
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);
+ } catch (\Throwable) {
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 0cc4c51e7..fd2e1231f 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -4,7 +4,7 @@ use App\Models\Application;
use App\Models\EnvironmentVariable;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
-use Illuminate\Support\Str;
+use Illuminate\Support\Stringable;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
@@ -16,15 +16,15 @@ 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)
{
try {
- if ($oneService->getMorphClass() === 'App\Models\Application') {
+ if ($oneService->getMorphClass() === \App\Models\Application::class) {
$workdir = $oneService->workdir();
$server = $oneService->destination->server;
} else {
@@ -38,7 +38,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
]);
instant_remote_process($commands, $server);
foreach ($fileVolumes as $fileVolume) {
- $path = Str::of(data_get($fileVolume, 'fs_path'));
+ $path = str(data_get($fileVolume, 'fs_path'));
$content = data_get($fileVolume, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
@@ -51,29 +51,38 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
// Exists and is a directory
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
- if ($isFile == 'OK') {
+ 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') {
+ } elseif ($isDir === 'OK') {
// If its a directory & exists
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false;
$fileVolume->save();
$content = base64_encode($content);
- $dir = Str::of($fileLocation)->dirname();
+ $dir = str($fileLocation)->dirname();
instant_remote_process([
"mkdir -p $dir",
"echo '$content' | base64 -d | tee $fileLocation",
], $server);
- } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) {
+ // Does not exists (no dir or file), flagged as directory, is init
+ $fileVolume->content = null;
+ $fileVolume->is_directory = true;
+ $fileVolume->save();
+ instant_remote_process(["mkdir -p $fileLocation"], $server);
+ } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) {
+ // Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
@@ -89,6 +98,9 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
try {
$name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
+ if (! $dockerComposeRaw) {
+ throw new \Exception('No compose file found or not a valid YAML file.');
+ }
$dockerCompose = Yaml::parse($dockerComposeRaw);
// Switch Image
@@ -106,41 +118,56 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$resourceFqdns = str($resource->fqdn)->explode(',');
if ($resourceFqdns->count() === 1) {
$resourceFqdns = $resourceFqdns->first();
- $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', '');
+ $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($resourceFqdns);
$port = $fqdn->getPort();
$path = $fqdn->getPath();
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost();
if ($generatedEnv) {
- $generatedEnv->value = $fqdn.$path;
+ if ($path === '/') {
+ $generatedEnv->value = $fqdn;
+ } else {
+ $generatedEnv->value = $fqdn.$path;
+ }
$generatedEnv->save();
}
if ($port) {
$variableName = $variableName."_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
- // ray($generatedEnv);
if ($generatedEnv) {
- $generatedEnv->value = $fqdn.$path;
+ if ($path === '/') {
+ $generatedEnv->value = $fqdn;
+ } else {
+ $generatedEnv->value = $fqdn.$path;
+ }
$generatedEnv->save();
}
}
- $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', '');
+ $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$port = $url->getPort();
$path = $url->getPath();
$url = $url->getHost();
if ($generatedEnv) {
- $url = Str::of($fqdn)->after('://');
- $generatedEnv->value = $url.$path;
+ $url = str($fqdn)->after('://');
+ if ($path === '/') {
+ $generatedEnv->value = $url;
+ } else {
+ $generatedEnv->value = $url.$path;
+ }
$generatedEnv->save();
}
if ($port) {
$variableName = $variableName."_$port";
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
- $generatedEnv->value = $url.$path;
+ if ($path === '/') {
+ $generatedEnv->value = $url;
+ } else {
+ $generatedEnv->value = $url.$path;
+ }
$generatedEnv->save();
}
}
@@ -157,10 +184,18 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_'.$service_fqdn)->first();
if ($env) {
- $env->value = $host.$path;
+ if ($path === '/') {
+ $env->value = $host;
+ } else {
+ $env->value = $host.$path;
+ }
$env->save();
}
- $port_env->value = $host.$path;
+ if ($path === '/') {
+ $port_env->value = $host;
+ } else {
+ $port_env->value = $host.$path;
+ }
$port_env->save();
}
$port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get();
@@ -168,14 +203,22 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_');
$env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_'.$service_url)->first();
if ($env) {
- $env->value = $url.$path;
+ if ($path === '/') {
+ $env->value = $url;
+ } else {
+ $env->value = $url.$path;
+ }
$env->save();
}
- $port_env_url->value = $url.$path;
+ if ($path === '/') {
+ $port_env_url->value = $url;
+ } else {
+ $port_env_url->value = $url.$path;
+ }
$port_env_url->save();
}
} else {
- $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', '');
+ $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$fqdn = Url::fromString($fqdn);
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath();
@@ -183,12 +226,12 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
$generatedEnv->value = $fqdn;
$generatedEnv->save();
}
- $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', '');
+ $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '');
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Url::fromString($fqdn);
$url = $url->getHost().$url->getPath();
if ($generatedEnv) {
- $url = Str::of($fqdn)->after('://');
+ $url = str($fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
@@ -200,3 +243,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
return handleError($e);
}
}
+function serviceKeys()
+{
+ return get_service_templates()->keys();
+}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 7994c10af..6e52dcde9 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -1,11 +1,13 @@
user()?->currentTeam() ?? null;
+ return Auth::user()?->currentTeam() ?? null;
}
function showBoarding(): bool
{
- if (auth()->user()?->isMember()) {
+ if (Auth::user()?->isMember()) {
return false;
}
@@ -106,21 +116,20 @@ function showBoarding(): bool
function refreshSession(?Team $team = null): void
{
if (! $team) {
- if (auth()->user()?->currentTeam()) {
- $team = Team::find(auth()->user()->currentTeam()->id);
+ if (Auth::user()->currentTeam()) {
+ $team = Team::find(Auth::user()->currentTeam()->id);
} else {
- $team = User::find(auth()->user()->id)->teams->first();
+ $team = User::find(Auth::id())->teams->first();
}
}
- Cache::forget('team:'.auth()->user()->id);
- Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) {
+ Cache::forget('team:'.Auth::id());
+ Cache::remember('team:'.Auth::id(), 3600, function () use ($team) {
return $team;
});
session(['currentTeam' => $team]);
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
- ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -136,6 +145,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.';
}
+ if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ abort(404);
+ }
+
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@@ -162,10 +175,7 @@ function get_latest_sentinel_version(): string
$versions = $response->json();
return data_get($versions, 'coolify.sentinel.version');
- } catch (\Throwable $e) {
- //throw $e;
- ray($e->getMessage());
-
+ } catch (\Throwable) {
return '0.0.0';
}
}
@@ -176,11 +186,7 @@ function get_latest_version_of_coolify(): string
$versions = json_decode($versions, true);
return data_get($versions, 'coolify.v4.version');
- // $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
- // $versions = $response->json();
- // return data_get($versions, 'coolify.v4.version');
} catch (\Throwable $e) {
- //throw $e;
ray($e->getMessage());
return '0.0.0';
@@ -191,11 +197,11 @@ function generate_random_name(?string $cuid = null): string
{
$generator = new \Nubs\RandomNameGenerator\All(
[
- new \Nubs\RandomNameGenerator\Alliteration(),
+ new \Nubs\RandomNameGenerator\Alliteration,
]
);
if (is_null($cuid)) {
- $cuid = new Cuid2(7);
+ $cuid = new Cuid2;
}
return Str::kebab("{$generator->getName()}-$cuid");
@@ -231,7 +237,7 @@ function formatPrivateKey(string $privateKey)
function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string
{
if (is_null($cuid)) {
- $cuid = new Cuid2(7);
+ $cuid = new Cuid2;
}
return Str::kebab("$git_repository:$git_branch-$cuid");
@@ -239,13 +245,13 @@ function generate_application_name(string $git_repository, string $git_branch, ?
function is_transactional_emails_active(): bool
{
- return isEmailEnabled(InstanceSettings::get());
+ return isEmailEnabled(\App\Models\InstanceSettings::get());
}
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{
if (! $settings) {
- $settings = 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'));
@@ -279,7 +285,7 @@ function base_ip(): string
if (isDev()) {
return 'localhost';
}
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->public_ipv4) {
return "$settings->public_ipv4";
}
@@ -298,7 +304,7 @@ function getFqdnWithoutPort(string $fqdn)
$path = $url->getPath();
return "$scheme://$host$path";
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
return $fqdn;
}
}
@@ -307,7 +313,7 @@ function getFqdnWithoutPort(string $fqdn)
*/
function base_url(bool $withPort = true): string
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->fqdn) {
return $settings->fqdn;
}
@@ -341,6 +347,11 @@ function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
}
+
+function isProduction(): bool
+{
+ return ! isDev();
+}
function isDev(): bool
{
return config('app.env') === 'local';
@@ -351,8 +362,19 @@ 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
{
+ if (empty($expression_to_validate)) {
+ return false;
+ }
$isValid = false;
$expression = new CronExpression($expression_to_validate);
$isValid = $expression->isValid();
@@ -374,7 +396,7 @@ function send_internal_notification(string $message): void
}
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
throw new Exception('No email settings found.');
@@ -465,10 +487,10 @@ function data_get_str($data, $key, $default = null): Stringable
{
$str = data_get($data, $key, $default) ?? $default;
- return Str::of($str);
+ 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 === '') {
@@ -478,9 +500,11 @@ function generateFqdn(Server $server, string $random)
$host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme();
- $finalFqdn = "$scheme://{$random}.$host$path";
+ if ($forceHttps) {
+ $scheme = 'https';
+ }
- return $finalFqdn;
+ return "$scheme://{$random}.$host$path";
}
function sslip(Server $server)
{
@@ -492,22 +516,33 @@ 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, 50)->get(config('constants.services.official'));
+ $response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->failed()) {
return collect([]);
}
$services = $response->json();
return collect($services);
- } catch (\Throwable $e) {
+ } catch (\Throwable) {
$services = File::get(base_path('templates/service-templates.json'));
return collect(json_decode($services))->sortKeys();
@@ -531,6 +566,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
return null;
}
+function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId)
+{
+ $postgresql = StandalonePostgresql::whereUuid($uuid)->first();
+ if ($postgresql && $postgresql->team()->id == $teamId) {
+ return $postgresql->unsetRelation('environment')->unsetRelation('destination');
+ }
+ $redis = StandaloneRedis::whereUuid($uuid)->first();
+ if ($redis && $redis->team()->id == $teamId) {
+ return $redis->unsetRelation('environment');
+ }
+ $mongodb = StandaloneMongodb::whereUuid($uuid)->first();
+ if ($mongodb && $mongodb->team()->id == $teamId) {
+ return $mongodb->unsetRelation('environment');
+ }
+ $mysql = StandaloneMysql::whereUuid($uuid)->first();
+ if ($mysql && $mysql->team()->id == $teamId) {
+ return $mysql->unsetRelation('environment');
+ }
+ $mariadb = StandaloneMariadb::whereUuid($uuid)->first();
+ if ($mariadb && $mariadb->team()->id == $teamId) {
+ return $mariadb->unsetRelation('environment');
+ }
+ $keydb = StandaloneKeydb::whereUuid($uuid)->first();
+ if ($keydb && $keydb->team()->id == $teamId) {
+ return $keydb->unsetRelation('environment');
+ }
+ $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first();
+ if ($dragonfly && $dragonfly->team()->id == $teamId) {
+ return $dragonfly->unsetRelation('environment');
+ }
+ $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first();
+ if ($clickhouse && $clickhouse->team()->id == $teamId) {
+ return $clickhouse->unsetRelation('environment');
+ }
+
+ return null;
+}
function queryResourcesByUuid(string $uuid)
{
$resource = null;
@@ -577,14 +649,13 @@ function queryResourcesByUuid(string $uuid)
return $resource;
}
-function generatTagDeployWebhook($tag_name)
+function generateTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = "/deploy?tag=$tag_name";
- $url = $api.$endpoint;
- return $url;
+ return $api.$endpoint;
}
function generateDeployWebhook($resource)
{
@@ -592,20 +663,18 @@ function generateDeployWebhook($resource)
$api = Url::fromString($baseUrl).'/api/v1';
$endpoint = '/deploy';
$uuid = data_get($resource, 'uuid');
- $url = $api.$endpoint."?uuid=$uuid&force=false";
- return $url;
+ return $api.$endpoint."?uuid=$uuid&force=false";
}
function generateGitManualWebhook($resource, $type)
{
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) {
return null;
}
- if ($resource->getMorphClass() === 'App\Models\Application') {
+ if ($resource->getMorphClass() === \App\Models\Application::class) {
$baseUrl = base_url();
- $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
- return $api;
+ return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
}
return null;
@@ -617,7 +686,7 @@ function removeAnsiColors($text)
function getTopLevelNetworks(Service|Application $resource)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -647,7 +716,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);
+ }
}
}
}
@@ -670,7 +741,7 @@ function getTopLevelNetworks(Service|Application $resource)
return $topLevelNetworks->keys();
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
@@ -697,7 +768,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);
+ }
}
}
}
@@ -719,10 +792,710 @@ 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 = CarbonImmutable::now();
+ $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 = CarbonImmutable::now();
+ $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) {
+ }
+ }
+ 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::class && $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: Link: {$app->name} ");
+ }
+ } elseif ($domain) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource: Link: {$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: Link: {$app->service->name} ");
+ }
+ } elseif ($domain) {
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource: Link: {$app->service->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 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::class) {
+ $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::class) {
+ $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)
{
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
@@ -732,6 +1505,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$allServices = get_service_templates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
+ $topLevelConfigs = collect(data_get($yaml, 'configs', []));
+ $topLevelSecrets = collect(data_get($yaml, 'secrets', []));
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
@@ -864,7 +1639,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);
+ }
}
}
}
@@ -929,26 +1706,33 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$content = null;
$isDirectory = false;
if (is_string($volume)) {
- $source = Str::of($volume)->before(':');
- $target = Str::of($volume)->after(':')->beforeLast(':');
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
- $type = Str::of('bind');
+ $type = str('bind');
+ // By default, we cannot determine if the bind is a directory or not, so we set it to directory
+ $isDirectory = true;
} else {
- $type = Str::of('volume');
+ $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', false) || (bool) data_get($volume, 'is_directory', false);
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
- $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false);
+ $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
+ }
+ if (is_null($isDirectory) && is_null($content)) {
+ // if isDirectory is not set & content is also not set, we assume it is a directory
+ ray('setting isDirectory to true');
+ $isDirectory = true;
}
}
if ($type?->value() === 'bind') {
@@ -983,8 +1767,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
- $source = Str::of($volume)->before(':');
- $target = Str::of($volume)->after(':')->beforeLast(':');
+ $source = str($volume)->before(':');
+ $target = str($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} elseif (is_array($volume)) {
@@ -1014,36 +1798,49 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray());
}
- // Add env_file with at least .env to the service
- // $envFile = collect(data_get($service, 'env_file', []));
- // if ($envFile->count() > 0) {
- // if (!$envFile->contains('.env')) {
- // $envFile->push('.env');
- // }
- // } else {
- // $envFile = collect(['.env']);
- // }
- // data_set($service, 'env_file', $envFile->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)) {
- $variable = Str::of($variable);
- if ($variable->contains('=')) {
- // - SESSION_SECRET=123
- // - SESSION_SECRET=
- $key = $variable->before('=');
- $value = $variable->after('=');
+ if (is_array($variable)) {
+ // - SESSION_SECRET: 123
+ // - SESSION_SECRET:
+ $key = str(collect($variable)->keys()->first());
+ $value = str(collect($variable)->values()->first());
} else {
- // - SESSION_SECRET
- $key = $variable;
- $value = null;
+ $variable = str($variable);
+ if ($variable->contains('=')) {
+ // - SESSION_SECRET=123
+ // - SESSION_SECRET=
+ $key = $variable->before('=');
+ $value = $variable->after('=');
+ } else {
+ // - SESSION_SECRET
+ $key = $variable;
+ $value = null;
+ }
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
- $key = Str::of($variableName);
- $value = Str::of($variable);
+ $key = str($variableName);
+ $value = str($variable);
}
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
@@ -1133,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'key' => $key,
'service_id' => $resource->id,
])->first();
- $value = Str::of(replaceVariables($value));
+ $value = replaceVariables($value);
$key = $value;
if ($value->startsWith('SERVICE_')) {
$foundEnv = EnvironmentVariable::where([
@@ -1166,7 +1963,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// }
} else {
if ($command->value() === 'URL') {
- $fqdn = Str::of($fqdn)->after('://')->value();
+ $fqdn = str($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
@@ -1261,38 +2058,62 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (! $isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
- $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
- uuid: $resource->uuid,
- domains: $fqdns,
- is_force_https_enabled: true,
- serviceLabels: $serviceLabels,
- is_gzip_enabled: $savedService->isGzipEnabled(),
- is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
- service_name: $serviceName,
- image: data_get($service, 'image')
- ));
- $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
- network: $resource->destination->network,
- uuid: $resource->uuid,
- domains: $fqdns,
- is_force_https_enabled: true,
- serviceLabels: $serviceLabels,
- is_gzip_enabled: $savedService->isGzipEnabled(),
- is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
- service_name: $serviceName,
- image: data_get($service, 'image')
- ));
+ $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
+ if ($shouldGenerateLabelsExactly) {
+ switch ($resource->server->proxyType()) {
+ case ProxyTypes::TRAEFIK->value:
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $savedService->isGzipEnabled(),
+ is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: data_get($service, 'image')
+ ));
+ break;
+ case ProxyTypes::CADDY->value:
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
+ network: $resource->destination->network,
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $savedService->isGzipEnabled(),
+ is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: data_get($service, 'image')
+ ));
+ break;
+ }
+ } else {
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $savedService->isGzipEnabled(),
+ is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: data_get($service, 'image')
+ ));
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
+ network: $resource->destination->network,
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ is_force_https_enabled: true,
+ serviceLabels: $serviceLabels,
+ is_gzip_enabled: $savedService->isGzipEnabled(),
+ is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
+ service_name: $serviceName,
+ image: data_get($service, 'image')
+ ));
+ }
}
}
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) {
@@ -1314,31 +2135,73 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_forget($service, 'volumes.*.isDirectory');
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
-
- // Remove unnecessary variables from service.environment
- // $withoutServiceEnvs = collect([]);
- // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
- // ray($key, $value);
- // if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
- // $k = Str::of($value)->before("=");
- // $v = Str::of($value)->after("=");
- // $withoutServiceEnvs->put($k->value(), $v->value());
- // }
- // });
- // ray($withoutServiceEnvs);
- // data_set($service, 'environment', $withoutServiceEnvs->toArray());
+ data_set($service, 'environment', $serviceVariables->toArray());
updateCompose($savedService);
return $service;
});
+
+ $envs_from_coolify = $resource->environment_variables()->get();
+ $services = collect($services)->map(function ($service, $serviceName) use ($resource, $envs_from_coolify) {
+ $serviceVariables = collect(data_get($service, 'environment', []));
+ $parsedServiceVariables = collect([]);
+ foreach ($serviceVariables as $key => $value) {
+ if (is_numeric($key)) {
+ $value = str($value);
+ if ($value->contains('=')) {
+ $key = $value->before('=')->value();
+ $value = $value->after('=')->value();
+ } else {
+ $key = $value->value();
+ $value = null;
+ }
+ $parsedServiceVariables->put($key, $value);
+ } else {
+ $parsedServiceVariables->put($key, $value);
+ }
+ }
+ $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();
+ if ($found_env) {
+ return $found_env->value;
+ }
+ }
+
+ return $value;
+ });
+
+ data_set($service, 'environment', $parsedServiceVariables->toArray());
+
+ return $service;
+ });
$finalServices = [
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
+ 'configs' => $topLevelConfigs->toArray(),
+ 'secrets' => $topLevelSecrets->toArray(),
];
$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();
@@ -1346,14 +2209,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} else {
return collect([]);
}
- } elseif ($resource->getMorphClass() === 'App\Models\Application') {
- $isSameDockerComposeFile = false;
- if ($resource->dockerComposePrLocation() === $resource->dockerComposeLocation()) {
- $isSameDockerComposeFile = true;
- }
+ } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
+ } catch (\Exception) {
return;
}
$server = $resource->destination->server;
@@ -1374,6 +2233,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
+ $topLevelConfigs = collect(data_get($yaml, 'configs', []));
+ $topLevelSecrets = collect(data_get($yaml, 'secrets', []));
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
@@ -1415,128 +2276,259 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$baseName = generateApplicationContainerName($resource, $pull_request_id);
$containerName = "$serviceName-$baseName";
- if (count($serviceVolumes) > 0) {
- $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
- if (is_string($volume)) {
- $volume = str($volume);
- if ($volume->contains(':') && ! $volume->startsWith('/')) {
- $name = $volume->before(':');
- $mount = $volume->after(':');
- if ($name->startsWith('.') || $name->startsWith('~')) {
- $dir = base_configuration_dir().'/applications/'.$resource->uuid;
- if ($name->startsWith('.')) {
- $name = $name->replaceFirst('.', $dir);
- }
- if ($name->startsWith('~')) {
- $name = $name->replaceFirst('~', $dir);
- }
- if ($pull_request_id !== 0) {
- $name = $name."-pr-$pull_request_id";
- }
- $volume = str("$name:$mount");
- } else {
- if ($pull_request_id !== 0) {
- $name = $name."-pr-$pull_request_id";
- $volume = str("$name:$mount");
- if ($topLevelVolumes->has($name)) {
- $v = $topLevelVolumes->get($name);
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($v, 'name', $name);
- data_set($topLevelVolumes, $name, $v);
- }
- }
- } else {
- $topLevelVolumes->put($name, [
- 'name' => $name,
- ]);
- }
- } else {
- if ($topLevelVolumes->has($name->value())) {
- $v = $topLevelVolumes->get($name->value());
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($topLevelVolumes, $name->value(), $v);
- }
- }
- } else {
- $topLevelVolumes->put($name->value(), [
- 'name' => $name->value(),
- ]);
- }
- }
- }
- } else {
- if ($volume->startsWith('/')) {
+ if ($resource->compose_parsing_version === '1') {
+ if (count($serviceVolumes) > 0) {
+ $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
+ if (is_string($volume)) {
+ $volume = str($volume);
+ if ($volume->contains(':') && ! $volume->startsWith('/')) {
$name = $volume->before(':');
$mount = $volume->after(':');
- if ($pull_request_id !== 0) {
- $name = $name."-pr-$pull_request_id";
- }
- $volume = str("$name:$mount");
- }
- }
- } elseif (is_array($volume)) {
- $source = data_get($volume, 'source');
- $target = data_get($volume, 'target');
- $read_only = data_get($volume, 'read_only');
- if ($source && $target) {
- if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) {
- $dir = base_configuration_dir().'/applications/'.$resource->uuid;
- if (str($source, '.')) {
- $source = str($source)->replaceFirst('.', $dir);
- }
- if (str($source, '~')) {
- $source = str($source)->replaceFirst('~', $dir);
- }
- if ($pull_request_id !== 0) {
- $source = $source."-pr-$pull_request_id";
- }
- if ($read_only) {
- data_set($volume, 'source', $source.':'.$target.':ro');
+ if ($name->startsWith('.') || $name->startsWith('~')) {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ if ($name->startsWith('.')) {
+ $name = $name->replaceFirst('.', $dir);
+ }
+ if ($name->startsWith('~')) {
+ $name = $name->replaceFirst('~', $dir);
+ }
+ if ($pull_request_id !== 0) {
+ $name = $name."-pr-$pull_request_id";
+ }
+ $volume = str("$name:$mount");
} else {
- data_set($volume, 'source', $source.':'.$target);
- }
- } else {
- if ($pull_request_id !== 0) {
- $source = $source."-pr-$pull_request_id";
- }
- if ($read_only) {
- data_set($volume, 'source', $source.':'.$target.':ro');
- } else {
- data_set($volume, 'source', $source.':'.$target);
- }
- if (! str($source)->startsWith('/')) {
- if ($topLevelVolumes->has($source)) {
- $v = $topLevelVolumes->get($source);
- if (data_get($v, 'driver_opts.type') === 'cifs') {
- // Do nothing
- } else {
- if (is_null(data_get($v, 'name'))) {
- data_set($v, 'name', $source);
- data_set($topLevelVolumes, $source, $v);
+ if ($pull_request_id !== 0) {
+ $name = $name."-pr-$pull_request_id";
+ $volume = str("$name:$mount");
+ if ($topLevelVolumes->has($name)) {
+ $v = $topLevelVolumes->get($name);
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($v, 'name', $name);
+ data_set($topLevelVolumes, $name, $v);
+ }
}
+ } else {
+ $topLevelVolumes->put($name, [
+ 'name' => $name,
+ ]);
}
} else {
- $topLevelVolumes->put($source, [
- 'name' => $source,
- ]);
+ if ($topLevelVolumes->has($name->value())) {
+ $v = $topLevelVolumes->get($name->value());
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($topLevelVolumes, $name->value(), $v);
+ }
+ }
+ } else {
+ $topLevelVolumes->put($name->value(), [
+ 'name' => $name->value(),
+ ]);
+ }
+ }
+ }
+ } else {
+ if ($volume->startsWith('/')) {
+ $name = $volume->before(':');
+ $mount = $volume->after(':');
+ if ($pull_request_id !== 0) {
+ $name = $name."-pr-$pull_request_id";
+ }
+ $volume = str("$name:$mount");
+ }
+ }
+ } elseif (is_array($volume)) {
+ $source = data_get($volume, 'source');
+ $target = data_get($volume, 'target');
+ $read_only = data_get($volume, 'read_only');
+ if ($source && $target) {
+ if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ if (str($source, '.')) {
+ $source = str($source)->replaceFirst('.', $dir);
+ }
+ if (str($source, '~')) {
+ $source = str($source)->replaceFirst('~', $dir);
+ }
+ if ($pull_request_id !== 0) {
+ $source = $source."-pr-$pull_request_id";
+ }
+ if ($read_only) {
+ data_set($volume, 'source', $source.':'.$target.':ro');
+ } else {
+ data_set($volume, 'source', $source.':'.$target);
+ }
+ } else {
+ if ($pull_request_id !== 0) {
+ $source = $source."-pr-$pull_request_id";
+ }
+ if ($read_only) {
+ data_set($volume, 'source', $source.':'.$target.':ro');
+ } else {
+ data_set($volume, 'source', $source.':'.$target);
+ }
+ if (! str($source)->startsWith('/')) {
+ if ($topLevelVolumes->has($source)) {
+ $v = $topLevelVolumes->get($source);
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($v, 'name', $source);
+ data_set($topLevelVolumes, $source, $v);
+ }
+ }
+ } else {
+ $topLevelVolumes->put($source, [
+ 'name' => $source,
+ ]);
+ }
}
}
}
}
- }
- if (is_array($volume)) {
- return data_get($volume, 'source');
- }
+ if (is_array($volume)) {
+ return data_get($volume, 'source');
+ }
- return $volume->value();
- });
- data_set($service, 'volumes', $serviceVolumes->toArray());
+ return $volume->value();
+ });
+ data_set($service, 'volumes', $serviceVolumes->toArray());
+ }
+ } elseif ($resource->compose_parsing_version === '2') {
+ if (count($serviceVolumes) > 0) {
+ $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) {
+ if (is_string($volume)) {
+ $volume = str($volume);
+ if ($volume->contains(':') && ! $volume->startsWith('/')) {
+ $name = $volume->before(':');
+ $mount = $volume->after(':');
+ if ($name->startsWith('.') || $name->startsWith('~')) {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ if ($name->startsWith('.')) {
+ $name = $name->replaceFirst('.', $dir);
+ }
+ if ($name->startsWith('~')) {
+ $name = $name->replaceFirst('~', $dir);
+ }
+ if ($pull_request_id !== 0) {
+ $name = $name."-pr-$pull_request_id";
+ }
+ $volume = str("$name:$mount");
+ } else {
+ if ($pull_request_id !== 0) {
+ $uuid = $resource->uuid;
+ $name = $uuid."-$name-pr-$pull_request_id";
+ $volume = str("$name:$mount");
+ if ($topLevelVolumes->has($name)) {
+ $v = $topLevelVolumes->get($name);
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($v, 'name', $name);
+ data_set($topLevelVolumes, $name, $v);
+ }
+ }
+ } else {
+ $topLevelVolumes->put($name, [
+ 'name' => $name,
+ ]);
+ }
+ } else {
+ $uuid = $resource->uuid;
+ $name = str($uuid."-$name");
+ $volume = str("$name:$mount");
+ if ($topLevelVolumes->has($name->value())) {
+ $v = $topLevelVolumes->get($name->value());
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($topLevelVolumes, $name->value(), $v);
+ }
+ }
+ } else {
+ $topLevelVolumes->put($name->value(), [
+ 'name' => $name->value(),
+ ]);
+ }
+ }
+ }
+ } else {
+ if ($volume->startsWith('/')) {
+ $name = $volume->before(':');
+ $mount = $volume->after(':');
+ if ($pull_request_id !== 0) {
+ $name = $name."-pr-$pull_request_id";
+ }
+ $volume = str("$name:$mount");
+ }
+ }
+ } elseif (is_array($volume)) {
+ $source = data_get($volume, 'source');
+ $target = data_get($volume, 'target');
+ $read_only = data_get($volume, 'read_only');
+ if ($source && $target) {
+ $uuid = $resource->uuid;
+ if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) {
+ $dir = base_configuration_dir().'/applications/'.$resource->uuid;
+ if (str($source, '.')) {
+ $source = str($source)->replaceFirst('.', $dir);
+ }
+ if (str($source, '~')) {
+ $source = str($source)->replaceFirst('~', $dir);
+ }
+ if ($read_only) {
+ data_set($volume, 'source', $source.':'.$target.':ro');
+ } else {
+ data_set($volume, 'source', $source.':'.$target);
+ }
+ } else {
+ if ($pull_request_id === 0) {
+ $source = $uuid."-$source";
+ } else {
+ $source = $uuid."-$source-pr-$pull_request_id";
+ }
+ if ($read_only) {
+ data_set($volume, 'source', $source.':'.$target.':ro');
+ } else {
+ data_set($volume, 'source', $source.':'.$target);
+ }
+ if (! str($source)->startsWith('/')) {
+ if ($topLevelVolumes->has($source)) {
+ $v = $topLevelVolumes->get($source);
+ if (data_get($v, 'driver_opts.type') === 'cifs') {
+ // Do nothing
+ } else {
+ if (is_null(data_get($v, 'name'))) {
+ data_set($v, 'name', $source);
+ data_set($topLevelVolumes, $source, $v);
+ }
+ }
+ } else {
+ $topLevelVolumes->put($source, [
+ 'name' => $source,
+ ]);
+ }
+ }
+ }
+ }
+ }
+ if (is_array($volume)) {
+ return data_get($volume, 'source');
+ }
+ dispatch(new ServerFilesFromServerJob($resource));
+
+ return $volume->value();
+ });
+ data_set($service, 'volumes', $serviceVolumes->toArray());
+ }
}
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
@@ -1564,7 +2556,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);
+ }
}
}
}
@@ -1584,7 +2578,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;
@@ -1633,22 +2627,29 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Get variables from the service
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
- $variable = Str::of($variable);
- if ($variable->contains('=')) {
- // - SESSION_SECRET=123
- // - SESSION_SECRET=
- $key = $variable->before('=');
- $value = $variable->after('=');
+ if (is_array($variable)) {
+ // - SESSION_SECRET: 123
+ // - SESSION_SECRET:
+ $key = str(collect($variable)->keys()->first());
+ $value = str(collect($variable)->values()->first());
} else {
- // - SESSION_SECRET
- $key = $variable;
- $value = null;
+ $variable = str($variable);
+ if ($variable->contains('=')) {
+ // - SESSION_SECRET=123
+ // - SESSION_SECRET=
+ $key = $variable->before('=');
+ $value = $variable->after('=');
+ } else {
+ // - SESSION_SECRET
+ $key = $variable;
+ $value = null;
+ }
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
- $key = Str::of($variableName);
- $value = Str::of($variable);
+ $key = str($variableName);
+ $value = str($variable);
}
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew) {
@@ -1692,7 +2693,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'application_id' => $resource->id,
'is_preview' => false,
])->first();
- $value = Str::of(replaceVariables($value));
+ $value = replaceVariables($value);
$key = $value;
if ($value->startsWith('SERVICE_')) {
$foundEnv = EnvironmentVariable::where([
@@ -1714,7 +2715,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdn = data_get($foundEnv, 'value');
} else {
if ($command?->value() === 'URL') {
- $fqdn = Str::of($fqdn)->after('://')->value();
+ $fqdn = str($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
@@ -1808,7 +2809,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$template = $resource->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
- $random = new Cuid2(7);
+ $random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
@@ -1820,35 +2821,74 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
});
}
}
- $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
- uuid: $resource->uuid,
- domains: $fqdns,
- serviceLabels: $serviceLabels,
- generate_unique_uuid: $resource->build_pack === 'dockercompose',
- image: data_get($service, 'image')
- ));
- $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
- network: $resource->destination->network,
- uuid: $resource->uuid,
- domains: $fqdns,
- serviceLabels: $serviceLabels,
- image: data_get($service, 'image')
- ));
+ $shouldGenerateLabelsExactly = $server->settings->generate_exact_labels;
+ if ($shouldGenerateLabelsExactly) {
+ switch ($server->proxyType()) {
+ case ProxyTypes::TRAEFIK->value:
+ $serviceLabels = $serviceLabels->merge(
+ fqdnLabelsForTraefik(
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ serviceLabels: $serviceLabels,
+ generate_unique_uuid: $resource->build_pack === 'dockercompose',
+ image: data_get($service, 'image'),
+ is_force_https_enabled: $resource->isForceHttpsEnabled(),
+ is_gzip_enabled: $resource->isGzipEnabled(),
+ is_stripprefix_enabled: $resource->isStripprefixEnabled(),
+ )
+ );
+ break;
+ case ProxyTypes::CADDY->value:
+ $serviceLabels = $serviceLabels->merge(
+ fqdnLabelsForCaddy(
+ network: $resource->destination->network,
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ serviceLabels: $serviceLabels,
+ image: data_get($service, 'image'),
+ is_force_https_enabled: $resource->isForceHttpsEnabled(),
+ is_gzip_enabled: $resource->isGzipEnabled(),
+ is_stripprefix_enabled: $resource->isStripprefixEnabled(),
+ )
+ );
+ break;
+ }
+ } else {
+ $serviceLabels = $serviceLabels->merge(
+ fqdnLabelsForTraefik(
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ serviceLabels: $serviceLabels,
+ generate_unique_uuid: $resource->build_pack === 'dockercompose',
+ image: data_get($service, 'image'),
+ is_force_https_enabled: $resource->isForceHttpsEnabled(),
+ is_gzip_enabled: $resource->isGzipEnabled(),
+ is_stripprefix_enabled: $resource->isStripprefixEnabled(),
+ )
+ );
+ $serviceLabels = $serviceLabels->merge(
+ fqdnLabelsForCaddy(
+ network: $resource->destination->network,
+ uuid: $resource->uuid,
+ domains: $fqdns,
+ serviceLabels: $serviceLabels,
+ image: data_get($service, 'image'),
+ is_force_https_enabled: $resource->isForceHttpsEnabled(),
+ is_gzip_enabled: $resource->isGzipEnabled(),
+ is_stripprefix_enabled: $resource->isStripprefixEnabled(),
+ )
+ );
+ }
}
}
}
$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) {
@@ -1878,407 +2918,1228 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
+ 'configs' => $topLevelConfigs->toArray(),
+ 'secrets' => $topLevelSecrets->toArray(),
];
- if ($isSameDockerComposeFile) {
- $resource->docker_compose_pr_raw = Yaml::dump($yaml, 10, 2);
- $resource->docker_compose_pr = Yaml::dump($finalServices, 10, 2);
- $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) {
+ 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::class && $value->startsWith('/')) {
+ $path = $value->value();
+ if ($path !== '/') {
+ $fqdn = "$fqdn$path";
+ }
+ }
+ $fqdnWithPort = $fqdn;
+ if ($port) {
+ $fqdnWithPort = "$fqdn:$port";
+ }
+ if ($isApplication && is_null($resource->fqdn)) {
+ data_forget($resource, 'environment_variables');
+ data_forget($resource, 'environment_variables_preview');
+ $resource->fqdn = $fqdnWithPort;
+ $resource->save();
+ } elseif ($isService && is_null($savedService->fqdn)) {
+ $savedService->fqdn = $fqdnWithPort;
+ $savedService->save();
+ }
+
+ if (substr_count(str($key)->value(), '_') === 2) {
+ $resource->environment_variables()->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");
+ }
+ $fqdn = str($fqdn)->replace('http://', '')->replace('https://', '');
+ $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('$')) {
+ $isRequired = false;
+ 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(':?');
+ $isRequired = true;
+ } elseif ($value->contains('?')) {
+ $value = replaceVariables($value);
+
+ $key = $value->before('?');
+ $value = $value->after('?');
+ $isRequired = true;
+ }
+ 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,
+ 'is_required' => $isRequired,
+ ]);
+ // 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,
+ 'is_required' => $isRequired,
+ ]);
+ }
+ }
+ }
+ 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 [
+ '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 isAssociativeArray($array)
+{
+ if ($array instanceof Collection) {
+ $array = $array->toArray();
+ }
+
+ 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);
+}
+
+/**
+ * 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)
+{
+ // Currently disabled
+ return;
+ 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 {
+ $where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\"");
+ }
+ }
+ 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 {
+ $where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\"");
+ }
+ }
+ 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 {
+ // 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);
+ }
+ }
+ }
+ }
+
+ return $convertedServiceVariables;
+}
+function instanceSettings()
+{
+ return InstanceSettings::get();
+}
+
+function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
+{
+ $server = Server::find($server_id)->where('team_id', $team_id)->first();
+ if (! $server) {
+ return;
+ }
+ $uuid = new Cuid2;
+ $cloneCommand = "git clone --no-checkout -b $branch $repository .";
+ $workdir = rtrim($base_directory, '/');
+ $fileList = collect([".$workdir/coolify.json"]);
+ $commands = collect([
+ "rm -rf /tmp/{$uuid}",
+ "mkdir -p /tmp/{$uuid}",
+ "cd /tmp/{$uuid}",
+ $cloneCommand,
+ 'git sparse-checkout init --cone',
+ "git sparse-checkout set {$fileList->implode(' ')}",
+ 'git read-tree -mu HEAD',
+ "cat .$workdir/coolify.json",
+ 'rm -rf /tmp/{$uuid}',
+ ]);
+ try {
+ return instant_remote_process($commands, $server);
+ } catch (\Exception) {
+ // continue
+ }
+}
+
+function loggy($message = null, array $context = [])
+{
+ if (! isDev()) {
+ return;
+ }
+ if (function_exists('ray') && config('app.debug')) {
+ ray($message, $context);
+ }
+ if (is_null($message)) {
+ return app('log');
+ }
+
+ return app('log')->debug($message, $context);
+}
+function sslipDomainWarning(string $domains)
+{
+ $domains = str($domains)->trim()->explode(',');
+ $showSslipHttpsWarning = false;
+ $domains->each(function ($domain) use (&$showSslipHttpsWarning) {
+ if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
+ $showSslipHttpsWarning = true;
+ }
+ });
+
+ return $showSslipHttpsWarning;
+}
+
+function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool
+{
+ if (isDev()) {
+ $decaySeconds = 120;
+ }
+ $rateLimited = false;
+ $executed = RateLimiter::attempt(
+ $limiterKey,
+ $maxAttempts = 0,
+ function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
+ isDev() && loggy('Rate limit not reached for '.$limiterKey);
+ $rateLimited = false;
+
+ if ($callbackOnSuccess) {
+ $callbackOnSuccess();
+ }
+ },
+ $decaySeconds,
+ );
+ if (! $executed) {
+ isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.');
+ $rateLimited = true;
+ }
+
+ return $rateLimited;
+}
+
+function defaultNginxConfiguration(): string
+{
+ return 'server {
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/index.html =404;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ try_files $uri @redirect_to_index;
+ internal;
+ }
+
+ error_page 404 = @handle_404;
+
+ location @handle_404 {
+ root /usr/share/nginx/html;
+ try_files /404.html @redirect_to_index;
+ internal;
+ }
+
+ location @redirect_to_index {
+ return 302 /;
+ }
+}';
+}
+
+function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array
+{
+ $repository = $gitRepository;
+ $providerInfo = [
+ 'host' => null,
+ 'user' => 'git',
+ 'port' => 22,
+ 'repository' => $gitRepository,
+ ];
+ $sshMatches = [];
+ $matches = [];
+
+ // Let's try and parse the string to detect if it's a valid SSH string or not
+ preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches);
+
+ if ($deploymentType === 'deploy_key' && empty($sshMatches) && $source) {
+ // If this happens, the user may have provided an HTTP URL when they needed an SSH one
+ // Let's try and fix that for known Git providers
+ switch ($source->getMorphClass()) {
+ case \App\Models\GithubApp::class:
+ $providerInfo['host'] = Url::fromString($source->html_url)->getHost();
+ $providerInfo['port'] = $source->custom_port;
+ $providerInfo['user'] = $source->custom_user;
+ break;
+ }
+ if (! empty($providerInfo['host'])) {
+ // Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22
+ if ($providerInfo['port'] === 22) {
+ $repository = "{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['repository']}";
+ } else {
+ $repository = "ssh://{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['port']}/{$providerInfo['repository']}";
+ }
+ }
+ }
+
+ preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches);
+
+ if (count($matches) === 1) {
+ $providerInfo['port'] = $matches[0];
+ $gitHost = str($gitRepository)->before(':');
+ $gitRepo = str($gitRepository)->after('/');
+ $repository = "$gitHost:$gitRepo";
+ }
+
return [
- 'command' => $command,
- 'forService' => $forService,
- 'generatedValue' => $generatedValue,
- 'port' => $port,
+ 'repository' => $repository,
+ 'port' => $providerInfo['port'],
];
}
-function generateEnvValue(string $command, ?Service $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);
- 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::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) {
- }
- }
- 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 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');
- ray($domains);
- $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::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
-{
- $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 = 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);
-}
diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php
index a23dc24d3..cad9de7fa 100644
--- a/bootstrap/helpers/socialite.php
+++ b/bootstrap/helpers/socialite.php
@@ -7,7 +7,7 @@ function get_socialite_provider(string $provider)
{
$oauth_setting = OauthSetting::firstWhere('provider', $provider);
- if ($provider == 'azure') {
+ if ($provider === 'azure') {
$azure_config = new \SocialiteProviders\Manager\Config(
$oauth_setting->client_id,
$oauth_setting->client_secret,
diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php
index 224a65f0a..8ddb1331c 100644
--- a/bootstrap/helpers/subscriptions.php
+++ b/bootstrap/helpers/subscriptions.php
@@ -1,51 +1,8 @@
user()->id;
- $team_id = currentTeam()->id ?? null;
- $email = auth()->user()->email ?? null;
- $name = auth()->user()->name ?? null;
- $url = "https://store.coollabs.io/checkout/buy/$checkout_id?";
- if ($user_id) {
- $url .= "&checkout[custom][user_id]={$user_id}";
- }
- if (isset($team_id)) {
- $url .= "&checkout[custom][team_id]={$team_id}";
- }
- if ($email) {
- $url .= "&checkout[email]={$email}";
- }
- if ($name) {
- $url .= "&checkout[name]={$name}";
- }
-
- return $url;
-}
-
-function getPaymentLink()
-{
- return currentTeam()->subscription->lemon_update_payment_menthod_url;
-}
-
-function getRenewDate()
-{
- return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
-}
-
-function getEndDate()
-{
- return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s');
-}
-
function isSubscriptionActive()
{
if (! isCloud()) {
@@ -60,12 +17,6 @@ function isSubscriptionActive()
if (is_null($subscription)) {
return false;
}
- if (isLemon()) {
- return $subscription->lemon_status === 'active';
- }
- // if (isPaddle()) {
- // return $subscription->paddle_status === 'active';
- // }
if (isStripe()) {
return $subscription->stripe_invoice_paid === true;
}
@@ -82,12 +33,6 @@ function isSubscriptionOnGracePeriod()
if (! $subscription) {
return false;
}
- if (isLemon()) {
- $is_still_grace_period = $subscription->lemon_ends_at &&
- Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
-
- return $is_still_grace_period;
- }
if (isStripe()) {
return $subscription->stripe_cancel_at_period_end;
}
@@ -98,18 +43,10 @@ function subscriptionProvider()
{
return config('subscription.provider');
}
-function isLemon()
-{
- return config('subscription.provider') === 'lemon';
-}
function isStripe()
{
return config('subscription.provider') === 'stripe';
}
-function isPaddle()
-{
- return config('subscription.provider') === 'paddle';
-}
function getStripeCustomerPortalSession(Team $team)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
@@ -118,12 +55,11 @@ function getStripeCustomerPortalSession(Team $team)
if (! $stripe_customer_id) {
return null;
}
- $session = \Stripe\BillingPortal\Session::create([
+
+ return \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
'return_url' => $return_url,
]);
-
- return $session;
}
function allowedPathsForUnsubscribedAccounts()
{
diff --git a/composer.json b/composer.json
index b49f9668a..2bae1149c 100644
--- a/composer.json
+++ b/composer.json
@@ -1,38 +1,43 @@
{
- "name": "laravel/laravel",
+ "name": "coollabsio/coolify",
+ "description": "The Coolify project.",
+ "license": "Apache-2.0",
"type": "project",
- "description": "The Laravel Framework.",
"keywords": [
- "framework",
- "laravel"
+ "coolify",
+ "deployment",
+ "docker",
+ "self-hosted",
+ "server"
],
- "license": "MIT",
"require": {
"php": "^8.2",
"danharrin/livewire-rate-limiting": "^1.1",
"doctrine/dbal": "^3.6",
"guzzlehttp/guzzle": "^7.5.0",
- "laravel/fortify": "^v1.16.0",
- "laravel/framework": "^v10.7.1",
- "laravel/horizon": "^5.23.1",
+ "laravel/fortify": "^1.16.0",
+ "laravel/framework": "^11",
+ "laravel/horizon": "^5.29.1",
+ "laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6",
- "laravel/sanctum": "^v3.2.1",
- "laravel/socialite": "^v5.14.0",
- "laravel/tinker": "^v2.8.1",
+ "laravel/sanctum": "^4.0",
+ "laravel/socialite": "^5.14.0",
+ "laravel/tinker": "^2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "3.4.9",
+ "log1x/laravel-webfonts": "^1.0",
"lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2",
- "phpseclib/phpseclib": "~3.0",
+ "phpseclib/phpseclib": "^3.0",
"pion/laravel-chunk-upload": "^1.5",
"poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1",
"pusher/pusher-php-server": "^7.2",
- "resend/resend-laravel": "^0.5.0",
- "sentry/sentry-laravel": "^3.4",
+ "resend/resend-laravel": "^0.13.0",
+ "sentry/sentry-laravel": "^4.6",
"socialiteproviders/microsoft-azure": "^5.1",
"spatie/laravel-activitylog": "^4.7.3",
"spatie/laravel-data": "^3.4.3",
@@ -42,67 +47,76 @@
"stripe/stripe-php": "^12.0",
"symfony/yaml": "^6.2",
"visus/cuid2": "^2.0.0",
- "yosymfony/toml": "^1.0"
+ "yosymfony/toml": "^1.0",
+ "zircote/swagger-php": "^4.10"
},
"require-dev": {
- "fakerphp/faker": "^v1.21.0",
- "laravel/dusk": "^v7.7.0",
+ "barryvdh/laravel-debugbar": "^3.13",
+ "fakerphp/faker": "^1.21.0",
+ "laravel/dusk": "^8.0",
"laravel/pint": "^1.16",
+ "laravel/telescope": "^5.2",
"mockery/mockery": "^1.5.1",
- "nunomaduro/collision": "^v7.4.0",
+ "nunomaduro/collision": "^8.1",
"pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19",
- "serversideup/spin": "^v1.1.0",
+ "serversideup/spin": "^1.1.0",
"spatie/laravel-ignition": "^2.1.0",
"symfony/http-client": "^6.2"
},
+ "minimum-stability": "stable",
+ "prefer-stable": true,
"autoload": {
- "files": [
- "bootstrap/includeHelpers.php"
- ],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
- }
+ },
+ "files": [
+ "bootstrap/includeHelpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true,
+ "php-http/discovery": true
+ },
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "extra": {
+ "laravel": {
+ "dont-discover": [
+ "laravel/telescope"
+ ]
+ }
+ },
"scripts": {
- "post-autoload-dump": [
- "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover --ansi"
+ "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-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate"
],
- "post-install-cmd": [],
+ "post-autoload-dump": [
+ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
+ "@php artisan package:discover --ansi"
+ ],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
- },
- "extra": {
- "laravel": {
- "dont-discover": []
- }
- },
- "config": {
- "optimize-autoloader": true,
- "preferred-install": "dist",
- "sort-packages": true,
- "allow-plugins": {
- "pestphp/pest-plugin": true,
- "php-http/discovery": true
- }
- },
- "minimum-stability": "stable",
- "prefer-stable": true
-}
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
index 9d04e9ec7..5eb03b5fc 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": "dbce9f366320f4d58392673fe25c69f6",
+ "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962",
"packages": [
{
"name": "amphp/amp",
@@ -229,16 +229,16 @@
},
{
"name": "amphp/dns",
- "version": "v2.1.2",
+ "version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/dns.git",
- "reference": "04c88e67bef804203df934703bd422ea72f46b0e"
+ "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/dns/zipball/04c88e67bef804203df934703bd422ea72f46b0e",
- "reference": "04c88e67bef804203df934703bd422ea72f46b0e",
+ "url": "https://api.github.com/repos/amphp/dns/zipball/758266b0ea7470e2e42cd098493bc6d6c7100cf7",
+ "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7",
"shasum": ""
},
"require": {
@@ -305,7 +305,7 @@
],
"support": {
"issues": "https://github.com/amphp/dns/issues",
- "source": "https://github.com/amphp/dns/tree/v2.1.2"
+ "source": "https://github.com/amphp/dns/tree/v2.2.0"
},
"funding": [
{
@@ -313,20 +313,20 @@
"type": "github"
}
],
- "time": "2024-04-19T03:49:29+00:00"
+ "time": "2024-06-02T19:54:12+00:00"
},
{
"name": "amphp/parallel",
- "version": "v2.2.9",
+ "version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/amphp/parallel.git",
- "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238"
+ "reference": "9777db1460d1535bc2a843840684fb1205225b87"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/parallel/zipball/73d293f1fc4df1bebc3c4fce1432e82dd7032238",
- "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238",
+ "url": "https://api.github.com/repos/amphp/parallel/zipball/9777db1460d1535bc2a843840684fb1205225b87",
+ "reference": "9777db1460d1535bc2a843840684fb1205225b87",
"shasum": ""
},
"require": {
@@ -389,7 +389,7 @@
],
"support": {
"issues": "https://github.com/amphp/parallel/issues",
- "source": "https://github.com/amphp/parallel/tree/v2.2.9"
+ "source": "https://github.com/amphp/parallel/tree/v2.3.0"
},
"funding": [
{
@@ -397,7 +397,7 @@
"type": "github"
}
],
- "time": "2024-03-24T18:27:44+00:00"
+ "time": "2024-09-14T19:16:14+00:00"
},
{
"name": "amphp/parser",
@@ -463,16 +463,16 @@
},
{
"name": "amphp/pipeline",
- "version": "v1.2.0",
+ "version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/amphp/pipeline.git",
- "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b"
+ "reference": "66c095673aa5b6e689e63b52d19e577459129ab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/pipeline/zipball/f1c2ce35d27ae86ead018adb803eccca7421dd9b",
- "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b",
+ "url": "https://api.github.com/repos/amphp/pipeline/zipball/66c095673aa5b6e689e63b52d19e577459129ab3",
+ "reference": "66c095673aa5b6e689e63b52d19e577459129ab3",
"shasum": ""
},
"require": {
@@ -518,7 +518,7 @@
],
"support": {
"issues": "https://github.com/amphp/pipeline/issues",
- "source": "https://github.com/amphp/pipeline/tree/v1.2.0"
+ "source": "https://github.com/amphp/pipeline/tree/v1.2.1"
},
"funding": [
{
@@ -526,7 +526,7 @@
"type": "github"
}
],
- "time": "2024-03-10T14:48:16+00:00"
+ "time": "2024-07-04T00:56:47+00:00"
},
{
"name": "amphp/process",
@@ -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",
@@ -867,16 +867,16 @@
},
{
"name": "aws/aws-crt-php",
- "version": "v1.2.5",
+ "version": "v1.2.6",
"source": {
"type": "git",
"url": "https://github.com/awslabs/aws-crt-php.git",
- "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b"
+ "reference": "a63485b65b6b3367039306496d49737cf1995408"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b",
- "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b",
+ "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408",
+ "reference": "a63485b65b6b3367039306496d49737cf1995408",
"shasum": ""
},
"require": {
@@ -915,22 +915,22 @@
],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
- "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5"
+ "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6"
},
- "time": "2024-04-19T21:30:56+00:00"
+ "time": "2024-06-13T17:21:28+00:00"
},
{
"name": "aws/aws-sdk-php",
- "version": "3.308.4",
+ "version": "3.324.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6"
+ "reference": "b258712f0d986e00e1143d55246b6f9e344c7184"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6",
- "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b258712f0d986e00e1143d55246b6f9e344c7184",
+ "reference": "b258712f0d986e00e1143d55246b6f9e344c7184",
"shasum": ""
},
"require": {
@@ -983,7 +983,10 @@
],
"psr-4": {
"Aws\\": "src/"
- }
+ },
+ "exclude-from-classmap": [
+ "src/data/"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -1010,22 +1013,22 @@
"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.308.4"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.324.0"
},
- "time": "2024-05-28T18:05:38+00:00"
+ "time": "2024-10-10T18:06:36+00:00"
},
{
"name": "bacon/bacon-qr-code",
- "version": "v3.0.0",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
- "reference": "510de6eca6248d77d31b339d62437cc995e2fb41"
+ "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41",
- "reference": "510de6eca6248d77d31b339d62437cc995e2fb41",
+ "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
+ "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
"shasum": ""
},
"require": {
@@ -1064,9 +1067,9 @@
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
- "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0"
+ "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
},
- "time": "2024-04-18T11:16:25+00:00"
+ "time": "2024-10-01T13:55:55+00:00"
},
{
"name": "brick/math",
@@ -1197,72 +1200,6 @@
],
"time": "2023-12-11T17:09:12+00:00"
},
- {
- "name": "clue/stream-filter",
- "version": "v1.7.0",
- "source": {
- "type": "git",
- "url": "https://github.com/clue/stream-filter.git",
- "reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
- "reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/functions_include.php"
- ],
- "psr-4": {
- "Clue\\StreamFilter\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Christian Lück",
- "email": "christian@clue.engineering"
- }
- ],
- "description": "A simple and modern approach to stream filtering in PHP",
- "homepage": "https://github.com/clue/stream-filter",
- "keywords": [
- "bucket brigade",
- "callback",
- "filter",
- "php_user_filter",
- "stream",
- "stream_filter_append",
- "stream_filter_register"
- ],
- "support": {
- "issues": "https://github.com/clue/stream-filter/issues",
- "source": "https://github.com/clue/stream-filter/tree/v1.7.0"
- },
- "funding": [
- {
- "url": "https://clue.engineering/support",
- "type": "custom"
- },
- {
- "url": "https://github.com/clue",
- "type": "github"
- }
- ],
- "time": "2023-12-20T15:40:13+00:00"
- },
{
"name": "danharrin/livewire-rate-limiting",
"version": "v1.3.1",
@@ -1319,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",
@@ -1363,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",
@@ -1413,16 +1350,16 @@
},
{
"name": "dflydev/dot-access-data",
- "version": "v3.0.2",
+ "version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
- "reference": "f41715465d65213d644d3141a6a93081be5d3549"
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549",
- "reference": "f41715465d65213d644d3141a6a93081be5d3549",
+ "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
@@ -1482,9 +1419,9 @@
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
- "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2"
+ "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
- "time": "2022-10-27T11:44:00+00:00"
+ "time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/cache",
@@ -1581,16 +1518,16 @@
},
{
"name": "doctrine/dbal",
- "version": "3.8.4",
+ "version": "3.9.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
- "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd"
+ "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/b05e48a745f722801f55408d0dbd8003b403dbbd",
- "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba",
+ "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba",
"shasum": ""
},
"require": {
@@ -1606,12 +1543,12 @@
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
- "phpstan/phpstan": "1.10.58",
- "phpstan/phpstan-strict-rules": "^1.5",
- "phpunit/phpunit": "9.6.16",
+ "phpstan/phpstan": "1.12.6",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "phpunit/phpunit": "9.6.20",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
- "squizlabs/php_codesniffer": "3.9.0",
+ "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"
@@ -1674,7 +1611,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
- "source": "https://github.com/doctrine/dbal/tree/3.8.4"
+ "source": "https://github.com/doctrine/dbal/tree/3.9.3"
},
"funding": [
{
@@ -1690,7 +1627,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-25T07:04:44+00:00"
+ "time": "2024-10-10T17:56:43+00:00"
},
{
"name": "doctrine/deprecations",
@@ -2000,16 +1937,16 @@
},
{
"name": "dragonmantank/cron-expression",
- "version": "v3.3.3",
+ "version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
- "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a"
+ "reference": "8c784d071debd117328803d86b2097615b457500"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
- "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500",
+ "reference": "8c784d071debd117328803d86b2097615b457500",
"shasum": ""
},
"require": {
@@ -2022,10 +1959,14 @@
"require-dev": {
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.0",
- "phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
@@ -2049,7 +1990,7 @@
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
- "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3"
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0"
},
"funding": [
{
@@ -2057,7 +1998,7 @@
"type": "github"
}
],
- "time": "2023-08-10T19:36:49+00:00"
+ "time": "2024-10-09T13:47:03+00:00"
},
{
"name": "egulias/email-validator",
@@ -2262,24 +2203,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": {
@@ -2308,7 +2249,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": [
{
@@ -2320,26 +2261,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"
@@ -2350,9 +2291,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": {
@@ -2430,7 +2371,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": [
{
@@ -2446,20 +2387,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": {
@@ -2467,7 +2408,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": {
@@ -2513,7 +2454,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": [
{
@@ -2529,20 +2470,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": {
@@ -2557,8 +2498,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"
@@ -2629,7 +2570,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": [
{
@@ -2645,7 +2586,7 @@
"type": "tidelift"
}
],
- "time": "2023-12-03T20:05:35+00:00"
+ "time": "2024-07-18T11:15:46+00:00"
},
{
"name": "guzzlehttp/uri-template",
@@ -2733,64 +2674,6 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
- {
- "name": "http-interop/http-factory-guzzle",
- "version": "1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/http-interop/http-factory-guzzle.git",
- "reference": "8f06e92b95405216b237521cc64c804dd44c4a81"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81",
- "reference": "8f06e92b95405216b237521cc64c804dd44c4a81",
- "shasum": ""
- },
- "require": {
- "guzzlehttp/psr7": "^1.7||^2.0",
- "php": ">=7.3",
- "psr/http-factory": "^1.0"
- },
- "provide": {
- "psr/http-factory-implementation": "^1.0"
- },
- "require-dev": {
- "http-interop/http-factory-tests": "^0.9",
- "phpunit/phpunit": "^9.5"
- },
- "suggest": {
- "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Http\\Factory\\Guzzle\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
- }
- ],
- "description": "An HTTP Factory using Guzzle PSR7",
- "keywords": [
- "factory",
- "http",
- "psr-17",
- "psr-7"
- ],
- "support": {
- "issues": "https://github.com/http-interop/http-factory-guzzle/issues",
- "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0"
- },
- "time": "2021-07-21T13:50:14+00:00"
- },
{
"name": "jean85/pretty-package-versions",
"version": "2.0.6",
@@ -2910,16 +2793,16 @@
},
{
"name": "laravel/fortify",
- "version": "v1.21.3",
+ "version": "v1.24.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
- "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1"
+ "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/fortify/zipball/a725684d17959c4750f3b441ff2e94ecde7793a1",
- "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1",
+ "url": "https://api.github.com/repos/laravel/fortify/zipball/42695c45087e5abb3e173725b4f1ef4956a7b47d",
+ "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d",
"shasum": ""
},
"require": {
@@ -2971,20 +2854,20 @@
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
- "time": "2024-05-08T18:07:38+00:00"
+ "time": "2024-09-16T19:20:52+00:00"
},
{
"name": "laravel/framework",
- "version": "v10.48.12",
+ "version": "v11.27.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "590afea38e708022662629fbf5184351fa82cf08"
+ "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/590afea38e708022662629fbf5184351fa82cf08",
- "reference": "590afea38e708022662629fbf5184351fa82cf08",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9",
+ "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9",
"shasum": ""
},
"require": {
@@ -3000,44 +2883,44 @@
"ext-openssl": "*",
"ext-session": "*",
"ext-tokenizer": "*",
- "fruitcake/php-cors": "^1.2",
+ "fruitcake/php-cors": "^1.3",
+ "guzzlehttp/guzzle": "^7.8",
"guzzlehttp/uri-template": "^1.0",
- "laravel/prompts": "^0.1.9",
+ "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
"laravel/serializable-closure": "^1.3",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
"monolog/monolog": "^3.0",
- "nesbot/carbon": "^2.67",
- "nunomaduro/termwind": "^1.13",
- "php": "^8.1",
+ "nesbot/carbon": "^2.72.2|^3.0",
+ "nunomaduro/termwind": "^2.0",
+ "php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
"ramsey/uuid": "^4.7",
- "symfony/console": "^6.2",
- "symfony/error-handler": "^6.2",
- "symfony/finder": "^6.2",
- "symfony/http-foundation": "^6.4",
- "symfony/http-kernel": "^6.2",
- "symfony/mailer": "^6.2",
- "symfony/mime": "^6.2",
- "symfony/process": "^6.2",
- "symfony/routing": "^6.2",
- "symfony/uid": "^6.2",
- "symfony/var-dumper": "^6.2",
+ "symfony/console": "^7.0",
+ "symfony/error-handler": "^7.0",
+ "symfony/finder": "^7.0",
+ "symfony/http-foundation": "^7.0",
+ "symfony/http-kernel": "^7.0",
+ "symfony/mailer": "^7.0",
+ "symfony/mime": "^7.0",
+ "symfony/polyfill-php83": "^1.28",
+ "symfony/process": "^7.0",
+ "symfony/routing": "^7.0",
+ "symfony/uid": "^7.0",
+ "symfony/var-dumper": "^7.0",
"tijsverkoyen/css-to-inline-styles": "^2.2.5",
"vlucas/phpdotenv": "^5.4.1",
"voku/portable-ascii": "^2.0"
},
"conflict": {
- "carbonphp/carbon-doctrine-types": ">=3.0",
- "doctrine/dbal": ">=4.0",
"mockery/mockery": "1.6.8",
- "phpunit/phpunit": ">=11.0.0",
"tightenco/collect": "<5.5.33"
},
"provide": {
"psr/container-implementation": "1.1|2.0",
+ "psr/log-implementation": "1.0|2.0|3.0",
"psr/simple-cache-implementation": "1.0|2.0|3.0"
},
"replace": {
@@ -3046,6 +2929,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",
@@ -3073,36 +2957,35 @@
"illuminate/testing": "self.version",
"illuminate/translation": "self.version",
"illuminate/validation": "self.version",
- "illuminate/view": "self.version"
+ "illuminate/view": "self.version",
+ "spatie/once": "*"
},
"require-dev": {
"ably/ably-php": "^1.0",
"aws/aws-sdk-php": "^3.235.5",
- "doctrine/dbal": "^3.5.1",
"ext-gmp": "*",
- "fakerphp/faker": "^1.21",
- "guzzlehttp/guzzle": "^7.5",
+ "fakerphp/faker": "^1.23",
"league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-ftp": "^3.0",
"league/flysystem-path-prefixing": "^3.3",
"league/flysystem-read-only": "^3.3",
"league/flysystem-sftp-v3": "^3.0",
- "mockery/mockery": "^1.5.1",
+ "mockery/mockery": "^1.6",
"nyholm/psr7": "^1.2",
- "orchestra/testbench-core": "^8.23.4",
- "pda/pheanstalk": "^4.0",
- "phpstan/phpstan": "^1.4.7",
- "phpunit/phpunit": "^10.0.7",
+ "orchestra/testbench-core": "^9.5",
+ "pda/pheanstalk": "^5.0",
+ "phpstan/phpstan": "^1.11.5",
+ "phpunit/phpunit": "^10.5|^11.0",
"predis/predis": "^2.0.2",
- "symfony/cache": "^6.2",
- "symfony/http-client": "^6.2.4",
- "symfony/psr-http-message-bridge": "^2.0"
+ "resend/resend-php": "^0.10.0",
+ "symfony/cache": "^7.0",
+ "symfony/http-client": "^7.0",
+ "symfony/psr-http-message-bridge": "^7.0"
},
"suggest": {
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).",
- "brianium/paratest": "Required to run tests in parallel (^6.0).",
- "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).",
+ "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).",
"ext-apcu": "Required to use the APC cache driver.",
"ext-fileinfo": "Required to use the Filesystem class.",
"ext-ftp": "Required to use the Flysystem FTP driver.",
@@ -3111,34 +2994,34 @@
"ext-pcntl": "Required to use all features of the queue worker and console signal trapping.",
"ext-pdo": "Required to use all database features.",
"ext-posix": "Required to use all features of the queue worker.",
- "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
+ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
- "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).",
"league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).",
"league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).",
"league/flysystem-read-only": "Required to use read-only disks (^3.3)",
"league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).",
- "mockery/mockery": "Required to use mocking (^1.5.1).",
+ "mockery/mockery": "Required to use mocking (^1.6).",
"nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).",
- "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
- "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).",
+ "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
+ "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).",
"predis/predis": "Required to use the predis connector (^2.0.2).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
- "symfony/cache": "Required to PSR-6 cache bridge (^6.2).",
- "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).",
- "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).",
- "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).",
- "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).",
- "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)."
+ "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
+ "symfony/cache": "Required to PSR-6 cache bridge (^7.0).",
+ "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).",
+ "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).",
+ "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).",
+ "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).",
+ "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)."
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "10.x-dev"
+ "dev-master": "11.x-dev"
}
},
"autoload": {
@@ -3147,6 +3030,8 @@
"src/Illuminate/Events/functions.php",
"src/Illuminate/Filesystem/functions.php",
"src/Illuminate/Foundation/helpers.php",
+ "src/Illuminate/Log/functions.php",
+ "src/Illuminate/Support/functions.php",
"src/Illuminate/Support/helpers.php"
],
"psr-4": {
@@ -3178,20 +3063,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2024-05-28T15:46:19+00:00"
+ "time": "2024-10-09T04:17:35+00:00"
},
{
"name": "laravel/horizon",
- "version": "v5.24.4",
+ "version": "v5.29.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/horizon.git",
- "reference": "8d31ff178bf5493efc2b2629c10612054f31f584"
+ "reference": "9f482f21c23ed01c2366d1157843165165579c23"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/horizon/zipball/8d31ff178bf5493efc2b2629c10612054f31f584",
- "reference": "8d31ff178bf5493efc2b2629c10612054f31f584",
+ "url": "https://api.github.com/repos/laravel/horizon/zipball/9f482f21c23ed01c2366d1157843165165579c23",
+ "reference": "9f482f21c23ed01c2366d1157843165165579c23",
"shasum": ""
},
"require": {
@@ -3255,22 +3140,99 @@
],
"support": {
"issues": "https://github.com/laravel/horizon/issues",
- "source": "https://github.com/laravel/horizon/tree/v5.24.4"
+ "source": "https://github.com/laravel/horizon/tree/v5.29.1"
},
- "time": "2024-05-03T13:34:14+00:00"
+ "time": "2024-10-08T18:23:02+00:00"
},
{
- "name": "laravel/prompts",
- "version": "v0.1.23",
+ "name": "laravel/pail",
+ "version": "v1.1.5",
"source": {
"type": "git",
- "url": "https://github.com/laravel/prompts.git",
- "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400"
+ "url": "https://github.com/laravel/pail.git",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/9bc4df7c699b0452c6b815e64a2d84b6d7f99400",
- "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.24|^11.0",
+ "illuminate/contracts": "^10.24|^11.0",
+ "illuminate/log": "^10.24|^11.0",
+ "illuminate/process": "^10.24|^11.0",
+ "illuminate/support": "^10.24|^11.0",
+ "nunomaduro/termwind": "^1.15|^2.0",
+ "php": "^8.2",
+ "symfony/console": "^6.0|^7.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.13",
+ "orchestra/testbench": "^8.12|^9.0",
+ "pestphp/pest": "^2.20",
+ "pestphp/pest-plugin-type-coverage": "^2.3",
+ "phpstan/phpstan": "^1.10",
+ "symfony/var-dumper": "^6.3|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Pail\\PailServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Pail\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Easily delve into your Laravel application's log files directly from the command line.",
+ "homepage": "https://github.com/laravel/pail",
+ "keywords": [
+ "laravel",
+ "logs",
+ "php",
+ "tail"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pail/issues",
+ "source": "https://github.com/laravel/pail"
+ },
+ "time": "2024-10-15T20:06:24+00:00"
+ },
+ {
+ "name": "laravel/prompts",
+ "version": "v0.1.25",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/prompts.git",
+ "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95",
+ "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95",
"shasum": ""
},
"require": {
@@ -3313,43 +3275,41 @@
"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.23"
+ "source": "https://github.com/laravel/prompts/tree/v0.1.25"
},
- "time": "2024-05-27T13:53:20+00:00"
+ "time": "2024-08-12T22:06:33+00:00"
},
{
"name": "laravel/sanctum",
- "version": "v3.3.3",
+ "version": "v4.0.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
- "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5"
+ "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5",
- "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab",
+ "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab",
"shasum": ""
},
"require": {
"ext-json": "*",
- "illuminate/console": "^9.21|^10.0",
- "illuminate/contracts": "^9.21|^10.0",
- "illuminate/database": "^9.21|^10.0",
- "illuminate/support": "^9.21|^10.0",
- "php": "^8.0.2"
+ "illuminate/console": "^11.0",
+ "illuminate/contracts": "^11.0",
+ "illuminate/database": "^11.0",
+ "illuminate/support": "^11.0",
+ "php": "^8.2",
+ "symfony/console": "^7.0"
},
"require-dev": {
- "mockery/mockery": "^1.0",
- "orchestra/testbench": "^7.28.2|^8.8.3",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^9.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.6"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- },
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
@@ -3381,30 +3341,31 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
- "time": "2023-12-19T18:44:48+00:00"
+ "time": "2024-09-27T14:55:41+00:00"
},
{
"name": "laravel/serializable-closure",
- "version": "v1.3.3",
+ "version": "v1.3.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "3dbf8a8e914634c48d389c1234552666b3d43754"
+ "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c"
},
"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/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c",
+ "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c",
"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": {
@@ -3441,20 +3402,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-09-23T13:33:08+00:00"
},
{
"name": "laravel/socialite",
- "version": "v5.14.0",
+ "version": "v5.16.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a"
+ "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/c7b0193a3753a29aff8ce80aa2f511917e6ed68a",
- "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
+ "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
"shasum": ""
},
"require": {
@@ -3513,20 +3474,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2024-05-03T20:31:38+00:00"
+ "time": "2024-09-03T09:46:57+00:00"
},
{
"name": "laravel/tinker",
- "version": "v2.9.0",
+ "version": "v2.10.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe"
+ "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe",
- "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5",
+ "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5",
"shasum": ""
},
"require": {
@@ -3577,9 +3538,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.9.0"
+ "source": "https://github.com/laravel/tinker/tree/v2.10.0"
},
- "time": "2024-01-04T16:10:04+00:00"
+ "time": "2024-09-23T13:32:56+00:00"
},
{
"name": "laravel/ui",
@@ -3646,38 +3607,38 @@
},
{
"name": "lcobucci/jwt",
- "version": "5.3.0",
+ "version": "5.4.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
- "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83"
+ "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
- "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051",
+ "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-sodium": "*",
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"psr/clock": "^1.0"
},
"require-dev": {
- "infection/infection": "^0.27.0",
- "lcobucci/clock": "^3.0",
+ "infection/infection": "^0.29",
+ "lcobucci/clock": "^3.2",
"lcobucci/coding-standard": "^11.0",
- "phpbench/phpbench": "^1.2.9",
+ "phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.10.7",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.10",
"phpstan/phpstan-strict-rules": "^1.5.0",
- "phpunit/phpunit": "^10.2.6"
+ "phpunit/phpunit": "^11.1"
},
"suggest": {
- "lcobucci/clock": ">= 3.0"
+ "lcobucci/clock": ">= 3.2"
},
"type": "library",
"autoload": {
@@ -3703,7 +3664,7 @@
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
- "source": "https://github.com/lcobucci/jwt/tree/5.3.0"
+ "source": "https://github.com/lcobucci/jwt/tree/5.4.0"
},
"funding": [
{
@@ -3715,20 +3676,20 @@
"type": "patreon"
}
],
- "time": "2024-04-11T23:07:54+00:00"
+ "time": "2024-10-08T22:06:45+00:00"
},
{
"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": {
@@ -3741,8 +3702,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",
@@ -3764,7 +3725,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "2.6-dev"
}
},
"autoload": {
@@ -3821,7 +3782,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-02T11:59:32+00:00"
+ "time": "2024-08-16T11:46:16+00:00"
},
{
"name": "league/config",
@@ -3907,16 +3868,16 @@
},
{
"name": "league/flysystem",
- "version": "3.28.0",
+ "version": "3.29.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c"
+ "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c",
- "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319",
+ "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319",
"shasum": ""
},
"require": {
@@ -3984,22 +3945,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.28.0"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.29.1"
},
- "time": "2024-05-22T10:09:12+00:00"
+ "time": "2024-10-08T08:58:34+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "3.28.0",
+ "version": "3.29.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "22071ef1604bc776f5ff2468ac27a752514665c8"
+ "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/22071ef1604bc776f5ff2468ac27a752514665c8",
- "reference": "22071ef1604bc776f5ff2468ac27a752514665c8",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9",
+ "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9",
"shasum": ""
},
"require": {
@@ -4039,22 +4000,22 @@
"storage"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.28.0"
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0"
},
- "time": "2024-05-06T20:05:52+00:00"
+ "time": "2024-08-17T13:10:48+00:00"
},
{
"name": "league/flysystem-local",
- "version": "3.28.0",
+ "version": "3.29.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
- "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40"
+ "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40",
- "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27",
+ "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27",
"shasum": ""
},
"require": {
@@ -4088,22 +4049,22 @@
"local"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0"
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0"
},
- "time": "2024-05-06T20:05:52+00:00"
+ "time": "2024-08-09T21:24:39+00:00"
},
{
"name": "league/flysystem-sftp-v3",
- "version": "3.28.0",
+ "version": "3.29.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-sftp-v3.git",
- "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41"
+ "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/abedadd3c64d4f0e276d6ecc796ec8194d136b41",
- "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252",
+ "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252",
"shasum": ""
},
"require": {
@@ -4137,22 +4098,22 @@
"sftp"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.28.0"
+ "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0"
},
- "time": "2024-05-06T20:05:52+00:00"
+ "time": "2024-08-14T19:35:54+00:00"
},
{
"name": "league/mime-type-detection",
- "version": "1.15.0",
+ "version": "1.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
- "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301"
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301",
- "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301",
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
"shasum": ""
},
"require": {
@@ -4183,7 +4144,7 @@
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
- "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0"
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
},
"funding": [
{
@@ -4195,7 +4156,7 @@
"type": "tidelift"
}
],
- "time": "2024-01-28T23:22:08+00:00"
+ "time": "2024-09-21T08:32:55+00:00"
},
{
"name": "league/oauth1-client",
@@ -4523,17 +4484,79 @@
"time": "2024-03-14T14:03:32+00:00"
},
{
- "name": "lorisleiva/laravel-actions",
- "version": "v2.8.0",
+ "name": "log1x/laravel-webfonts",
+ "version": "v1.0.1",
"source": {
"type": "git",
- "url": "https://github.com/lorisleiva/laravel-actions.git",
- "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f"
+ "url": "https://github.com/Log1x/laravel-webfonts.git",
+ "reference": "0d38122aa7f5501394006a6715f7d97dac223507"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/d5c2ca544f40d85f877b38eb6d23e9c967ecb69f",
- "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f",
+ "url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/0d38122aa7f5501394006a6715f7d97dac223507",
+ "reference": "0d38122aa7f5501394006a6715f7d97dac223507",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.8",
+ "laravel/prompts": "^0.1.15",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "illuminate/console": "^10.41",
+ "illuminate/http": "^10.41",
+ "illuminate/support": "^10.41",
+ "laravel/pint": "^1.13"
+ },
+ "type": "package",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Log1x\\LaravelWebfonts\\WebfontsServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Log1x\\LaravelWebfonts\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brandon Nifong",
+ "email": "brandon@tendency.me",
+ "homepage": "https://github.com/log1x"
+ }
+ ],
+ "description": "Download, install, and preload over 1500 Google fonts locally in your Laravel project",
+ "support": {
+ "issues": "https://github.com/Log1x/laravel-webfonts/issues",
+ "source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Log1x",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-28T11:53:11+00:00"
+ },
+ {
+ "name": "lorisleiva/laravel-actions",
+ "version": "v2.8.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lorisleiva/laravel-actions.git",
+ "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/5a168bfdd3b75dd6ff259019d4aeef784bbd5403",
+ "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403",
"shasum": ""
},
"require": {
@@ -4542,7 +4565,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"
},
@@ -4583,11 +4606,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": [
{
@@ -4595,7 +4619,7 @@
"type": "github"
}
],
- "time": "2024-03-13T12:47:32+00:00"
+ "time": "2024-09-10T09:57:29+00:00"
},
{
"name": "lorisleiva/lody",
@@ -4671,16 +4695,16 @@
},
{
"name": "monolog/monolog",
- "version": "3.6.0",
+ "version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654"
+ "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654",
- "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8",
+ "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8",
"shasum": ""
},
"require": {
@@ -4756,7 +4780,7 @@
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
- "source": "https://github.com/Seldaek/monolog/tree/3.6.0"
+ "source": "https://github.com/Seldaek/monolog/tree/3.7.0"
},
"funding": [
{
@@ -4768,20 +4792,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-12T21:02:21+00:00"
+ "time": "2024-06-28T09:40:51+00:00"
},
{
"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": {
@@ -4798,7 +4822,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7-dev"
+ "dev-master": "2.8-dev"
}
},
"autoload": {
@@ -4832,48 +4856,47 @@
],
"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": "2.72.3",
+ "version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83"
+ "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83",
- "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f",
+ "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f",
"shasum": ""
},
"require": {
"carbonphp/carbon-doctrine-types": "*",
"ext-json": "*",
- "php": "^7.1.8 || ^8.0",
+ "php": "^8.1",
"psr/clock": "^1.0",
+ "symfony/clock": "^6.3 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
- "symfony/polyfill-php80": "^1.16",
- "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0"
+ "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
- "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0",
- "doctrine/orm": "^2.7 || ^3.0",
- "friendsofphp/php-cs-fixer": "^3.0",
- "kylekatarnls/multi-tester": "^2.0",
- "ondrejmirtes/better-reflection": "*",
- "phpmd/phpmd": "^2.9",
- "phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "^0.12.99 || ^1.7.14",
- "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6",
- "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20",
- "squizlabs/php_codesniffer": "^3.4"
+ "doctrine/dbal": "^3.6.3 || ^4.0",
+ "doctrine/orm": "^2.15.2 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^3.57.2",
+ "kylekatarnls/multi-tester": "^2.5.3",
+ "ondrejmirtes/better-reflection": "^6.25.0.4",
+ "phpmd/phpmd": "^2.15.0",
+ "phpstan/extension-installer": "^1.3.1",
+ "phpstan/phpstan": "^1.11.2",
+ "phpunit/phpunit": "^10.5.20",
+ "squizlabs/php_codesniffer": "^3.9.0"
},
"bin": [
"bin/carbon"
@@ -4881,8 +4904,8 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-3.x": "3.x-dev",
- "dev-master": "2.x-dev"
+ "dev-master": "3.x-dev",
+ "dev-2.x": "2.x-dev"
},
"laravel": {
"providers": [
@@ -4941,28 +4964,28 @@
"type": "tidelift"
}
],
- "time": "2024-01-25T10:35:09+00:00"
+ "time": "2024-08-19T06:22:39+00:00"
},
{
"name": "nette/schema",
- "version": "v1.3.0",
+ "version": "v1.3.2",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
- "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188"
+ "reference": "da801d52f0354f70a638673c4a0f04e16529431d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
- "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
+ "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
+ "reference": "da801d52f0354f70a638673c4a0f04e16529431d",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
- "php": "8.1 - 8.3"
+ "php": "8.1 - 8.4"
},
"require-dev": {
- "nette/tester": "^2.4",
+ "nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.8"
},
@@ -5001,26 +5024,26 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
- "source": "https://github.com/nette/schema/tree/v1.3.0"
+ "source": "https://github.com/nette/schema/tree/v1.3.2"
},
- "time": "2023-12-11T11:54:22+00:00"
+ "time": "2024-10-06T23:10:23+00:00"
},
{
"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",
@@ -5087,22 +5110,22 @@
],
"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",
- "version": "v5.0.2",
+ "version": "v5.3.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13"
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13",
- "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
+ "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"shasum": ""
},
"require": {
@@ -5113,7 +5136,7 @@
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^9.0"
},
"bin": [
"bin/php-parse"
@@ -5145,9 +5168,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
},
- "time": "2024-03-05T20:51:40+00:00"
+ "time": "2024-10-08T18:51:32+00:00"
},
{
"name": "nubs/random-name-generator",
@@ -5204,33 +5227,32 @@
},
{
"name": "nunomaduro/termwind",
- "version": "v1.15.1",
+ "version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
- "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc"
+ "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc",
- "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc",
+ "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/e5f21eade88689536c0cdad4c3cd75f3ed26e01a",
+ "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
- "php": "^8.0",
- "symfony/console": "^5.3.0|^6.0.0"
+ "php": "^8.2",
+ "symfony/console": "^7.0.4"
},
"require-dev": {
- "ergebnis/phpstan-rules": "^1.0.",
- "illuminate/console": "^8.0|^9.0",
- "illuminate/support": "^8.0|^9.0",
- "laravel/pint": "^1.0.0",
- "pestphp/pest": "^1.21.0",
- "pestphp/pest-plugin-mock": "^1.0",
- "phpstan/phpstan": "^1.4.6",
- "phpstan/phpstan-strict-rules": "^1.1.0",
- "symfony/var-dumper": "^5.2.7|^6.0.0",
+ "ergebnis/phpstan-rules": "^2.2.0",
+ "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"
},
"type": "library",
@@ -5239,6 +5261,9 @@
"providers": [
"Termwind\\Laravel\\TermwindServiceProvider"
]
+ },
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
}
},
"autoload": {
@@ -5270,7 +5295,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
- "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1"
+ "source": "https://github.com/nunomaduro/termwind/tree/v2.1.0"
},
"funding": [
{
@@ -5286,20 +5311,20 @@
"type": "github"
}
],
- "time": "2023-02-08T01:06:31+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": {
@@ -5352,7 +5377,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": [
{
@@ -5364,28 +5389,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": {
@@ -5431,7 +5456,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",
@@ -5570,385 +5595,132 @@
"time": "2024-04-22T22:05:04+00:00"
},
{
- "name": "php-http/client-common",
- "version": "2.7.1",
+ "name": "php-di/invoker",
+ "version": "2.3.4",
"source": {
"type": "git",
- "url": "https://github.com/php-http/client-common.git",
- "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612"
+ "url": "https://github.com/PHP-DI/Invoker.git",
+ "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612",
- "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612",
+ "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86",
+ "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0",
- "php-http/httplug": "^2.0",
- "php-http/message": "^1.6",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.0",
- "psr/http-message": "^1.0 || ^2.0",
- "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0",
- "symfony/polyfill-php80": "^1.17"
+ "php": ">=7.3",
+ "psr/container": "^1.0|^2.0"
},
"require-dev": {
- "doctrine/instantiator": "^1.1",
- "guzzlehttp/psr7": "^1.4",
- "nyholm/psr7": "^1.2",
- "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
- "phpspec/prophecy": "^1.10.2",
- "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7"
- },
- "suggest": {
- "ext-json": "To detect JSON responses with the ContentTypePlugin",
- "ext-libxml": "To detect XML responses with the ContentTypePlugin",
- "php-http/cache-plugin": "PSR-6 Cache plugin",
- "php-http/logger-plugin": "PSR-3 Logger plugin",
- "php-http/stopwatch-plugin": "Symfony Stopwatch plugin"
+ "athletic/athletic": "~0.1.8",
+ "mnapoli/hard-mode": "~0.3.0",
+ "phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
- "Http\\Client\\Common\\": "src/"
+ "Invoker\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com"
- }
- ],
- "description": "Common HTTP Client implementations and tools for HTTPlug",
- "homepage": "http://httplug.io",
+ "description": "Generic and extensible callable invoker",
+ "homepage": "https://github.com/PHP-DI/Invoker",
"keywords": [
- "client",
- "common",
- "http",
- "httplug"
+ "callable",
+ "dependency",
+ "dependency-injection",
+ "injection",
+ "invoke",
+ "invoker"
],
"support": {
- "issues": "https://github.com/php-http/client-common/issues",
- "source": "https://github.com/php-http/client-common/tree/2.7.1"
+ "issues": "https://github.com/PHP-DI/Invoker/issues",
+ "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4"
},
- "time": "2023-11-30T10:31:25+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ }
+ ],
+ "time": "2023-09-08T09:24:21+00:00"
},
{
- "name": "php-http/discovery",
- "version": "1.19.4",
+ "name": "php-di/php-di",
+ "version": "7.0.7",
"source": {
"type": "git",
- "url": "https://github.com/php-http/discovery.git",
- "reference": "0700efda8d7526335132360167315fdab3aeb599"
+ "url": "https://github.com/PHP-DI/PHP-DI.git",
+ "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599",
- "reference": "0700efda8d7526335132360167315fdab3aeb599",
+ "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
+ "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1",
"shasum": ""
},
"require": {
- "composer-plugin-api": "^1.0|^2.0",
- "php": "^7.1 || ^8.0"
- },
- "conflict": {
- "nyholm/psr7": "<1.0",
- "zendframework/zend-diactoros": "*"
+ "laravel/serializable-closure": "^1.0",
+ "php": ">=8.0",
+ "php-di/invoker": "^2.0",
+ "psr/container": "^1.1 || ^2.0"
},
"provide": {
- "php-http/async-client-implementation": "*",
- "php-http/client-implementation": "*",
- "psr/http-client-implementation": "*",
- "psr/http-factory-implementation": "*",
- "psr/http-message-implementation": "*"
+ "psr/container-implementation": "^1.0"
},
"require-dev": {
- "composer/composer": "^1.0.2|^2.0",
- "graham-campbell/phpspec-skip-example-extension": "^5.0",
- "php-http/httplug": "^1.0 || ^2.0",
- "php-http/message-factory": "^1.0",
- "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
- "sebastian/comparator": "^3.0.5 || ^4.0.8",
- "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
- },
- "type": "composer-plugin",
- "extra": {
- "class": "Http\\Discovery\\Composer\\Plugin",
- "plugin-optional": true
- },
- "autoload": {
- "psr-4": {
- "Http\\Discovery\\": "src/"
- },
- "exclude-from-classmap": [
- "src/Composer/Plugin.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com"
- }
- ],
- "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
- "homepage": "http://php-http.org",
- "keywords": [
- "adapter",
- "client",
- "discovery",
- "factory",
- "http",
- "message",
- "psr17",
- "psr7"
- ],
- "support": {
- "issues": "https://github.com/php-http/discovery/issues",
- "source": "https://github.com/php-http/discovery/tree/1.19.4"
- },
- "time": "2024-03-29T13:00:05+00:00"
- },
- {
- "name": "php-http/httplug",
- "version": "2.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-http/httplug.git",
- "reference": "625ad742c360c8ac580fcc647a1541d29e257f67"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67",
- "reference": "625ad742c360c8ac580fcc647a1541d29e257f67",
- "shasum": ""
- },
- "require": {
- "php": "^7.1 || ^8.0",
- "php-http/promise": "^1.1",
- "psr/http-client": "^1.0",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "require-dev": {
- "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
- "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Http\\Client\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Eric GELOEN",
- "email": "geloen.eric@gmail.com"
- },
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://sagikazarmark.hu"
- }
- ],
- "description": "HTTPlug, the HTTP client abstraction for PHP",
- "homepage": "http://httplug.io",
- "keywords": [
- "client",
- "http"
- ],
- "support": {
- "issues": "https://github.com/php-http/httplug/issues",
- "source": "https://github.com/php-http/httplug/tree/2.4.0"
- },
- "time": "2023-04-14T15:10:03+00:00"
- },
- {
- "name": "php-http/message",
- "version": "1.16.1",
- "source": {
- "type": "git",
- "url": "https://github.com/php-http/message.git",
- "reference": "5997f3289332c699fa2545c427826272498a2088"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088",
- "reference": "5997f3289332c699fa2545c427826272498a2088",
- "shasum": ""
- },
- "require": {
- "clue/stream-filter": "^1.5",
- "php": "^7.2 || ^8.0",
- "psr/http-message": "^1.1 || ^2.0"
- },
- "provide": {
- "php-http/message-factory-implementation": "1.0"
- },
- "require-dev": {
- "ergebnis/composer-normalize": "^2.6",
- "ext-zlib": "*",
- "guzzlehttp/psr7": "^1.0 || ^2.0",
- "laminas/laminas-diactoros": "^2.0 || ^3.0",
- "php-http/message-factory": "^1.0.2",
- "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
- "slim/slim": "^3.0"
+ "friendsofphp/php-cs-fixer": "^3",
+ "friendsofphp/proxy-manager-lts": "^1",
+ "mnapoli/phpunit-easymock": "^1.3",
+ "phpunit/phpunit": "^9.5",
+ "vimeo/psalm": "^4.6"
},
"suggest": {
- "ext-zlib": "Used with compressor/decompressor streams",
- "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
- "laminas/laminas-diactoros": "Used with Diactoros Factories",
- "slim/slim": "Used with Slim Framework PSR-7 implementation"
+ "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)"
},
"type": "library",
"autoload": {
"files": [
- "src/filters.php"
+ "src/functions.php"
],
"psr-4": {
- "Http\\Message\\": "src/"
+ "DI\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com"
- }
- ],
- "description": "HTTP Message related tools",
- "homepage": "http://php-http.org",
+ "description": "The dependency injection container for humans",
+ "homepage": "https://php-di.org/",
"keywords": [
- "http",
- "message",
- "psr-7"
+ "PSR-11",
+ "container",
+ "container-interop",
+ "dependency injection",
+ "di",
+ "ioc",
+ "psr11"
],
"support": {
- "issues": "https://github.com/php-http/message/issues",
- "source": "https://github.com/php-http/message/tree/1.16.1"
+ "issues": "https://github.com/PHP-DI/PHP-DI/issues",
+ "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.7"
},
- "time": "2024-03-07T13:22:09+00:00"
- },
- {
- "name": "php-http/message-factory",
- "version": "1.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/php-http/message-factory.git",
- "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57",
- "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4",
- "psr/http-message": "^1.0 || ^2.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Http\\Message\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
+ "funding": [
{
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com"
- }
- ],
- "description": "Factory interfaces for PSR-7 HTTP Message",
- "homepage": "http://php-http.org",
- "keywords": [
- "factory",
- "http",
- "message",
- "stream",
- "uri"
- ],
- "support": {
- "issues": "https://github.com/php-http/message-factory/issues",
- "source": "https://github.com/php-http/message-factory/tree/1.1.0"
- },
- "abandoned": "psr/http-factory",
- "time": "2023-04-14T14:16:17+00:00"
- },
- {
- "name": "php-http/promise",
- "version": "1.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/php-http/promise.git",
- "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
- "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
- "shasum": ""
- },
- "require": {
- "php": "^7.1 || ^8.0"
- },
- "require-dev": {
- "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3",
- "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Http\\Promise\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Joel Wurtz",
- "email": "joel.wurtz@gmail.com"
+ "url": "https://github.com/mnapoli",
+ "type": "github"
},
{
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com"
+ "url": "https://tidelift.com/funding/github/packagist/php-di/php-di",
+ "type": "tidelift"
}
],
- "description": "Promise used for asynchronous HTTP requests",
- "homepage": "http://httplug.io",
- "keywords": [
- "promise"
- ],
- "support": {
- "issues": "https://github.com/php-http/promise/issues",
- "source": "https://github.com/php-http/promise/tree/1.3.1"
- },
- "time": "2024-03-15T13:55:21+00:00"
+ "time": "2024-07-21T15:55:45+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -6063,16 +5835,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": {
@@ -6080,13 +5852,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"
@@ -6122,7 +5894,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": [
{
@@ -6134,24 +5906,24 @@
"type": "tidelift"
}
],
- "time": "2023-11-12T21:59:55+00:00"
+ "time": "2024-07-20T21:41:07+00:00"
},
{
"name": "phpseclib/phpseclib",
- "version": "3.0.37",
+ "version": "3.0.42",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
- "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8"
+ "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
- "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98",
+ "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98",
"shasum": ""
},
"require": {
- "paragonie/constant_time_encoding": "^1|^2",
+ "paragonie/constant_time_encoding": "^1|^2|^3",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1"
},
@@ -6228,7 +6000,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
- "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37"
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42"
},
"funding": [
{
@@ -6244,20 +6016,20 @@
"type": "tidelift"
}
],
- "time": "2024-03-03T02:14:58+00:00"
+ "time": "2024-09-16T03:06:04+00:00"
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.29.0",
+ "version": "1.32.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc"
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc",
- "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
"shasum": ""
},
"require": {
@@ -6289,22 +6061,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.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0"
},
- "time": "2024-05-06T12:04:23+00:00"
+ "time": "2024-09-26T07:23:32+00:00"
},
{
"name": "phpstan/phpstan",
- "version": "1.11.2",
+ "version": "1.12.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "0d5d4294a70deb7547db655c47685d680e39cfec"
+ "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec",
- "reference": "0d5d4294a70deb7547db655c47685d680e39cfec",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae",
+ "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae",
"shasum": ""
},
"require": {
@@ -6349,60 +6121,7 @@
"type": "github"
}
],
- "time": "2024-05-24T13:23:04+00:00"
- },
- {
- "name": "pimple/pimple",
- "version": "v3.5.0",
- "source": {
- "type": "git",
- "url": "https://github.com/silexphp/Pimple.git",
- "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed",
- "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2.5",
- "psr/container": "^1.1 || ^2.0"
- },
- "require-dev": {
- "symfony/phpunit-bridge": "^5.4@dev"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Pimple": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- }
- ],
- "description": "Pimple, a simple Dependency Injection Container",
- "homepage": "https://pimple.symfony.com",
- "keywords": [
- "container",
- "dependency injection"
- ],
- "support": {
- "source": "https://github.com/silexphp/Pimple/tree/v3.5.0"
- },
- "time": "2021-10-28T11:13:42+00:00"
+ "time": "2024-10-06T15:03:59+00:00"
},
{
"name": "pion/laravel-chunk-upload",
@@ -6516,24 +6235,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",
@@ -6562,9 +6281,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",
@@ -6928,16 +6647,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": {
@@ -6972,9 +6691,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",
@@ -7029,16 +6748,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.3",
+ "version": "v0.12.4",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73"
+ "reference": "2fd717afa05341b4f8152547f142cd2f130f6818"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
- "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818",
+ "reference": "2fd717afa05341b4f8152547f142cd2f130f6818",
"shasum": ""
},
"require": {
@@ -7102,22 +6821,22 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.3"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.4"
},
- "time": "2024-04-02T15:57:53+00:00"
+ "time": "2024-06-10T01:18:23+00:00"
},
{
"name": "purplepixie/phpdns",
- "version": "2.1.1",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/purplepixie/phpdns.git",
- "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad"
+ "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/18cd3a43fadcfd16e2789e3c78a264945f6cbfad",
- "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad",
+ "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d",
+ "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d",
"shasum": ""
},
"require": {
@@ -7150,9 +6869,9 @@
"description": "PHP DNS Direct Query Module",
"support": {
"issues": "https://github.com/purplepixie/phpdns/issues",
- "source": "https://github.com/purplepixie/phpdns/tree/2.1.1"
+ "source": "https://github.com/purplepixie/phpdns/tree/2.2.0"
},
- "time": "2024-05-27T13:27:50+00:00"
+ "time": "2024-09-26T14:39:58+00:00"
},
{
"name": "pusher/pusher-php-server",
@@ -7442,21 +7161,21 @@
},
{
"name": "rector/rector",
- "version": "1.1.0",
+ "version": "1.2.6",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "556509e2dcf527369892b7d411379c4a02f31859"
+ "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/556509e2dcf527369892b7d411379c4a02f31859",
- "reference": "556509e2dcf527369892b7d411379c4a02f31859",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99",
+ "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
- "phpstan/phpstan": "^1.11"
+ "phpstan/phpstan": "^1.12.5"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -7489,7 +7208,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/1.1.0"
+ "source": "https://github.com/rectorphp/rector/tree/1.2.6"
},
"funding": [
{
@@ -7497,33 +7216,34 @@
"type": "github"
}
],
- "time": "2024-05-18T09:40:27+00:00"
+ "time": "2024-10-03T08:56:44+00:00"
},
{
"name": "resend/resend-laravel",
- "version": "v0.5.0",
+ "version": "v0.13.0",
"source": {
"type": "git",
"url": "https://github.com/resend/resend-laravel.git",
- "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d"
+ "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/resend/resend-laravel/zipball/e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d",
- "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d",
+ "url": "https://api.github.com/repos/resend/resend-laravel/zipball/23aed22df0d0b23c2952da2aaed6a8b88d301a8a",
+ "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a",
"shasum": ""
},
"require": {
- "illuminate/support": "^9.21|^10.0",
+ "illuminate/http": "^10.0|^11.0",
+ "illuminate/support": "^10.0|^11.0",
"php": "^8.1",
- "resend/resend-php": "^0.7.1",
- "symfony/mailer": "^6.2"
+ "resend/resend-php": "^0.12.0",
+ "symfony/mailer": "^6.2|^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.14",
"mockery/mockery": "^1.5",
- "orchestra/testbench": "^7.22|^8.0",
- "pestphp/pest": "^1.22"
+ "orchestra/testbench": "^8.17|^9.0",
+ "pestphp/pest": "^2.0"
},
"type": "library",
"extra": {
@@ -7548,7 +7268,7 @@
"authors": [
{
"name": "Resend and contributors",
- "homepage": "https://github.com/resendlabs/resend-laravel/contributors"
+ "homepage": "https://github.com/resend/resend-laravel/contributors"
}
],
"description": "Resend for Laravel",
@@ -7563,22 +7283,22 @@
],
"support": {
"issues": "https://github.com/resend/resend-laravel/issues",
- "source": "https://github.com/resend/resend-laravel/tree/v0.5.0"
+ "source": "https://github.com/resend/resend-laravel/tree/v0.13.0"
},
- "time": "2023-07-15T17:56:14+00:00"
+ "time": "2024-07-08T18:51:42+00:00"
},
{
"name": "resend/resend-php",
- "version": "v0.7.2",
+ "version": "v0.12.0",
"source": {
"type": "git",
"url": "https://github.com/resend/resend-php.git",
- "reference": "bef429c2cd43ae1a1d990059c73750d46f249872"
+ "reference": "37fb79bb8160ce2de521bf37484ba59e89236521"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/resend/resend-php/zipball/bef429c2cd43ae1a1d990059c73750d46f249872",
- "reference": "bef429c2cd43ae1a1d990059c73750d46f249872",
+ "url": "https://api.github.com/repos/resend/resend-php/zipball/37fb79bb8160ce2de521bf37484ba59e89236521",
+ "reference": "37fb79bb8160ce2de521bf37484ba59e89236521",
"shasum": ""
},
"require": {
@@ -7587,8 +7307,8 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.13",
- "pestphp/pest": "^2.0",
- "pestphp/pest-plugin-mock": "^2.0"
+ "mockery/mockery": "^1.6",
+ "pestphp/pest": "^2.0"
},
"type": "library",
"autoload": {
@@ -7606,7 +7326,7 @@
"authors": [
{
"name": "Resend and contributors",
- "homepage": "https://github.com/resendlabs/resend-php/contributors"
+ "homepage": "https://github.com/resend/resend-php/contributors"
}
],
"description": "Resend PHP library.",
@@ -7620,9 +7340,9 @@
],
"support": {
"issues": "https://github.com/resend/resend-php/issues",
- "source": "https://github.com/resend/resend-php/tree/v0.7.2"
+ "source": "https://github.com/resend/resend-php/tree/v0.12.0"
},
- "time": "2023-09-08T23:47:23+00:00"
+ "time": "2024-03-04T03:16:28+00:00"
},
{
"name": "revolt/event-loop",
@@ -7696,112 +7416,42 @@
},
"time": "2023-11-30T05:34:44+00:00"
},
- {
- "name": "sentry/sdk",
- "version": "3.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/getsentry/sentry-php-sdk.git",
- "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a",
- "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a",
- "shasum": ""
- },
- "require": {
- "http-interop/http-factory-guzzle": "^1.0",
- "sentry/sentry": "^3.22",
- "symfony/http-client": "^4.3|^5.0|^6.0|^7.0"
- },
- "type": "metapackage",
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sentry",
- "email": "accounts@sentry.io"
- }
- ],
- "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.",
- "homepage": "http://sentry.io",
- "keywords": [
- "crash-reporting",
- "crash-reports",
- "error-handler",
- "error-monitoring",
- "log",
- "logging",
- "sentry"
- ],
- "support": {
- "issues": "https://github.com/getsentry/sentry-php-sdk/issues",
- "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0"
- },
- "funding": [
- {
- "url": "https://sentry.io/",
- "type": "custom"
- },
- {
- "url": "https://sentry.io/pricing/",
- "type": "custom"
- }
- ],
- "time": "2023-12-04T10:49:33+00:00"
- },
{
"name": "sentry/sentry",
- "version": "3.22.1",
+ "version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
- "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d"
+ "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d",
- "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d",
+ "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/788ec170f51ebb22f2809a1e3f78b19ccd39b70d",
+ "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d",
"shasum": ""
},
"require": {
+ "ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "guzzlehttp/promises": "^1.5.3|^2.0",
+ "guzzlehttp/psr7": "^1.8.4|^2.1.1",
"jean85/pretty-package-versions": "^1.5|^2.0.4",
"php": "^7.2|^8.0",
- "php-http/async-client-implementation": "^1.0",
- "php-http/client-common": "^1.5|^2.0",
- "php-http/discovery": "^1.15",
- "php-http/httplug": "^1.1|^2.0",
- "php-http/message": "^1.5",
- "php-http/message-factory": "^1.1",
- "psr/http-factory": "^1.0",
- "psr/http-factory-implementation": "^1.0",
"psr/log": "^1.0|^2.0|^3.0",
- "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0",
- "symfony/polyfill-php80": "^1.17"
+ "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0"
},
"conflict": {
- "php-http/client-common": "1.8.0",
"raven/raven": "*"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.19|3.4.*",
+ "friendsofphp/php-cs-fixer": "^3.4",
+ "guzzlehttp/promises": "^1.0|^2.0",
"guzzlehttp/psr7": "^1.8.4|^2.1.1",
- "http-interop/http-factory-guzzle": "^1.0",
"monolog/monolog": "^1.6|^2.0|^3.0",
- "nikic/php-parser": "^4.10.3",
- "php-http/mock-client": "^1.3",
"phpbench/phpbench": "^1.0",
- "phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.3",
- "phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5.14|^9.4",
- "symfony/phpunit-bridge": "^5.2|^6.0",
+ "symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
"vimeo/psalm": "^4.17"
},
"suggest": {
@@ -7826,7 +7476,7 @@
"email": "accounts@sentry.io"
}
],
- "description": "A PHP SDK for Sentry (http://sentry.io)",
+ "description": "PHP SDK for Sentry (http://sentry.io)",
"homepage": "http://sentry.io",
"keywords": [
"crash-reporting",
@@ -7835,11 +7485,13 @@
"error-monitoring",
"log",
"logging",
- "sentry"
+ "profiling",
+ "sentry",
+ "tracing"
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
- "source": "https://github.com/getsentry/sentry-php/tree/3.22.1"
+ "source": "https://github.com/getsentry/sentry-php/tree/4.9.0"
},
"funding": [
{
@@ -7851,47 +7503,42 @@
"type": "custom"
}
],
- "time": "2023-11-13T11:47:28+00:00"
+ "time": "2024-08-08T14:40:50+00:00"
},
{
"name": "sentry/sentry-laravel",
- "version": "3.8.2",
+ "version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-laravel.git",
- "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0"
+ "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/1293e5732f8405e12f000cdf5dee78c927a18de0",
- "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0",
+ "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3",
+ "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3",
"shasum": ""
},
"require": {
- "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
+ "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
"nyholm/psr7": "^1.0",
"php": "^7.2 | ^8.0",
- "sentry/sdk": "^3.4",
- "sentry/sentry": "^3.20.1",
- "symfony/psr-http-message-bridge": "^1.0 | ^2.0"
+ "sentry/sentry": "^4.9",
+ "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.11",
- "laravel/folio": "^1.0",
- "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
+ "guzzlehttp/guzzle": "^7.2",
+ "laravel/folio": "^1.1",
+ "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
+ "livewire/livewire": "^2.0 | ^3.0",
"mockery/mockery": "^1.3",
- "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0",
+ "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^8.4 | ^9.3"
+ "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.x-dev",
- "dev-2.x": "2.x-dev",
- "dev-1.x": "1.x-dev",
- "dev-0.x": "0.x-dev"
- },
"laravel": {
"providers": [
"Sentry\\Laravel\\ServiceProvider",
@@ -7927,11 +7574,13 @@
"laravel",
"log",
"logging",
- "sentry"
+ "profiling",
+ "sentry",
+ "tracing"
],
"support": {
"issues": "https://github.com/getsentry/sentry-laravel/issues",
- "source": "https://github.com/getsentry/sentry-laravel/tree/3.8.2"
+ "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0"
},
"funding": [
{
@@ -7943,7 +7592,7 @@
"type": "custom"
}
],
- "time": "2023-10-12T14:38:46+00:00"
+ "time": "2024-09-19T12:58:53+00:00"
},
{
"name": "socialiteproviders/manager",
@@ -8072,16 +7721,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": {
@@ -8119,7 +7768,7 @@
"spatie"
],
"support": {
- "source": "https://github.com/spatie/backtrace/tree/1.6.1"
+ "source": "https://github.com/spatie/backtrace/tree/1.6.2"
},
"funding": [
{
@@ -8131,7 +7780,7 @@
"type": "other"
}
],
- "time": "2024-04-24T13:22:11+00:00"
+ "time": "2024-07-22T08:21:24+00:00"
},
{
"name": "spatie/laravel-activitylog",
@@ -8311,16 +7960,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": {
@@ -8359,7 +8008,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": [
{
@@ -8367,20 +8016,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.36.2",
+ "version": "1.37.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ray.git",
- "reference": "1852faa96e5aa6778ea3401ec3176eee77268718"
+ "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/1852faa96e5aa6778ea3401ec3176eee77268718",
- "reference": "1852faa96e5aa6778ea3401ec3176eee77268718",
+ "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/c2bedfd1172648df2c80aaceb2541d70f1d9a5b9",
+ "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9",
"shasum": ""
},
"require": {
@@ -8394,7 +8043,7 @@
"spatie/backtrace": "^1.0",
"spatie/ray": "^1.41.1",
"symfony/stopwatch": "4.2|^5.1|^6.0|^7.0",
- "zbateson/mail-mime-parser": "^1.3.1|^2.0"
+ "zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.3",
@@ -8442,7 +8091,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-ray/issues",
- "source": "https://github.com/spatie/laravel-ray/tree/1.36.2"
+ "source": "https://github.com/spatie/laravel-ray/tree/1.37.1"
},
"funding": [
{
@@ -8454,7 +8103,7 @@
"type": "other"
}
],
- "time": "2024-05-02T08:26:02+00:00"
+ "time": "2024-07-12T12:35:17+00:00"
},
{
"name": "spatie/laravel-schemaless-attributes",
@@ -8584,16 +8233,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": {
@@ -8652,7 +8301,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": [
{
@@ -8660,7 +8309,7 @@
"type": "github"
}
],
- "time": "2024-03-13T16:08:30+00:00"
+ "time": "2024-08-29T10:43:45+00:00"
},
{
"name": "spatie/ray",
@@ -8869,48 +8518,121 @@
"time": "2023-10-16T18:04:12+00:00"
},
{
- "name": "symfony/console",
- "version": "v6.4.7",
+ "name": "symfony/clock",
+ "version": "v7.1.1",
"source": {
"type": "git",
- "url": "https://github.com/symfony/console.git",
- "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f"
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/a170e64ae10d00ba89e2acbb590dc2e54da8ad8f",
- "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7",
+ "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
+ "psr/clock": "^1.0",
+ "symfony/polyfill-php83": "^1.28"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/tree/v7.1.1"
+ },
+ "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-05-31T14:57:53+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v7.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
+ "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^5.4|^6.0|^7.0"
+ "symfony/string": "^6.4|^7.0"
},
"conflict": {
- "symfony/dependency-injection": "<5.4",
- "symfony/dotenv": "<5.4",
- "symfony/event-dispatcher": "<5.4",
- "symfony/lock": "<5.4",
- "symfony/process": "<5.4"
+ "symfony/dependency-injection": "<6.4",
+ "symfony/dotenv": "<6.4",
+ "symfony/event-dispatcher": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/process": "<6.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^5.4|^6.0|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
- "symfony/lock": "^5.4|^6.0|^7.0",
- "symfony/messenger": "^5.4|^6.0|^7.0",
- "symfony/process": "^5.4|^6.0|^7.0",
- "symfony/stopwatch": "^5.4|^6.0|^7.0",
- "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ "symfony/lock": "^6.4|^7.0",
+ "symfony/messenger": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0",
+ "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/var-dumper": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -8944,7 +8666,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.7"
+ "source": "https://github.com/symfony/console/tree/v7.1.5"
},
"funding": [
{
@@ -8960,20 +8682,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-09-20T08:28:38+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v7.0.7",
+ "version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc"
+ "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc",
- "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4",
+ "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4",
"shasum": ""
},
"require": {
@@ -9009,7 +8731,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v7.0.7"
+ "source": "https://github.com/symfony/css-selector/tree/v7.1.1"
},
"funding": [
{
@@ -9025,7 +8747,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:29:19+00:00"
+ "time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -9096,22 +8818,22 @@
},
{
"name": "symfony/error-handler",
- "version": "v6.4.7",
+ "version": "v7.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "667a072466c6a53827ed7b119af93806b884cbb3"
+ "reference": "432bb369952795c61ca1def65e078c4a80dad13c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/667a072466c6a53827ed7b119af93806b884cbb3",
- "reference": "667a072466c6a53827ed7b119af93806b884cbb3",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/432bb369952795c61ca1def65e078c4a80dad13c",
+ "reference": "432bb369952795c61ca1def65e078c4a80dad13c",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"psr/log": "^1|^2|^3",
- "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ "symfony/var-dumper": "^6.4|^7.0"
},
"conflict": {
"symfony/deprecation-contracts": "<2.5",
@@ -9120,7 +8842,7 @@
"require-dev": {
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-kernel": "^6.4|^7.0",
- "symfony/serializer": "^5.4|^6.0|^7.0"
+ "symfony/serializer": "^6.4|^7.0"
},
"bin": [
"Resources/bin/patch-type-declarations"
@@ -9151,7 +8873,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/v6.4.7"
+ "source": "https://github.com/symfony/error-handler/tree/v7.1.3"
},
"funding": [
{
@@ -9167,20 +8889,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-07-26T13:02:51+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.0.7",
+ "version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9"
+ "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9",
- "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
+ "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
"shasum": ""
},
"require": {
@@ -9231,7 +8953,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1"
},
"funding": [
{
@@ -9247,7 +8969,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:29:19+00:00"
+ "time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -9327,23 +9049,23 @@
},
{
"name": "symfony/finder",
- "version": "v6.4.7",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "511c48990be17358c23bf45c5d71ab85d40fb764"
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/511c48990be17358c23bf45c5d71ab85d40fb764",
- "reference": "511c48990be17358c23bf45c5d71ab85d40fb764",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.2"
},
"require-dev": {
- "symfony/filesystem": "^6.0|^7.0"
+ "symfony/filesystem": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -9371,7 +9093,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v6.4.7"
+ "source": "https://github.com/symfony/finder/tree/v7.1.4"
},
"funding": [
{
@@ -9387,211 +9109,40 @@
"type": "tidelift"
}
],
- "time": "2024-04-23T10:36:43+00:00"
- },
- {
- "name": "symfony/http-client",
- "version": "v6.4.7",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/http-client.git",
- "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/3683d8107cf1efdd24795cc5f7482be1eded34ac",
- "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "psr/log": "^1|^2|^3",
- "symfony/deprecation-contracts": "^2.5|^3",
- "symfony/http-client-contracts": "^3.4.1",
- "symfony/service-contracts": "^2.5|^3"
- },
- "conflict": {
- "php-http/discovery": "<1.15",
- "symfony/http-foundation": "<6.3"
- },
- "provide": {
- "php-http/async-client-implementation": "*",
- "php-http/client-implementation": "*",
- "psr/http-client-implementation": "1.0",
- "symfony/http-client-implementation": "3.0"
- },
- "require-dev": {
- "amphp/amp": "^2.5",
- "amphp/http-client": "^4.2.1",
- "amphp/http-tunnel": "^1.0",
- "amphp/socket": "^1.1",
- "guzzlehttp/promises": "^1.4|^2.0",
- "nyholm/psr7": "^1.0",
- "php-http/httplug": "^1.0|^2.0",
- "psr/http-client": "^1.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/http-kernel": "^5.4|^6.0|^7.0",
- "symfony/messenger": "^5.4|^6.0|^7.0",
- "symfony/process": "^5.4|^6.0|^7.0",
- "symfony/stopwatch": "^5.4|^6.0|^7.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\HttpClient\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "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": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
- "homepage": "https://symfony.com",
- "keywords": [
- "http"
- ],
- "support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.7"
- },
- "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-04-18T09:22:46+00:00"
- },
- {
- "name": "symfony/http-client-contracts",
- "version": "v3.5.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/http-client-contracts.git",
- "reference": "20414d96f391677bf80078aa55baece78b82647d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d",
- "reference": "20414d96f391677bf80078aa55baece78b82647d",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.5-dev"
- },
- "thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Contracts\\HttpClient\\": ""
- },
- "exclude-from-classmap": [
- "/Test/"
- ]
- },
- "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": "Generic abstractions related to HTTP clients",
- "homepage": "https://symfony.com",
- "keywords": [
- "abstractions",
- "contracts",
- "decoupling",
- "interfaces",
- "interoperability",
- "standards"
- ],
- "support": {
- "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.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-04-18T09:32:20+00:00"
+ "time": "2024-08-13T14:28:19+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v6.4.7",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759"
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b4db6b833035477cb70e18d0ae33cb7c2b521759",
- "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9",
+ "reference": "5183b61657807099d98f3367bcccb850238b17a9",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
"symfony/polyfill-mbstring": "~1.1",
"symfony/polyfill-php83": "^1.27"
},
"conflict": {
- "symfony/cache": "<6.3"
+ "doctrine/dbal": "<3.6",
+ "symfony/cache": "<6.4"
},
"require-dev": {
- "doctrine/dbal": "^2.13.1|^3|^4",
+ "doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
- "symfony/cache": "^6.3|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/expression-language": "^5.4|^6.0|^7.0",
- "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0",
- "symfony/mime": "^5.4|^6.0|^7.0",
- "symfony/rate-limiter": "^5.4|^6.0|^7.0"
+ "symfony/cache": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/mime": "^6.4|^7.0",
+ "symfony/rate-limiter": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -9619,7 +9170,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v6.4.7"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.1.7"
},
"funding": [
{
@@ -9635,77 +9186,77 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-11-06T09:02:46+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v6.4.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98"
+ "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b7b5e6cdef670a0c82d015a966ffc7e855861a98",
- "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b",
+ "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/error-handler": "^6.4|^7.0",
- "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
- "symfony/browser-kit": "<5.4",
- "symfony/cache": "<5.4",
- "symfony/config": "<6.1",
- "symfony/console": "<5.4",
+ "symfony/browser-kit": "<6.4",
+ "symfony/cache": "<6.4",
+ "symfony/config": "<6.4",
+ "symfony/console": "<6.4",
"symfony/dependency-injection": "<6.4",
- "symfony/doctrine-bridge": "<5.4",
- "symfony/form": "<5.4",
- "symfony/http-client": "<5.4",
+ "symfony/doctrine-bridge": "<6.4",
+ "symfony/form": "<6.4",
+ "symfony/http-client": "<6.4",
"symfony/http-client-contracts": "<2.5",
- "symfony/mailer": "<5.4",
- "symfony/messenger": "<5.4",
- "symfony/translation": "<5.4",
+ "symfony/mailer": "<6.4",
+ "symfony/messenger": "<6.4",
+ "symfony/translation": "<6.4",
"symfony/translation-contracts": "<2.5",
- "symfony/twig-bridge": "<5.4",
+ "symfony/twig-bridge": "<6.4",
"symfony/validator": "<6.4",
- "symfony/var-dumper": "<6.3",
- "twig/twig": "<2.13"
+ "symfony/var-dumper": "<6.4",
+ "twig/twig": "<3.0.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
- "symfony/browser-kit": "^5.4|^6.0|^7.0",
- "symfony/clock": "^6.2|^7.0",
- "symfony/config": "^6.1|^7.0",
- "symfony/console": "^5.4|^6.0|^7.0",
- "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/browser-kit": "^6.4|^7.0",
+ "symfony/clock": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0",
+ "symfony/css-selector": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
- "symfony/dom-crawler": "^5.4|^6.0|^7.0",
- "symfony/expression-language": "^5.4|^6.0|^7.0",
- "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/dom-crawler": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/finder": "^6.4|^7.0",
"symfony/http-client-contracts": "^2.5|^3",
- "symfony/process": "^5.4|^6.0|^7.0",
- "symfony/property-access": "^5.4.5|^6.0.5|^7.0",
- "symfony/routing": "^5.4|^6.0|^7.0",
- "symfony/serializer": "^6.4.4|^7.0.4",
- "symfony/stopwatch": "^5.4|^6.0|^7.0",
- "symfony/translation": "^5.4|^6.0|^7.0",
+ "symfony/process": "^6.4|^7.0",
+ "symfony/property-access": "^7.1",
+ "symfony/routing": "^6.4|^7.0",
+ "symfony/serializer": "^7.1",
+ "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/translation": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3",
- "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
- "symfony/var-dumper": "^5.4|^6.4|^7.0",
- "symfony/var-exporter": "^6.2|^7.0",
- "twig/twig": "^2.13|^3.0.4"
+ "symfony/var-dumper": "^6.4|^7.0",
+ "symfony/var-exporter": "^6.4|^7.0",
+ "twig/twig": "^3.0.4"
},
"type": "library",
"autoload": {
@@ -9733,7 +9284,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/v6.4.7"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.1.5"
},
"funding": [
{
@@ -9749,43 +9300,43 @@
"type": "tidelift"
}
],
- "time": "2024-04-29T11:24:44+00:00"
+ "time": "2024-09-21T06:09:21+00:00"
},
{
"name": "symfony/mailer",
- "version": "v6.4.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd"
+ "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/2c446d4e446995bed983c0b5bb9ff837e8de7dbd",
- "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b",
+ "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.1.10|^3|^4",
- "php": ">=8.1",
+ "php": ">=8.2",
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
- "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
- "symfony/mime": "^6.2|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
+ "symfony/mime": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/http-client-contracts": "<2.5",
- "symfony/http-kernel": "<5.4",
- "symfony/messenger": "<6.2",
- "symfony/mime": "<6.2",
- "symfony/twig-bridge": "<6.2.1"
+ "symfony/http-kernel": "<6.4",
+ "symfony/messenger": "<6.4",
+ "symfony/mime": "<6.4",
+ "symfony/twig-bridge": "<6.4"
},
"require-dev": {
- "symfony/console": "^5.4|^6.0|^7.0",
- "symfony/http-client": "^5.4|^6.0|^7.0",
- "symfony/messenger": "^6.2|^7.0",
- "symfony/twig-bridge": "^6.2|^7.0"
+ "symfony/console": "^6.4|^7.0",
+ "symfony/http-client": "^6.4|^7.0",
+ "symfony/messenger": "^6.4|^7.0",
+ "symfony/twig-bridge": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -9813,7 +9364,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v6.4.7"
+ "source": "https://github.com/symfony/mailer/tree/v7.1.5"
},
"funding": [
{
@@ -9829,25 +9380,24 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-09-08T12:32:26+00:00"
},
{
"name": "symfony/mime",
- "version": "v6.4.7",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "decadcf3865918ecfcbfa90968553994ce935a5e"
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/decadcf3865918ecfcbfa90968553994ce935a5e",
- "reference": "decadcf3865918ecfcbfa90968553994ce935a5e",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598",
+ "reference": "caa1e521edb2650b8470918dfe51708c237f0598",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
@@ -9855,18 +9405,18 @@
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
- "symfony/mailer": "<5.4",
- "symfony/serializer": "<6.3.2"
+ "symfony/mailer": "<6.4",
+ "symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/process": "^5.4|^6.4|^7.0",
- "symfony/property-access": "^5.4|^6.0|^7.0",
- "symfony/property-info": "^5.4|^6.0|^7.0",
- "symfony/serializer": "^6.3.2|^7.0"
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0",
+ "symfony/property-access": "^6.4|^7.0",
+ "symfony/property-info": "^6.4|^7.0",
+ "symfony/serializer": "^6.4.3|^7.0.3"
},
"type": "library",
"autoload": {
@@ -9898,7 +9448,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v6.4.7"
+ "source": "https://github.com/symfony/mime/tree/v7.1.6"
},
"funding": [
{
@@ -9914,20 +9464,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/options-resolver",
- "version": "v7.0.7",
+ "version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa"
+ "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa",
- "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55",
+ "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55",
"shasum": ""
},
"require": {
@@ -9965,7 +9515,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v7.0.7"
+ "source": "https://github.com/symfony/options-resolver/tree/v7.1.1"
},
"funding": [
{
@@ -9981,24 +9531,24 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:29:19+00:00"
+ "time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
- "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@@ -10044,7 +9594,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@@ -10060,24 +9610,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-iconv",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
- "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f"
+ "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f",
- "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956",
+ "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-iconv": "*"
@@ -10124,7 +9674,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0"
},
"funding": [
{
@@ -10140,24 +9690,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
- "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+ "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"
@@ -10202,7 +9752,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
},
"funding": [
{
@@ -10218,26 +9768,25 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
- "reference": "a287ed7475f85bf6f61890146edbc932c0fff919"
+ "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919",
- "reference": "a287ed7475f85bf6f61890146edbc932c0fff919",
+ "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"
@@ -10286,7 +9835,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
},
"funding": [
{
@@ -10302,24 +9851,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
- "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
+ "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"
@@ -10367,7 +9916,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
},
"funding": [
{
@@ -10383,24 +9932,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
- "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@@ -10447,7 +9996,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@@ -10463,97 +10012,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
- },
- {
- "name": "symfony/polyfill-php72",
- "version": "v1.29.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25",
- "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25",
- "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.29.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-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
- "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -10600,7 +10076,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@@ -10616,25 +10092,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "86fcae159633351e5fd145d1c47de6c528f8caff"
+ "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff",
- "reference": "86fcae159633351e5fd145d1c47de6c528f8caff",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
"shasum": ""
},
"require": {
- "php": ">=7.1",
- "symfony/polyfill-php80": "^1.14"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -10677,7 +10152,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
},
"funding": [
{
@@ -10693,24 +10168,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.29.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
- "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853"
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853",
- "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-uuid": "*"
@@ -10756,7 +10231,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0"
},
"funding": [
{
@@ -10772,24 +10247,24 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
- "version": "v6.4.7",
+ "version": "v7.1.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "cdb1c81c145fd5aa9b0038bab694035020943381"
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381",
- "reference": "cdb1c81c145fd5aa9b0038bab694035020943381",
+ "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585",
+ "reference": "9b8a40b7289767aa7117e957573c2a535efe6585",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.2"
},
"type": "library",
"autoload": {
@@ -10817,7 +10292,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.4.7"
+ "source": "https://github.com/symfony/process/tree/v7.1.7"
},
"funding": [
{
@@ -10833,47 +10308,42 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-11-06T09:25:12+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
- "version": "v2.3.1",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/psr-http-message-bridge.git",
- "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e"
+ "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e",
- "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e",
+ "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/405a7bcd872f1563966f64be19f1362d94ce71ab",
+ "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab",
"shasum": ""
},
"require": {
- "php": ">=7.2.5",
- "psr/http-message": "^1.0 || ^2.0",
- "symfony/deprecation-contracts": "^2.5 || ^3.0",
- "symfony/http-foundation": "^5.4 || ^6.0"
+ "php": ">=8.2",
+ "psr/http-message": "^1.0|^2.0",
+ "symfony/http-foundation": "^6.4|^7.0"
+ },
+ "conflict": {
+ "php-http/discovery": "<1.15",
+ "symfony/http-kernel": "<6.4"
},
"require-dev": {
"nyholm/psr7": "^1.1",
- "psr/log": "^1.1 || ^2 || ^3",
- "symfony/browser-kit": "^5.4 || ^6.0",
- "symfony/config": "^5.4 || ^6.0",
- "symfony/event-dispatcher": "^5.4 || ^6.0",
- "symfony/framework-bundle": "^5.4 || ^6.0",
- "symfony/http-kernel": "^5.4 || ^6.0",
- "symfony/phpunit-bridge": "^6.2"
- },
- "suggest": {
- "nyholm/psr7": "For a super lightweight PSR-7/17 implementation"
+ "php-http/discovery": "^1.15",
+ "psr/log": "^1.1.4|^2|^3",
+ "symfony/browser-kit": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0"
},
"type": "symfony-bridge",
- "extra": {
- "branch-alias": {
- "dev-main": "2.3-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Bridge\\PsrHttpMessage\\": ""
@@ -10893,11 +10363,11 @@
},
{
"name": "Symfony Community",
- "homepage": "http://symfony.com/contributors"
+ "homepage": "https://symfony.com/contributors"
}
],
"description": "PSR HTTP message bridge",
- "homepage": "http://symfony.com",
+ "homepage": "https://symfony.com",
"keywords": [
"http",
"http-message",
@@ -10905,8 +10375,7 @@
"psr-7"
],
"support": {
- "issues": "https://github.com/symfony/psr-http-message-bridge/issues",
- "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1"
+ "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.4"
},
"funding": [
{
@@ -10922,40 +10391,38 @@
"type": "tidelift"
}
],
- "time": "2023-07-26T11:53:26+00:00"
+ "time": "2024-08-15T22:48:53+00:00"
},
{
"name": "symfony/routing",
- "version": "v6.4.7",
+ "version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "276e06398f71fa2a973264d94f28150f93cfb907"
+ "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/276e06398f71fa2a973264d94f28150f93cfb907",
- "reference": "276e06398f71fa2a973264d94f28150f93cfb907",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7",
+ "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
- "doctrine/annotations": "<1.12",
- "symfony/config": "<6.2",
- "symfony/dependency-injection": "<5.4",
- "symfony/yaml": "<5.4"
+ "symfony/config": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/yaml": "<6.4"
},
"require-dev": {
- "doctrine/annotations": "^1.12|^2",
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.2|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/expression-language": "^5.4|^6.0|^7.0",
- "symfony/http-foundation": "^5.4|^6.0|^7.0",
- "symfony/yaml": "^5.4|^6.0|^7.0"
+ "symfony/config": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/expression-language": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/yaml": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -10989,7 +10456,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v6.4.7"
+ "source": "https://github.com/symfony/routing/tree/v7.1.4"
},
"funding": [
{
@@ -11005,7 +10472,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-08-29T08:16:25+00:00"
},
{
"name": "symfony/service-contracts",
@@ -11092,16 +10559,16 @@
},
{
"name": "symfony/stopwatch",
- "version": "v7.0.7",
+ "version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
- "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84"
+ "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84",
- "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
+ "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
"shasum": ""
},
"require": {
@@ -11134,7 +10601,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/stopwatch/tree/v7.0.7"
+ "source": "https://github.com/symfony/stopwatch/tree/v7.1.1"
},
"funding": [
{
@@ -11150,20 +10617,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:29:19+00:00"
+ "time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/string",
- "version": "v7.0.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63"
+ "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63",
- "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63",
+ "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306",
+ "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306",
"shasum": ""
},
"require": {
@@ -11177,6 +10644,7 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
+ "symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
@@ -11220,7 +10688,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.0.7"
+ "source": "https://github.com/symfony/string/tree/v7.1.5"
},
"funding": [
{
@@ -11236,37 +10704,36 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:29:19+00:00"
+ "time": "2024-09-20T08:28:38+00:00"
},
{
"name": "symfony/translation",
- "version": "v6.4.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "7495687c58bfd88b7883823747b0656d90679123"
+ "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/7495687c58bfd88b7883823747b0656d90679123",
- "reference": "7495687c58bfd88b7883823747b0656d90679123",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea",
+ "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0"
},
"conflict": {
- "symfony/config": "<5.4",
- "symfony/console": "<5.4",
- "symfony/dependency-injection": "<5.4",
+ "symfony/config": "<6.4",
+ "symfony/console": "<6.4",
+ "symfony/dependency-injection": "<6.4",
"symfony/http-client-contracts": "<2.5",
- "symfony/http-kernel": "<5.4",
+ "symfony/http-kernel": "<6.4",
"symfony/service-contracts": "<2.5",
- "symfony/twig-bundle": "<5.4",
- "symfony/yaml": "<5.4"
+ "symfony/twig-bundle": "<6.4",
+ "symfony/yaml": "<6.4"
},
"provide": {
"symfony/translation-implementation": "2.3|3.0"
@@ -11274,17 +10741,17 @@
"require-dev": {
"nikic/php-parser": "^4.18|^5.0",
"psr/log": "^1|^2|^3",
- "symfony/config": "^5.4|^6.0|^7.0",
- "symfony/console": "^5.4|^6.0|^7.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0",
- "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/config": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/finder": "^6.4|^7.0",
"symfony/http-client-contracts": "^2.5|^3.0",
- "symfony/http-kernel": "^5.4|^6.0|^7.0",
- "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/intl": "^6.4|^7.0",
"symfony/polyfill-intl-icu": "^1.21",
- "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/routing": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^5.4|^6.0|^7.0"
+ "symfony/yaml": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -11315,7 +10782,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v6.4.7"
+ "source": "https://github.com/symfony/translation/tree/v7.1.5"
},
"funding": [
{
@@ -11331,7 +10798,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-09-16T06:30:38+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -11413,24 +10880,24 @@
},
{
"name": "symfony/uid",
- "version": "v6.4.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355"
+ "reference": "8c7bb8acb933964055215d89f9a9871df0239317"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/a66efcb71d8bc3a207d9d78e0bd67f3321510355",
- "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317",
+ "reference": "8c7bb8acb933964055215d89f9a9871df0239317",
"shasum": ""
},
"require": {
- "php": ">=8.1",
+ "php": ">=8.2",
"symfony/polyfill-uuid": "^1.15"
},
"require-dev": {
- "symfony/console": "^5.4|^6.0|^7.0"
+ "symfony/console": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -11467,7 +10934,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v6.4.7"
+ "source": "https://github.com/symfony/uid/tree/v7.1.5"
},
"funding": [
{
@@ -11483,38 +10950,36 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-09-17T09:16:35+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v6.4.7",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7"
+ "reference": "e20e03889539fd4e4211e14d2179226c513c010d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7a9cd977cd1c5fed3694bee52990866432af07d7",
- "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d",
+ "reference": "e20e03889539fd4e4211e14d2179226c513c010d",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "symfony/deprecation-contracts": "^2.5|^3",
+ "php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
- "symfony/console": "<5.4"
+ "symfony/console": "<6.4"
},
"require-dev": {
"ext-iconv": "*",
- "symfony/console": "^5.4|^6.0|^7.0",
- "symfony/error-handler": "^6.3|^7.0",
- "symfony/http-kernel": "^5.4|^6.0|^7.0",
- "symfony/process": "^5.4|^6.0|^7.0",
- "symfony/uid": "^5.4|^6.0|^7.0",
- "twig/twig": "^2.13|^3.0.4"
+ "symfony/console": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0",
+ "symfony/uid": "^6.4|^7.0",
+ "twig/twig": "^3.0.4"
},
"bin": [
"Resources/bin/var-dump-server"
@@ -11552,7 +11017,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v6.4.7"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.1.5"
},
"funding": [
{
@@ -11568,20 +11033,20 @@
"type": "tidelift"
}
],
- "time": "2024-04-18T09:22:46+00:00"
+ "time": "2024-09-16T10:07:02+00:00"
},
{
"name": "symfony/yaml",
- "version": "v6.4.7",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0"
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0",
- "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971",
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971",
"shasum": ""
},
"require": {
@@ -11624,7 +11089,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v6.4.7"
+ "source": "https://github.com/symfony/yaml/tree/v6.4.12"
},
"funding": [
{
@@ -11640,7 +11105,7 @@
"type": "tidelift"
}
],
- "time": "2024-04-28T10:28:08+00:00"
+ "time": "2024-09-17T12:47:12+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -11756,23 +11221,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"
@@ -11789,7 +11254,7 @@
"extra": {
"bamarni-bin": {
"bin-links": true,
- "forward-command": true
+ "forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
@@ -11824,7 +11289,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": [
{
@@ -11836,7 +11301,7 @@
"type": "tidelift"
}
],
- "time": "2023-11-12T22:43:29+00:00"
+ "time": "2024-07-20T21:52:34+00:00"
},
{
"name": "voku/portable-ascii",
@@ -12082,30 +11547,31 @@
},
{
"name": "zbateson/mail-mime-parser",
- "version": "2.4.1",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mail-mime-parser.git",
- "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c"
+ "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/ff49e02f6489b38f7cc3d1bd3971adc0f872569c",
- "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c",
+ "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/e0d4423fe27850c9dd301190767dbc421acc2f19",
+ "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19",
"shasum": ""
},
"require": {
- "guzzlehttp/psr7": "^1.7.0|^2.0",
- "php": ">=7.1",
- "pimple/pimple": "^3.0",
- "zbateson/mb-wrapper": "^1.0.1",
- "zbateson/stream-decorators": "^1.0.6"
+ "guzzlehttp/psr7": "^2.5",
+ "php": ">=8.0",
+ "php-di/php-di": "^6.0|^7.0",
+ "psr/log": "^1|^2|^3",
+ "zbateson/mb-wrapper": "^2.0",
+ "zbateson/stream-decorators": "^2.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
- "mikey179/vfsstream": "^1.6.0",
+ "monolog/monolog": "^2|^3",
"phpstan/phpstan": "*",
- "phpunit/phpunit": "<10"
+ "phpunit/phpunit": "^9.6"
},
"suggest": {
"ext-iconv": "For best support/performance",
@@ -12153,24 +11619,24 @@
"type": "github"
}
],
- "time": "2024-04-28T00:58:54+00:00"
+ "time": "2024-08-10T18:44:09+00:00"
},
{
"name": "zbateson/mb-wrapper",
- "version": "1.2.1",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/zbateson/mb-wrapper.git",
- "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f"
+ "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/09a8b77eb94af3823a9a6623dcc94f8d988da67f",
- "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f",
+ "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/9e4373a153585d12b6c621ac4a6bb143264d4619",
+ "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619",
"shasum": ""
},
"require": {
- "php": ">=7.1",
+ "php": ">=8.0",
"symfony/polyfill-iconv": "^1.9",
"symfony/polyfill-mbstring": "^1.9"
},
@@ -12214,7 +11680,7 @@
],
"support": {
"issues": "https://github.com/zbateson/mb-wrapper/issues",
- "source": "https://github.com/zbateson/mb-wrapper/tree/1.2.1"
+ "source": "https://github.com/zbateson/mb-wrapper/tree/2.0.0"
},
"funding": [
{
@@ -12222,31 +11688,31 @@
"type": "github"
}
],
- "time": "2024-03-18T04:31:04+00:00"
+ "time": "2024-03-20T01:38:07+00:00"
},
{
"name": "zbateson/stream-decorators",
- "version": "1.2.1",
+ "version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/zbateson/stream-decorators.git",
- "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9"
+ "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/783b034024fda8eafa19675fb2552f8654d3a3e9",
- "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9",
+ "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
+ "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5",
"shasum": ""
},
"require": {
- "guzzlehttp/psr7": "^1.9 | ^2.0",
- "php": ">=7.2",
- "zbateson/mb-wrapper": "^1.0.0"
+ "guzzlehttp/psr7": "^2.5",
+ "php": ">=8.0",
+ "zbateson/mb-wrapper": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
- "phpunit/phpunit": "<10.0"
+ "phpunit/phpunit": "^9.6|^10.0"
},
"type": "library",
"autoload": {
@@ -12277,7 +11743,7 @@
],
"support": {
"issues": "https://github.com/zbateson/stream-decorators/issues",
- "source": "https://github.com/zbateson/stream-decorators/tree/1.2.1"
+ "source": "https://github.com/zbateson/stream-decorators/tree/2.1.1"
},
"funding": [
{
@@ -12285,10 +11751,175 @@
"type": "github"
}
],
- "time": "2023-05-30T22:51:52+00:00"
+ "time": "2024-04-29T21:42:39+00:00"
+ },
+ {
+ "name": "zircote/swagger-php",
+ "version": "4.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zircote/swagger-php.git",
+ "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zircote/swagger-php/zipball/3b6f3800f4fd6544ada4dce180c6b69eaead7c7c",
+ "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=7.2",
+ "psr/log": "^1.1 || ^2.0 || ^3.0",
+ "symfony/deprecation-contracts": "^2 || ^3",
+ "symfony/finder": ">=2.2",
+ "symfony/yaml": ">=3.3"
+ },
+ "require-dev": {
+ "composer/package-versions-deprecated": "^1.11",
+ "doctrine/annotations": "^1.7 || ^2.0",
+ "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0",
+ "phpstan/phpstan": "^1.6",
+ "phpunit/phpunit": ">=8",
+ "vimeo/psalm": "^4.23"
+ },
+ "suggest": {
+ "doctrine/annotations": "^1.7 || ^2.0"
+ },
+ "bin": [
+ "bin/openapi"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenApi\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Robert Allen",
+ "email": "zircote@gmail.com"
+ },
+ {
+ "name": "Bob Fanger",
+ "email": "bfanger@gmail.com",
+ "homepage": "https://bfanger.nl"
+ },
+ {
+ "name": "Martin Rademacher",
+ "email": "mano@radebatz.net",
+ "homepage": "https://radebatz.net"
+ }
+ ],
+ "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations",
+ "homepage": "https://github.com/zircote/swagger-php/",
+ "keywords": [
+ "api",
+ "json",
+ "rest",
+ "service discovery"
+ ],
+ "support": {
+ "issues": "https://github.com/zircote/swagger-php/issues",
+ "source": "https://github.com/zircote/swagger-php/tree/4.11.0"
+ },
+ "time": "2024-10-09T03:11:12+00:00"
}
],
"packages-dev": [
+ {
+ "name": "barryvdh/laravel-debugbar",
+ "version": "v3.14.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/laravel-debugbar.git",
+ "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd",
+ "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/routing": "^9|^10|^11",
+ "illuminate/session": "^9|^10|^11",
+ "illuminate/support": "^9|^10|^11",
+ "maximebf/debugbar": "~1.23.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.14-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.14.3"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-02T09:17:49+00:00"
+ },
{
"name": "brianium/paratest",
"version": "v7.4.3",
@@ -12448,16 +12079,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": {
@@ -12497,7 +12128,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": [
{
@@ -12505,30 +12136,30 @@
"type": "github"
}
],
- "time": "2024-02-07T09:43:46+00:00"
+ "time": "2024-08-06T10:04:20+00:00"
},
{
"name": "filp/whoops",
- "version": "2.15.4",
+ "version": "2.16.0",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546"
+ "reference": "befcdc0e5dce67252aa6322d82424be928214fa2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546",
- "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2",
+ "reference": "befcdc0e5dce67252aa6322d82424be928214fa2",
"shasum": ""
},
"require": {
- "php": "^5.5.9 || ^7.0 || ^8.0",
+ "php": "^7.1 || ^8.0",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"require-dev": {
- "mockery/mockery": "^0.9 || ^1.0",
- "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
- "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3",
+ "symfony/var-dumper": "^4.0 || ^5.0"
},
"suggest": {
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
@@ -12568,7 +12199,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
- "source": "https://github.com/filp/whoops/tree/2.15.4"
+ "source": "https://github.com/filp/whoops/tree/2.16.0"
},
"funding": [
{
@@ -12576,7 +12207,7 @@
"type": "github"
}
],
- "time": "2023-11-03T12:00:00+00:00"
+ "time": "2024-09-25T12:00:00+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -12631,47 +12262,43 @@
},
{
"name": "laravel/dusk",
- "version": "v7.13.0",
+ "version": "v8.2.8",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
- "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6"
+ "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/dusk/zipball/dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6",
- "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6",
+ "url": "https://api.github.com/repos/laravel/dusk/zipball/5bff1e8dd87ec653a2202475377152e5d14fde40",
+ "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
- "guzzlehttp/guzzle": "^7.2",
- "illuminate/console": "^9.0|^10.0",
- "illuminate/support": "^9.0|^10.0",
- "nesbot/carbon": "^2.0",
- "php": "^8.0",
+ "guzzlehttp/guzzle": "^7.5",
+ "illuminate/console": "^10.0|^11.0",
+ "illuminate/support": "^10.0|^11.0",
+ "php": "^8.1",
"php-webdriver/webdriver": "^1.9.0",
- "symfony/console": "^6.0",
- "symfony/finder": "^6.0",
- "symfony/process": "^6.0",
+ "symfony/console": "^6.2|^7.0",
+ "symfony/finder": "^6.2|^7.0",
+ "symfony/process": "^6.2|^7.0",
"vlucas/phpdotenv": "^5.2"
},
"require-dev": {
- "mockery/mockery": "^1.4.2",
- "orchestra/testbench": "^7.33|^8.13",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^8.19|^9.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.5.10|^10.0.1",
- "psy/psysh": "^0.11.12"
+ "phpunit/phpunit": "^10.1|^11.0",
+ "psy/psysh": "^0.11.12|^0.12"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "7.x-dev"
- },
"laravel": {
"providers": [
"Laravel\\Dusk\\DuskServiceProvider"
@@ -12701,22 +12328,22 @@
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
- "source": "https://github.com/laravel/dusk/tree/v7.13.0"
+ "source": "https://github.com/laravel/dusk/tree/v8.2.8"
},
- "time": "2024-02-23T22:29:53+00:00"
+ "time": "2024-10-04T14:02:20+00:00"
},
{
"name": "laravel/pint",
- "version": "v1.16.0",
+ "version": "v1.18.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98"
+ "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98",
- "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9",
+ "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9",
"shasum": ""
},
"require": {
@@ -12727,13 +12354,13 @@
"php": "^8.1.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.57.1",
- "illuminate/view": "^10.48.10",
- "larastan/larastan": "^2.9.6",
+ "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.7"
+ "pestphp/pest": "^2.35.1"
},
"bin": [
"builds/pint"
@@ -12769,7 +12396,144 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2024-05-21T18:08:25+00:00"
+ "time": "2024-09-24T17:22:50+00:00"
+ },
+ {
+ "name": "laravel/telescope",
+ "version": "v5.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/telescope.git",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d",
+ "reference": "749369e996611d803e7c1b57929b482dd676008d",
+ "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.4"
+ },
+ "time": "2024-10-29T15:35:13+00:00"
+ },
+ {
+ "name": "maximebf/debugbar",
+ "version": "v1.23.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maximebf/php-debugbar.git",
+ "reference": "689720d724c771ac4add859056744b7b3f2406da"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da",
+ "reference": "689720d724c771ac4add859056744b7b3f2406da",
+ "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.23-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.23.2"
+ },
+ "time": "2024-09-16T11:23:09+00:00"
},
{
"name": "mockery/mockery",
@@ -12856,16 +12620,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.11.1",
+ "version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
@@ -12873,11 +12637,12 @@
},
"conflict": {
"doctrine/collections": "<1.6.8",
- "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
@@ -12903,7 +12668,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
@@ -12911,44 +12676,42 @@
"type": "tidelift"
}
],
- "time": "2023-03-08T13:26:56+00:00"
+ "time": "2024-06-12T14:39:25+00:00"
},
{
"name": "nunomaduro/collision",
- "version": "v7.10.0",
+ "version": "v8.4.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "49ec67fa7b002712da8526678abd651c09f375b2"
+ "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/49ec67fa7b002712da8526678abd651c09f375b2",
- "reference": "49ec67fa7b002712da8526678abd651c09f375b2",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/e7d1aa8ed753f63fa816932bbc89678238843b4a",
+ "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a",
"shasum": ""
},
"require": {
- "filp/whoops": "^2.15.3",
- "nunomaduro/termwind": "^1.15.1",
- "php": "^8.1.0",
- "symfony/console": "^6.3.4"
+ "filp/whoops": "^2.15.4",
+ "nunomaduro/termwind": "^2.0.1",
+ "php": "^8.2.0",
+ "symfony/console": "^7.1.3"
},
"conflict": {
- "laravel/framework": ">=11.0.0"
+ "laravel/framework": "<11.0.0 || >=12.0.0",
+ "phpunit/phpunit": "<10.5.1 || >=12.0.0"
},
"require-dev": {
- "brianium/paratest": "^7.3.0",
- "laravel/framework": "^10.28.0",
- "laravel/pint": "^1.13.3",
- "laravel/sail": "^1.25.0",
- "laravel/sanctum": "^3.3.1",
- "laravel/tinker": "^2.8.2",
- "nunomaduro/larastan": "^2.6.4",
- "orchestra/testbench-core": "^8.13.0",
- "pestphp/pest": "^2.23.2",
- "phpunit/phpunit": "^10.4.1",
- "sebastian/environment": "^6.0.1",
- "spatie/laravel-ignition": "^2.3.1"
+ "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.2.3",
+ "pestphp/pest": "^2.35.0 || ^3.0.0",
+ "sebastian/environment": "^6.1.0 || ^7.0.0"
},
"type": "library",
"extra": {
@@ -12956,6 +12719,9 @@
"providers": [
"NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
]
+ },
+ "branch-alias": {
+ "dev-8.x": "8.x-dev"
}
},
"autoload": {
@@ -13007,25 +12773,25 @@
"type": "patreon"
}
],
- "time": "2023-10-11T15:45:01+00:00"
+ "time": "2024-08-03T15:32:23+00:00"
},
{
"name": "pestphp/pest",
- "version": "v2.34.7",
+ "version": "v2.35.1",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76"
+ "reference": "b13acb630df52c06123588d321823c31fc685545"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/a7a3e4240e341d0fee1c54814ce18adc26ce5a76",
- "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76",
+ "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",
@@ -13039,8 +12805,8 @@
},
"require-dev": {
"pestphp/pest-dev-tools": "^2.16.0",
- "pestphp/pest-plugin-type-coverage": "^2.8.1",
- "symfony/process": "^6.4.0|^7.0.4"
+ "pestphp/pest-plugin-type-coverage": "^2.8.5",
+ "symfony/process": "^6.4.0|^7.1.3"
},
"bin": [
"bin/pest"
@@ -13103,7 +12869,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v2.34.7"
+ "source": "https://github.com/pestphp/pest/tree/v2.35.1"
},
"funding": [
{
@@ -13115,7 +12881,7 @@
"type": "github"
}
],
- "time": "2024-04-05T07:44:17+00:00"
+ "time": "2024-08-20T21:41:50+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -13508,32 +13274,32 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "10.1.14",
+ "version": "10.1.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b"
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b",
- "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b",
+ "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"
@@ -13545,7 +13311,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "10.1-dev"
+ "dev-main": "10.1.x-dev"
}
},
"autoload": {
@@ -13574,7 +13340,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.14"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
},
"funding": [
{
@@ -13582,7 +13348,7 @@
"type": "github"
}
],
- "time": "2024-03-12T15:33:41+00:00"
+ "time": "2024-08-22T04:31:57+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -14098,16 +13864,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": {
@@ -14118,7 +13884,7 @@
"sebastian/exporter": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.3"
+ "phpunit/phpunit": "^10.4"
},
"type": "library",
"extra": {
@@ -14163,7 +13929,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": [
{
@@ -14171,7 +13937,7 @@
"type": "github"
}
],
- "time": "2023-08-14T13:18:12+00:00"
+ "time": "2024-08-12T06:03:08+00:00"
},
{
"name": "sebastian/complexity",
@@ -14890,23 +14656,97 @@
"time": "2022-05-20T15:13:10+00:00"
},
{
- "name": "spatie/flare-client-php",
- "version": "1.6.0",
+ "name": "spatie/error-solutions",
+ "version": "1.1.1",
"source": {
"type": "git",
- "url": "https://github.com/spatie/flare-client-php.git",
- "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462"
+ "url": "https://github.com/spatie/error-solutions.git",
+ "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/220a7c8745e9fa427d54099f47147c4b97fe6462",
- "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462",
+ "url": "https://api.github.com/repos/spatie/error-solutions/zipball/ae7393122eda72eed7cc4f176d1e96ea444f2d67",
+ "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "illuminate/broadcasting": "^10.0|^11.0",
+ "illuminate/cache": "^10.0|^11.0",
+ "illuminate/support": "^10.0|^11.0",
+ "livewire/livewire": "^2.11|^3.3.5",
+ "openai-php/client": "^0.10.1",
+ "orchestra/testbench": "^7.0|8.22.3|^9.0",
+ "pestphp/pest": "^2.20",
+ "phpstan/phpstan": "^1.11",
+ "psr/simple-cache": "^3.0",
+ "psr/simple-cache-implementation": "^3.0",
+ "spatie/ray": "^1.28",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "vlucas/phpdotenv": "^5.5"
+ },
+ "suggest": {
+ "openai-php/client": "Require get solutions from OpenAI",
+ "simple-cache-implementation": "To cache solutions from OpenAI"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Spatie\\Ignition\\": "legacy/ignition",
+ "Spatie\\ErrorSolutions\\": "src",
+ "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ruben Van Assche",
+ "email": "ruben@spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "This is my package error-solutions",
+ "homepage": "https://github.com/spatie/error-solutions",
+ "keywords": [
+ "error-solutions",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/error-solutions/issues",
+ "source": "https://github.com/spatie/error-solutions/tree/1.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-25T11:06:04+00:00"
+ },
+ {
+ "name": "spatie/flare-client-php",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/flare-client-php.git",
+ "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
+ "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
"shasum": ""
},
"require": {
"illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0",
"php": "^8.0",
- "spatie/backtrace": "^1.5.2",
+ "spatie/backtrace": "^1.6.1",
"symfony/http-foundation": "^5.2|^6.0|^7.0",
"symfony/mime": "^5.2|^6.0|^7.0",
"symfony/process": "^5.2|^6.0|^7.0",
@@ -14918,7 +14758,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": {
@@ -14948,7 +14788,7 @@
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
- "source": "https://github.com/spatie/flare-client-php/tree/1.6.0"
+ "source": "https://github.com/spatie/flare-client-php/tree/1.8.0"
},
"funding": [
{
@@ -14956,28 +14796,28 @@
"type": "github"
}
],
- "time": "2024-05-22T09:45:39+00:00"
+ "time": "2024-08-01T08:27:26+00:00"
},
{
"name": "spatie/ignition",
- "version": "1.14.1",
+ "version": "1.15.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ignition.git",
- "reference": "c23cc018c5f423d2f413b99f84655fceb6549811"
+ "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/ignition/zipball/c23cc018c5f423d2f413b99f84655fceb6549811",
- "reference": "c23cc018c5f423d2f413b99f84655fceb6549811",
+ "url": "https://api.github.com/repos/spatie/ignition/zipball/e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
+ "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^8.0",
- "spatie/backtrace": "^1.5.3",
- "spatie/flare-client-php": "^1.4.0",
+ "spatie/error-solutions": "^1.0",
+ "spatie/flare-client-php": "^1.7",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0"
},
@@ -15039,20 +14879,20 @@
"type": "github"
}
],
- "time": "2024-05-03T15:56:16+00:00"
+ "time": "2024-06-12T14:55:22+00:00"
},
{
"name": "spatie/laravel-ignition",
- "version": "2.7.0",
+ "version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
- "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57"
+ "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/f52124d50122611e8a40f628cef5c19ff6cc5b57",
- "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57",
+ "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/3c067b75bfb50574db8f7e2c3978c65eed71126c",
+ "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c",
"shasum": ""
},
"require": {
@@ -15061,8 +14901,7 @@
"ext-mbstring": "*",
"illuminate/support": "^10.0|^11.0",
"php": "^8.1",
- "spatie/flare-client-php": "^1.5",
- "spatie/ignition": "^1.14",
+ "spatie/ignition": "^1.15",
"symfony/console": "^6.2.3|^7.0",
"symfony/var-dumper": "^6.2.3|^7.0"
},
@@ -15131,7 +14970,178 @@
"type": "github"
}
],
- "time": "2024-05-02T13:42:49+00:00"
+ "time": "2024-06-12T15:01:18+00:00"
+ },
+ {
+ "name": "symfony/http-client",
+ "version": "v6.4.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826",
+ "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-client-contracts": "^3.4.1",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "php-http/discovery": "<1.15",
+ "symfony/http-foundation": "<6.3"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/amp": "^2.5",
+ "amphp/http-client": "^4.2.1",
+ "amphp/http-tunnel": "^1.0",
+ "amphp/socket": "^1.1",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v6.4.14"
+ },
+ "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-11-05T16:39:55+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "20414d96f391677bf80078aa55baece78b82647d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d",
+ "reference": "20414d96f391677bf80078aa55baece78b82647d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "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": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.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-04-18T09:32:20+00:00"
},
{
"name": "ta-tikoma/phpunit-architecture-test",
diff --git a/config/app.php b/config/app.php
index 41e94c09b..371ac44ec 100644
--- a/config/app.php
+++ b/config/app.php
@@ -199,7 +199,6 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
-
],
/*
diff --git a/config/constants.php b/config/constants.php
index 444d144a8..1bec2e3bf 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -1,12 +1,14 @@
'26.0',
'docs' => [
'base_url' => 'https://coolify.io/docs',
'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,
@@ -17,13 +19,13 @@ return [
'invitation' => [
'link' => [
'base_url' => '/invitations/',
- 'expiration' => 10,
+ 'expiration_days' => 3,
],
],
'services' => [
// Temporary disabled until cache is implemented
- 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
- // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
+ // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
+ 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
],
'limits' => [
'trial_period' => 0,
diff --git a/config/coolify.php b/config/coolify.php
index c7cfe6101..225dfe6fa 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -1,18 +1,17 @@
env('SENTRY_DSN'),
'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact',
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
'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),
- 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false),
];
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..daeea96b6
--- /dev/null
+++ b/config/debugbar.php
@@ -0,0 +1,326 @@
+ env('DEBUGBAR_ENABLED', null),
+ 'except' => [
+ 'telescope*',
+ 'horizon*',
+ 'api*',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | 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