diff --git a/.dockerignore b/.dockerignore
index 3a0ec49f7..6f3c903f0 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,7 +3,6 @@
/public/build
/public/hot
/public/storage
-/storage/*.key
/vendor
.env
.env.backup
@@ -23,3 +22,17 @@ yarn-error.log
.rnd
/.ssh
.ignition.json
+.env.dusk.local
+docker/coolify-realtime/node_modules
+
+/storage/*.key
+/storage/app/backups
+/storage/app/ssh/keys
+/storage/app/ssh/mux
+/storage/app/tmp
+/storage/app/debugbar
+/storage/logs
+/storage/pail
+
+
+
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.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/.gitattributes b/.gitattributes
index fcb21d396..c48a5898b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,4 +8,4 @@
/.github export-ignore
CHANGELOG.md export-ignore
-.styleci.yml export-ignore
+.styleci.yml export-ignore
\ No newline at end of file
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/lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml
similarity index 100%
rename from .github/workflows/lock-closed-issues-discussions-and-prs.yml
rename to .github/workflows/chore-lock-closed-issues-discussions-and-prs.yml
diff --git a/.github/workflows/manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml
similarity index 100%
rename from .github/workflows/manage-stale-issues-and-prs.yml
rename to .github/workflows/chore-manage-stale-issues-and-prs.yml
diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml
similarity index 100%
rename from .github/workflows/remove-labels-and-assignees-on-close.yml
rename to .github/workflows/chore-remove-labels-and-assignees-on-close.yml
diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml
index 4add8516e..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,25 +20,36 @@ 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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - 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 }}:${{ steps.version.outputs.VERSION }}-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:
@@ -47,27 +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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - 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 }}:${{ steps.version.outputs.VERSION }}-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:
@@ -75,25 +99,42 @@ 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: 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: Create & publish manifest
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ 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:
diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml
index a9e8a5dd0..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,25 +20,36 @@ 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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - 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 }}:${{ steps.version.outputs.VERSION }}
+ 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:
@@ -47,25 +59,36 @@ 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: 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 image and push to registry
- uses: docker/build-push-action@v5
+ 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 }}:${{ steps.version.outputs.VERSION }}-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:
@@ -75,25 +98,43 @@ 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: 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: Create & publish manifest
+ 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.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
+ 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:
diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml
new file mode 100644
index 000000000..d7244fc84
--- /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/production/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/production/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
index 33e048627..ef247170f 100644
--- a/.github/workflows/coolify-realtime-next.yml
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -1,17 +1,19 @@
-name: Coolify Realtime Development (v4)
+name: Coolify Realtime Development
on:
push:
branches: [ "next" ]
paths:
- - .github/workflows/coolify-realtime.yml
+ - .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:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
@@ -22,27 +24,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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ 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:
@@ -50,27 +64,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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ 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:
@@ -78,26 +104,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: 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
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ 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_PROD_RELEASE_CHANNEL }}
+ webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
index 30910ae0b..9654a21b0 100644
--- a/.github/workflows/coolify-realtime.yml
+++ b/.github/workflows/coolify-realtime.yml
@@ -1,4 +1,4 @@
-name: Coolify Realtime (v4)
+name: Coolify Realtime
on:
push:
@@ -8,10 +8,12 @@ on:
- 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:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
@@ -22,27 +24,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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ 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:
@@ -50,27 +64,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: 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 image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ 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:
@@ -78,25 +104,43 @@ 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: 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
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
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
+ 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:
diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml
new file mode 100644
index 000000000..bcb65ecbf
--- /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/production/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/production/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/production-build.yml b/.github/workflows/production-build.yml
deleted file mode 100644
index c78c865bf..000000000
--- a/.github/workflows/production-build.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: Production Build (v4)
-
-on:
- push:
- branches: ["main"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
- - 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 09504afee..d7ee7e96c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,6 @@ _ide_helper_models.php
/.ssh
scripts/load-test/*
.ignition.json
+.env.dusk.local
+docker/coolify-realtime/node_modules
+.DS_Store
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 6fd6797b5..000000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-tasks:
- - name: Setup Spin environment and Composer dependencies
- # Fix because of https://github.com/gitpod-io/gitpod/issues/16614
- before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
- init: |
- cp .env.development.example .env &&
- sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
- sed -i "s#USERID=#USERID=33333#g" .env
- sed -i "s#GROUPID=#GROUPID=33333#g" .env
- composer install --ignore-platform-reqs
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec -u webuser coolify php artisan key:generate
- ./vendor/bin/spin exec -u webuser coolify php artisan storage:link
- ./vendor/bin/spin exec -u webuser coolify php artisan migrate:fresh --seed
- cat .coolify-logo
- gp sync-done spin-is-ready
-
- - name: Install Node dependencies and run Vite
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec vite npm install
- ./vendor/bin/spin exec vite npm run dev -- --host
-
- - name: Laravel Queue Worker, listening to code changes
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec -u webuser coolify php artisan queue:listen
-
-ports:
- - port: 5432
- onOpen: ignore
- name: PostgreSQL
- visibility: public
- - port: 5173
- onOpen: ignore
- visibility: public
- name: Node Server for Vite
- - port: 8000
- onOpen: ignore
- visibility: public
- name: Coolify
-
-# Configure vscode
-vscode:
- extensions:
- - bmewburn.vscode-intelephense-client
- - ikappas.composer
- - ms-azuretools.vscode-docker
- - ecmel.vscode-html-css
- - MehediDracula.php-namespace-resolver
- - wmaurer.change-case
- - Equinusocio.vsc-community-material-theme
- - EditorConfig.EditorConfig
- - streetsidesoftware.code-spell-checker
- - rangav.vscode-thunder-client
- - PKief.material-icon-theme
- - cierra.livewire-vscode
- - lennardv.livewire-goto-updated
- - bradlc.vscode-tailwindcss
- - heybourn.headwind
- - adrianwilczynski.alpine-js-intellisense
- - amiralizadeh9480.laravel-extra-intellisense
- - shufo.vscode-blade-formatter
diff --git a/README.md b/README.md
index 14a741088..dac48d127 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# About the Project
-Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
+Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
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.
@@ -22,6 +22,9 @@ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
+> [!NOTE]
+> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
+
# Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
@@ -37,21 +40,21 @@ 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.
+* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
-* [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.
@@ -60,6 +63,7 @@ Special thanks to our biggest sponsors!
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
+* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider.
## Github Sponsors ($40+)
@@ -87,7 +91,11 @@ Special thanks to our biggest sponsors!
-
+
+
+
+
+
## Organizations
@@ -121,7 +129,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
-
# Recognitions
@@ -138,6 +145,13 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
+# Core Maintainers
+
+| Andras Bacsai | 🏔️ Peak |
+|------------|------------|
+|
|
|
+|
|
|
+
# Repo Activity

diff --git a/RELEASE.md b/RELEASE.md
index d9f05f17d..bc159b040 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,6 @@
# 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.
+This guide outlines the release process for Coolify, intended for developers and those interested in understanding how Coolify releases are managed and deployed.
## Table of Contents
- [Release Process](#release-process)
@@ -19,19 +19,19 @@ This guide outlines the release process for Coolify, intended for developers and
- 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.
+ - Once ready, changes are merged from the `next` branch into the `main` branch (via a pull request).
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.
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry and Docker Hub with the specific 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)
+ - To make a new version publicly available, the version information on the CDN needs to be updated manually. After that the new version number will be available at [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.**
+> 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. After the CDN is updated, a discord announcement will be made in the Production Release channel.**
## Version Types
@@ -39,10 +39,10 @@ This guide outlines the release process for Coolify, intended for developers and
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.
+ - The production version suitable for stable, production environments (recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible fixes.
- **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`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`, `4.1.0`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -57,7 +57,7 @@ This guide outlines the release process for Coolify, intended for developers and
- 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
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-nightly.1`, `4.1.0-nightly.2`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
@@ -73,11 +73,11 @@ This guide outlines the release process for Coolify, intended for developers and
- **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`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`, `4.1.0-beta.2`, etc.).
- **Installation Command:**
- ```bash
+ ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
- ```
+ ```
@@ -117,12 +117,15 @@ When a new version is released and a new GitHub release is created, it doesn't i
> [!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
+## Manually Update/ Downgrade to Specific Versions
> [!CAUTION]
-> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
+> Updating to unreleased versions is not recommended and can cause issues.
-To update your Coolify instance to a specific (unreleased) version, use the following command:
+> [!IMPORTANT]
+> Downgrading is supported but not recommended and can cause issues because of database migrations and other changes.
+
+To update your Coolify instance to a specific version, use the following command:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s
diff --git a/SECURITY.md b/SECURITY.md
index ad3a4addd..0711bf5b5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,15 +2,24 @@
## Supported Versions
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
+Currently supported, maintained and updated versions:
-| Version | Supported |
-| ------- | ------------------ |
-| > 4 | :white_check_mark: |
-| 3 | :x: |
+| Version | Supported | Support Status |
+| ------- | ------------------ | -------------- |
+| 4.x | :white_check_mark: | Active Development & Security Updates |
+| < 4.0 | :x: | End of Life (no security updates) |
+## Security Updates
+
+We take security seriously. Security updates are released as soon as possible after a vulnerability is discovered and verified.
## Reporting a Vulnerability
-If you have any vulnerability please report at hi@coollabs.io
+If you discover a security vulnerability, please follow these steps:
+
+1. **DO NOT** disclose the vulnerability publicly.
+2. Send a detailed report to: `hi@coollabs.io`.
+3. Include in your report:
+ - A description of the vulnerability
+ - Steps to reproduce the issue
+ - Potential impact
diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php
index 69365f921..991146b48 100644
--- a/app/Actions/Application/GenerateConfig.php
+++ b/app/Actions/Application/GenerateConfig.php
@@ -11,7 +11,6 @@ class GenerateConfig
public function handle(Application $application, bool $is_json = false)
{
- ray()->clearAll();
return $application->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/StopApplication.php b/app/Actions/Application/StopApplication.php
index 61005845b..642b4ba45 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -10,6 +10,8 @@ class StopApplication
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
try {
@@ -17,7 +19,6 @@ class StopApplication
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- ray('Stopping application: '.$application->name);
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
@@ -36,8 +37,6 @@ class StopApplication
CleanupDocker::dispatch($server, true);
}
} catch (\Exception $e) {
- ray($e->getMessage());
-
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 686b60780..3f76a2e3c 100644
--- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php
+++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
@@ -3,7 +3,6 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
-use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -47,12 +46,7 @@ class PrepareCoolifyTask
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) {
- ray('Dispatching a high priority job');
- dispatch($job)->onQueue('high');
- } else {
- dispatch($job);
- }
+ dispatch($job);
$this->activity->refresh();
return $this->activity;
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index c691f52c0..981b81378 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -9,6 +9,7 @@ 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;
@@ -39,7 +40,6 @@ 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 && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) {
throw new \RuntimeException('Incompatible Activity to run a remote command.');
}
@@ -125,7 +125,7 @@ class RunRemoteProcess
]));
}
} catch (\Throwable $e) {
- ray($e);
+ Log::error('Error calling event: '.$e->getMessage());
}
}
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 6d0063749..42c6e1449 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -24,7 +24,7 @@ class StartClickhouse
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -51,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'",
@@ -97,8 +99,8 @@ class StartClickhouse
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php
index 323c52ff9..e2fa6fc87 100644
--- a/app/Actions/Database/StartDatabase.php
+++ b/app/Actions/Database/StartDatabase.php
@@ -16,6 +16,8 @@ class StartDatabase
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
@@ -23,28 +25,28 @@ class StartDatabase
return 'Server is not functional';
}
switch ($database->getMorphClass()) {
- case 'App\Models\StandalonePostgresql':
+ case \App\Models\StandalonePostgresql::class:
$activity = StartPostgresql::run($database);
break;
- case 'App\Models\StandaloneRedis':
+ case \App\Models\StandaloneRedis::class:
$activity = StartRedis::run($database);
break;
- case 'App\Models\StandaloneMongodb':
+ case \App\Models\StandaloneMongodb::class:
$activity = StartMongodb::run($database);
break;
- case 'App\Models\StandaloneMysql':
+ case \App\Models\StandaloneMysql::class:
$activity = StartMysql::run($database);
break;
- case 'App\Models\StandaloneMariadb':
+ case \App\Models\StandaloneMariadb::class:
$activity = StartMariadb::run($database);
break;
- case 'App\Models\StandaloneKeydb':
+ case \App\Models\StandaloneKeydb::class:
$activity = StartKeydb::run($database);
break;
- case 'App\Models\StandaloneDragonfly':
+ case \App\Models\StandaloneDragonfly::class:
$activity = StartDragonfly::run($database);
break;
- case 'App\Models\StandaloneClickhouse':
+ case \App\Models\StandaloneClickhouse::class:
$activity = StartClickhouse::run($database);
break;
}
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index a514c51b4..3ddf6c036 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -18,6 +18,8 @@ class StartDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
@@ -26,7 +28,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 +36,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 3ee46a2e1..ea235be4e 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -26,7 +26,7 @@ class StartDragonfly
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -48,6 +48,8 @@ class StartDragonfly
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
@@ -94,8 +96,8 @@ class StartDragonfly
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index a11452a68..010bf5884 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -27,7 +27,7 @@ class StartKeydb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -50,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",
@@ -105,8 +107,8 @@ class StartKeydb
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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";
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index a5630f734..2437a013e 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -24,7 +24,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -45,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'],
@@ -99,8 +101,8 @@ class StartMariadb
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 5bff194d5..a33e72c27 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -25,8 +25,12 @@ 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}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -49,6 +53,8 @@ class StartMongodb
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -115,8 +121,8 @@ class StartMongodb
];
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index cc4203580..0b19b3f0c 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -24,7 +24,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -45,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}"],
@@ -99,8 +101,8 @@ class StartMysql
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 2a8e5476c..7faa232c3 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -25,7 +25,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
];
@@ -49,6 +49,8 @@ class StartPostgresql
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -120,8 +122,8 @@ class StartPostgresql
];
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index eeddab924..bacf49f82 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -21,13 +21,11 @@ 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;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -37,6 +35,8 @@ class StartRedis
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
+ $startCommand = $this->buildStartCommand();
+
$docker_compose = [
'services' => [
$container_name => [
@@ -50,6 +50,8 @@ class StartRedis
],
'labels' => [
'coolify.managed' => 'true',
+ 'coolify.type' => 'database',
+ 'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [
'test' => [
@@ -105,12 +107,11 @@ 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 = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_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);
@@ -160,12 +161,26 @@ 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 ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
- $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
+ foreach ($this->database->runtime_environment_variables as $env) {
+ 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");
+ }
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
@@ -173,6 +188,27 @@ class StartRedis
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/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php
index b2092e2ef..9ee794351 100644
--- a/app/Actions/Database/StopDatabaseProxy.php
+++ b/app/Actions/Database/StopDatabaseProxy.php
@@ -2,7 +2,7 @@
namespace App\Actions\Database;
-use App\Events\DatabaseStatusChanged;
+use App\Events\DatabaseProxyStopped;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -18,16 +18,22 @@ class StopDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{
$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();
- DatabaseStatusChanged::dispatch();
+
+ DatabaseProxyStopped::dispatch();
+
}
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index ed563eaae..706356930 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -3,14 +3,10 @@
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;
@@ -19,6 +15,8 @@ class GetContainersStatus
{
use AsAction;
+ public string $jobQueue = 'high';
+
public $applications;
public ?Collection $containers;
@@ -33,7 +31,7 @@ class GetContainersStatus
$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([]);
@@ -49,323 +47,8 @@ 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 {
- // $this->containers = $this->server->getContainersWithSentinel();
- // if ($this->containers->count() === 0) {
- // return;
- // }
- // $databases = $this->server->databases();
- // $services = $this->server->services()->get();
- // $previews = $this->server->previews();
- // $foundApplications = [];
- // $foundApplicationPreviews = [];
- // $foundDatabases = [];
- // $foundServices = [];
-
- // foreach ($this->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 = $this->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 = $this->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 = $this->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);
- // }
- // }
-
- private function old_way()
- {
if ($this->containers === null) {
- ['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
+ ['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
}
if (is_null($this->containers)) {
@@ -425,6 +108,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.
@@ -436,6 +121,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.
@@ -478,7 +165,10 @@ class GetContainersStatus
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
+ } else {
+ $database->update(['last_online_at' => now()]);
}
+
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
@@ -489,7 +179,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", $this->server));
}
}
} else {
@@ -520,6 +210,8 @@ class GetContainersStatus
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
+ } else {
+ $service->update(['last_online_at' => now()]);
}
}
}
@@ -650,32 +342,5 @@ class GetContainersStatus
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
-
- if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
- return;
- }
- $foundProxyContainer = $this->containers->filter(function ($value, $key) {
- if ($this->server->isSwarm()) {
- return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
- } 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 481757162..ea2befd3a 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -6,12 +6,11 @@ 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.
*
@@ -32,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) {
@@ -41,7 +40,7 @@ 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();
@@ -53,7 +52,7 @@ class CreateNewUser implements CreatesNewUsers
} 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
deleted file mode 100644
index 55af1a8c0..000000000
--- a/app/Actions/License/CheckResaleLicense.php
+++ /dev/null
@@ -1,71 +0,0 @@
-update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- // if (!$settings->resale_license) {
- // return;
- // }
- $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", [
- 'license_key' => $settings->resale_license,
- '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,
- ]);
-
- return;
- }
- $data = Http::withHeaders([
- 'Accept' => 'application/json',
- ])->get("$base_url/lemon/activate", [
- 'license_key' => $settings->resale_license,
- 'instance_id' => $instance_id,
- ])->json();
- if (data_get($data, 'activated') === true) {
- ray('Activated license key');
- $settings->update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- if (data_get($data, 'license_key.status') === 'active') {
- throw new \Exception('Invalid license key.');
- }
- throw new \Exception('Cannot activate license key.');
- } catch (\Throwable $e) {
- ray($e);
- $settings->update([
- 'resale_license' => null,
- 'is_resale_license_active' => false,
- ]);
- throw $e;
- }
- }
-}
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 03a0beddf..6c8dd5234 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -4,6 +4,7 @@ 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;
@@ -29,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
@@ -88,7 +89,7 @@ class CheckProxy
$portsToCheck = [];
}
} catch (\Exception $e) {
- ray($e->getMessage());
+ Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index f20c10123..9bc506d9b 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,67 +14,65 @@ class StartProxy
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()) && $force === false) {
- return 'OK';
+ $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 {
+ if (isDev()) {
+ if ($proxyType === ProxyTypes::CADDY->value) {
+ $proxy_path = '/data/coolify/proxy/caddy';
+ }
}
- $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();
+ $caddyfile = 'import /dynamic/*.caddy';
+ $commands = $commands->merge([
+ "mkdir -p $proxy_path/dynamic",
+ "cd $proxy_path",
+ "echo '$caddyfile' > $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 '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));
- }
+ 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 dc6ac12bf..0349ead89 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -9,11 +9,13 @@ class CleanupDocker
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
$settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version');
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = [
diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php
index 0d36e8863..fc04e67a4 100644
--- a/app/Actions/Server/ConfigureCloudflared.php
+++ b/app/Actions/Server/ConfigureCloudflared.php
@@ -40,7 +40,6 @@ class ConfigureCloudflared
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
- ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
@@ -51,7 +50,6 @@ class ConfigureCloudflared
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
-
}
}
}
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..cbcb20368 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.minimum_required_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
index fce862eb0..254c78587 100644
--- a/app/Actions/Server/RunCommand.php
+++ b/app/Actions/Server/RunCommand.php
@@ -12,8 +12,6 @@ class RunCommand
public function handle(Server $server, $command)
{
- $activity = remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
-
- return $activity;
+ return remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
}
}
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
new file mode 100644
index 000000000..75b8501f3
--- /dev/null
+++ b/app/Actions/Server/ServerCheck.php
@@ -0,0 +1,267 @@
+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);
+ }
+ } else {
+ StartLogDrain::dispatch($this->server);
+ }
+ }
+
+ 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", $this->server));
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/StartLogDrain.php
similarity index 95%
rename from app/Actions/Server/InstallLogDrain.php
rename to app/Actions/Server/StartLogDrain.php
index 9b6741211..0d28a0099 100644
--- a/app/Actions/Server/InstallLogDrain.php
+++ b/app/Actions/Server/StartLogDrain.php
@@ -5,20 +5,26 @@ namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
-class InstallLogDrain
+class StartLogDrain
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
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';
}
@@ -151,6 +157,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
@@ -163,7 +171,7 @@ Files:
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
- $base_path = config('coolify.base_config_path');
+ $base_path = config('constants.coolify.base_config_path');
$config_path = $base_path.'/log-drains';
$fluent_bit_config = $config_path.'/fluent-bit.conf';
@@ -202,10 +210,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 b79bc8f67..587ac4a8d 100644
--- a/app/Actions/Server/StartSentinel.php
+++ b/app/Actions/Server/StartSentinel.php
@@ -9,18 +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 ($server->isSwarm() || $server->isBuildServer()) {
+ return;
+ }
if ($restart) {
StopSentinel::run($server);
}
- $metrics_history = $server->settings->metrics_history_days;
- $refresh_rate = $server->settings->metrics_refresh_rate_seconds;
- $token = $server->settings->metrics_token;
+ $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 \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --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, true);
+ '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
index a5bce94a5..96c2466de 100644
--- a/app/Actions/Server/StopLogDrain.php
+++ b/app/Actions/Server/StopLogDrain.php
@@ -12,7 +12,7 @@ class StopLogDrain
public function handle(Server $server)
{
try {
- return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server);
+ return instant_remote_process(['docker rm -f coolify-log-drain'], $server, false);
} catch (\Throwable $e) {
return handleError($e);
}
diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php
index 21ffca3bd..aecb96c87 100644
--- a/app/Actions/Server/StopSentinel.php
+++ b/app/Actions/Server/StopSentinel.php
@@ -12,5 +12,6 @@ class StopSentinel
public function handle(Server $server)
{
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
+ $server->sentinelHeartbeat(isReset: true);
}
}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 30664df26..be9b4062c 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -4,6 +4,7 @@ namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
+use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -18,49 +19,38 @@ class UpdateCoolify
public function handle($manual_update = false)
{
- try {
- $settings = instanceSettings();
- $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);
+ $this->latestVersion = get_latest_version_of_coolify();
+ $this->currentVersion = config('constants.coolify.version');
+ if (! $manual_update) {
+ if (! $settings->is_auto_update_enabled) {
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;
- }
- 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();
- $settings->new_version_available = false;
- $settings->save();
- } 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);
-
- return;
- }
-
- $all_servers = Server::all();
- $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
- foreach ($servers as $server) {
- PullHelperImageJob::dispatch($server);
- }
+ PullHelperImageJob::dispatch($this->server);
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
index d0a4cd6be..55b37a77c 100644
--- a/app/Actions/Server/ValidateServer.php
+++ b/app/Actions/Server/ValidateServer.php
@@ -9,6 +9,8 @@ class ValidateServer
{
use AsAction;
+ public string $jobQueue = 'high';
+
public ?string $uptime = null;
public ?string $error = null;
diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php
index f28e5490e..9b87454da 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -4,6 +4,7 @@ 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
@@ -39,8 +40,8 @@ class DeleteService
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
- if ($result !== 0) {
- ray("Failed to execute: $command");
+ if ($result !== null && $result !== 0) {
+ Log::error('Error deleting volumes: '.$result);
}
}
}
diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php
index 1b6a5c32c..4151ea947 100644
--- a/app/Actions/Service/RestartService.php
+++ b/app/Actions/Service/RestartService.php
@@ -9,6 +9,8 @@ class RestartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
StopService::run($service);
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 06d2e0efb..1dfaf6c49 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -10,9 +10,10 @@ class StartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
- ray('Starting service: '.$service->name);
$service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
@@ -34,8 +35,7 @@ class StartService
$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 5c7bbc2aa..95b08b437 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -10,6 +10,8 @@ class StopService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
@@ -28,8 +30,6 @@ class StopService
}
}
} catch (\Exception $e) {
- ray($e->getMessage());
-
return $e->getMessage();
}
}
diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php
index 6f130626b..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';
@@ -20,9 +20,9 @@ class CleanupDatabase extends Command
}
if (isCloud()) {
// Later on we can increase this to 180 days or dynamically set
- $keep_days = 60;
+ $keep_days = $this->option('keep-days') ?? 60;
} else {
- $keep_days = 60;
+ $keep_days = $this->option('keep-days') ?? 60;
}
echo "Keep days: $keep_days\n";
// Cleanup failed jobs table
@@ -64,6 +64,5 @@ class CleanupDatabase extends Command
if ($this->option('yes')) {
$webhooks->delete();
}
-
}
}
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
index ed0740d34..e16a82be4 100644
--- a/app/Console/Commands/CleanupRedis.php
+++ b/app/Console/Commands/CleanupRedis.php
@@ -13,7 +13,6 @@ class CleanupRedis extends Command
public function handle()
{
- echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
@@ -26,6 +25,5 @@ class CleanupRedis extends Command
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
-
}
}
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index 66c25ec27..def3d5a2c 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -30,14 +30,11 @@ 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();
diff --git a/app/Console/Commands/CleanupUnreachableServers.php b/app/Console/Commands/CleanupUnreachableServers.php
index df0c6b81b..def01b265 100644
--- a/app/Console/Commands/CleanupUnreachableServers.php
+++ b/app/Console/Commands/CleanupUnreachableServers.php
@@ -18,7 +18,6 @@ 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...");
$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
index d220aa00b..9198b003e 100644
--- a/app/Console/Commands/CloudCleanupSubscriptions.php
+++ b/app/Console/Commands/CloudCleanupSubscriptions.php
@@ -19,7 +19,6 @@ class CloudCleanupSubscriptions extends Command
return;
}
- ray()->clearAll();
$this->info('Cleaning up subcriptions teams');
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
@@ -37,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
}
// 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}");
+ $this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
@@ -62,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$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}");
+ $this->info("Skipping team {$team->id}");
} else {
- $this->info("Cancelling subscription for team {$team->id} {$team->name}");
+ $this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
@@ -74,7 +73,6 @@ class CloudCleanupSubscriptions extends Command
}
}
}
-
} catch (\Exception $e) {
$this->error($e->getMessage());
@@ -96,6 +94,5 @@ class CloudCleanupSubscriptions extends Command
]);
}
}
-
}
}
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 20a2667c3..257de0a92 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
+use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
@@ -25,26 +26,38 @@ class Dev extends Command
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']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
+ // Convert YAML to JSON
+ $yaml = file_get_contents('openapi.yaml');
+ $json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
+ file_put_contents('openapi.json', $json);
+ echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()
{
// Generate APP_KEY if not exists
- if (empty(env('APP_KEY'))) {
+ if (empty(config('app.key'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
@@ -63,7 +76,5 @@ class Dev extends Command
} else {
echo "Instance already initialized.\n";
}
- // Set permissions
- Process::run(['chmod', '-R', 'o+rwx', '.']);
}
}
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index 36722564c..33ddf3019 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -2,20 +2,17 @@
namespace App\Console\Commands;
-use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
-use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed;
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;
@@ -65,8 +62,6 @@ class Emails extends Command
'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed',
// 'invitation-link' => 'Invitation Link',
- 'waitlist-invitation-link' => 'Waitlist Invitation Link',
- 'waitlist-confirmation' => 'Waitlist Confirmation',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
],
@@ -121,28 +116,10 @@ class Emails extends Command
$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->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;
}
@@ -206,7 +183,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
- $this->mail = (new BackupSuccess($backup, $db))->toMail();
+ //$this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':
@@ -223,23 +200,6 @@ class Emails extends Command
// $this->mail = (new InvitationLink($user))->toMail();
// $this->sendEmail();
// break;
- case 'waitlist-invitation-link':
- $this->mail = new MailMessage;
- $this->mail->view('emails.waitlist-invitation', [
- 'loginLink' => 'https://coolify.io',
- ]);
- $this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
- $this->sendEmail();
- break;
- case 'waitlist-confirmation':
- $found = Waitlist::where('email', $this->email)->first();
- if ($found) {
- SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
- } else {
- throw new Exception('Waitlist not found');
- }
-
- break;
case 'realusers-before-trial':
$this->mail = new MailMessage;
$this->mail->view('emails.before-trial-conversion');
diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php
index 65a142d6e..d3e35ca5a 100644
--- a/app/Console/Commands/Horizon.php
+++ b/app/Console/Commands/Horizon.php
@@ -12,8 +12,8 @@ class Horizon extends Command
public function handle()
{
- if (config('coolify.is_horizon_enabled')) {
- $this->info('Horizon is enabled. Starting.');
+ if (config('constants.horizon.is_horizon_enabled')) {
+ $this->info('Horizon is enabled on this server.');
$this->call('horizon');
exit(0);
} else {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index ad7bff86d..cc9bee0a5 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -2,15 +2,17 @@
namespace App\Console\Commands;
-use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
+use App\Jobs\CheckHelperImageJob;
use App\Models\ApplicationDeploymentQueue;
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\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
@@ -24,6 +26,8 @@ class Init extends Command
public function handle()
{
+ $this->optimize();
+
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
@@ -32,16 +36,15 @@ class Init extends Command
$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')) {
@@ -53,15 +56,29 @@ class Init extends Command
$this->cleanup_in_progress_application_deployments();
}
$this->call('cleanup:redis');
+
$this->call('cleanup:stucked-resources');
+ try {
+ $this->pullHelperImage();
+ } catch (\Throwable $e) {
+ //
+ }
+
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));
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
+ }
+ }
+
+ if (! isCloud()) {
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
- } else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
@@ -69,8 +86,8 @@ class Init extends Command
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = instanceSettings();
- if (! is_null(env('AUTOUPDATE', null))) {
- if (env('AUTOUPDATE') == true) {
+ if (! is_null(config('constants.coolify.autoupdate', null))) {
+ if (config('constants.coolify.autoupdate') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
@@ -79,17 +96,32 @@ class Init extends Command
}
}
- private function disable_metrics()
+ private function pullHelperImage()
{
- 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);
- }
- }
+ CheckHelperImageJob::dispatch();
+ }
+
+ private function pullTemplatesFromCDN()
+ {
+ $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));
+ }
+ }
+
+ private function optimize()
+ {
+ Artisan::call('optimize:clear');
+ Artisan::call('optimize');
+ }
+
+ 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";
}
}
@@ -120,7 +152,6 @@ class Init extends Command
} catch (\Throwable $e) {
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
-
}
}
@@ -155,7 +186,6 @@ class Init extends Command
}
}
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) {
@@ -166,7 +196,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
- if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -180,7 +210,7 @@ class Init extends Command
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
+ 'database_type' => \App\Models\StandalonePostgresql::class,
'team_id' => 0,
]);
}
@@ -194,19 +224,18 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
- $version = config('version');
+ $version = config('constants.coolify.version');
$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";
+ echo "Do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
- echo "I am alive!\n";
} catch (\Throwable $e) {
- echo "Error in alive: {$e->getMessage()}\n";
+ echo "Error in sending live signal: {$e->getMessage()}\n";
}
}
@@ -219,8 +248,6 @@ class Init extends Command
}
$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();
}
@@ -231,7 +258,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
- if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
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
index e8d73ef47..6cbcb310c 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/OpenApi.php
@@ -15,12 +15,19 @@ class OpenApi extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
- $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->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/Scheduler.php b/app/Console/Commands/Scheduler.php
index 304cb357d..ee64368c3 100644
--- a/app/Console/Commands/Scheduler.php
+++ b/app/Console/Commands/Scheduler.php
@@ -12,8 +12,8 @@ class Scheduler extends Command
public function handle()
{
- if (config('coolify.is_scheduler_enabled')) {
- $this->info('Scheduler is enabled. Starting.');
+ if (config('constants.horizon.is_scheduler_enabled')) {
+ $this->info('Scheduler is enabled on this server.');
$this->call('schedule:work');
exit(0);
} else {
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index 9720e81ac..b45707c5c 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -3,128 +3,85 @@
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, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ $serviceTemplatesJson = collect(array_merge(
+ glob(base_path('templates/compose/*.yaml')),
+ glob(base_path('templates/compose/*.yml'))
+ ))
+ ->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/coolify.png';
- }
- $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/default.webp'),
+ '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 228467f88..df1903828 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -57,7 +57,7 @@ class SyncBunny extends Command
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
+ 'AccessKey' => config('constants.bunny.storage_api_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream',
];
@@ -69,7 +69,7 @@ class SyncBunny extends Command
});
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_API_KEY'),
+ 'AccessKey' => config('constants.bunny.api_key'),
'Accept' => 'application/json',
];
$that->info('Purging: '.$url);
diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php
deleted file mode 100644
index 2e330068c..000000000
--- a/app/Console/Commands/WaitlistInvite.php
+++ /dev/null
@@ -1,114 +0,0 @@
-option('people');
- for ($i = 0; $i < $people; $i++) {
- $this->main();
- }
- }
-
- private function main()
- {
- if ($this->argument('email')) {
- if ($this->option('only-email')) {
- $this->next_patient = User::whereEmail($this->argument('email'))->first();
- $this->password = Str::password();
- $this->next_patient->update([
- 'password' => Hash::make($this->password),
- 'force_password_reset' => true,
- ]);
- } else {
- $this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
- }
- if (! $this->next_patient) {
- $this->error("{$this->argument('email')} not found in the waitlist.");
-
- return;
- }
- } else {
- $this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
- }
- if ($this->next_patient) {
- if ($this->option('only-email')) {
- $this->send_email();
-
- return;
- }
- $this->register_user();
- $this->remove_from_waitlist();
- $this->send_email();
- } else {
- $this->info('No verified user found in the waitlist. 👀');
- }
- }
-
- private function register_user()
- {
- $already_registered = User::whereEmail($this->next_patient->email)->first();
- if (! $already_registered) {
- $this->password = Str::password();
- User::create([
- 'name' => str($this->next_patient->email)->before('@'),
- 'email' => $this->next_patient->email,
- 'password' => Hash::make($this->password),
- 'force_password_reset' => true,
- ]);
- $this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
- } else {
- throw new \Exception('User already registered');
- }
- }
-
- private function remove_from_waitlist()
- {
- $this->next_patient->delete();
- $this->info('User removed from waitlist successfully.');
- }
-
- private function send_email()
- {
- $token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
- $loginLink = route('auth.link', ['token' => $token]);
- $mail = new MailMessage;
- $mail->view('emails.waitlist-invitation', [
- 'loginLink' => $loginLink,
- ]);
- $mail->subject('Congratulations! You are invited to join Coolify Cloud.');
- send_user_an_email($mail, $this->next_patient->email);
- $this->info('Email sent successfully. 📧');
- }
-}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 1430fcdd1..19d22ae21 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -2,142 +2,181 @@
namespace App\Console;
+use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob;
+use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
-use App\Jobs\PullHelperImageJob;
-use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
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 Schedule $scheduleInstance;
+
+ private InstanceSettings $settings;
+
+ private string $updateCheckFrequency;
+
+ private string $instanceTimezone;
protected function schedule(Schedule $schedule): void
{
- $this->all_servers = Server::all();
- $settings = instanceSettings();
+ $this->scheduleInstance = $schedule;
+ $this->allServers = Server::where('ip', '!=', '1.2.3.4');
- $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
+ $this->settings = instanceSettings();
+ $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
+
+ $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
+
+ if (validate_timezone($this->instanceTimezone) === false) {
+ $this->instanceTimezone = config('app.timezone');
+ }
+
+ // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyMinute();
- $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
+ $this->scheduleInstance->command('horizon:snapshot')->everyMinute();
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
+ $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
+
// Server Jobs
- $this->check_scheduled_backups($schedule);
- $this->check_resources($schedule);
- $this->check_scheduled_tasks($schedule);
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->checkResources();
- $schedule->command('telescope:prune')->daily();
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
+
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
- $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
- $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
- $this->schedule_updates($schedule);
+ $this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
+ $this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
+
+ $this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
+
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
+ $this->scheduleUpdates();
// Server Jobs
- $this->check_scheduled_backups($schedule);
- $this->check_resources($schedule);
- $this->pull_images($schedule);
- $this->check_scheduled_tasks($schedule);
+ $this->checkResources();
- $schedule->command('cleanup:database --yes')->daily();
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->pullImages();
+
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
+
+ $this->scheduleInstance->command('cleanup:database --yes')->daily();
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
}
- private function pull_images($schedule)
+ private function pullImages(): void
{
- $settings = instanceSettings();
- $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 ($server->isSentinelEnabled()) {
- $schedule->job(function () use ($server) {
- $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
- $sentinel_found = json_decode($sentinel_found, true);
- $status = data_get($sentinel_found, '0.State.Status', 'exited');
- if ($status !== 'running') {
- PullSentinelImageJob::dispatch($server);
- }
- })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
+ $this->scheduleInstance->job(function () use ($server) {
+ CheckAndStartSentinelJob::dispatch($server);
+ })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- $schedule->job(new PullHelperImageJob)
- ->cron($settings->update_check_frequency)
- ->timezone($settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckHelperImageJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
- private function schedule_updates($schedule)
+ private function scheduleUpdates(): void
{
- $settings = instanceSettings();
-
- $updateCheckFrequency = $settings->update_check_frequency;
- $schedule->job(new CheckForUpdatesJob)
- ->cron($updateCheckFrequency)
- ->timezone($settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckForUpdatesJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
- if ($settings->is_auto_update_enabled) {
- $autoUpdateFrequency = $settings->auto_update_frequency;
- $schedule->job(new UpdateCoolifyJob)
+ if ($this->settings->is_auto_update_enabled) {
+ $autoUpdateFrequency = $this->settings->auto_update_frequency;
+ $this->scheduleInstance->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
- ->timezone($settings->instance_timezone)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
}
- private function check_resources($schedule)
+ private function checkResources(): 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);
} else {
- $servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
+ $servers = $this->allServers->get();
}
+
foreach ($servers as $server) {
- $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
- // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
- $serverTimezone = $server->settings->server_timezone;
+ $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
+
+ // 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
+ if (validate_timezone($serverTimezone) === false) {
+ $serverTimezone = config('app.timezone');
+ }
+ if (isCloud()) {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
+ } else {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
+ }
+ // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
+
+ // Check storage usage every 10 minutes if Sentinel does not activated
+ $this->scheduleInstance->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();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
- $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ }
+
+ // Cleanup multiplexed connections every hour
+ // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
+
+ // Temporary solution until we have better memory management for Sentinel
+ if ($server->isSentinelEnabled()) {
+ $this->scheduleInstance->job(function () use ($server) {
+ $server->restartContainer('coolify-sentinel');
+ })->daily()->onOneServer();
}
}
}
- private function check_scheduled_backups($schedule)
+ private function checkScheduledBackups(): 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;
@@ -145,35 +184,30 @@ class Kernel extends ConsoleKernel
$server = $scheduled_backup->server();
- if (! $server) {
+ if (is_null($server)) {
continue;
}
- $serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
- $schedule->job(new DatabaseBackupJob(
+ $this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup
- ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- private function check_scheduled_tasks($schedule)
+ private function checkScheduledTasks(): 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;
@@ -193,14 +227,13 @@ class Kernel extends ConsoleKernel
if (! $server) {
continue;
}
- $serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
- $schedule->job(new ScheduledTaskJob(
+ $this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task
- ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
diff --git a/app/Enums/Role.php b/app/Enums/Role.php
new file mode 100644
index 000000000..a37a5076c
--- /dev/null
+++ b/app/Enums/Role.php
@@ -0,0 +1,37 @@
+ 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/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php
new file mode 100644
index 000000000..96b35a5ca
--- /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 a94bc2272..913b21bc2 100644
--- a/app/Events/DatabaseStatusChanged.php
+++ b/app/Events/DatabaseStatusChanged.php
@@ -7,27 +7,29 @@ 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 ?string $userId = null;
+ 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)) {
return false;
}
+
$this->userId = $userId;
}
public function broadcastOn(): ?array
{
- if ($this->userId) {
+ if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php
index 27fdc6b5c..57004cf4c 100644
--- a/app/Events/FileStorageChanged.php
+++ b/app/Events/FileStorageChanged.php
@@ -16,7 +16,6 @@ class FileStorageChanged implements ShouldBroadcast
public function __construct($teamId = null)
{
- ray($teamId);
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
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 a86a8b02d..3950022e1 100644
--- a/app/Events/ServiceStatusChanged.php
+++ b/app/Events/ServiceStatusChanged.php
@@ -7,6 +7,7 @@ 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
{
@@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast
public function __construct($userId = null)
{
if (is_null($userId)) {
- $userId = auth()->user()->id ?? null;
+ $userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 63fbfc862..8c89bb07f 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -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/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index 1a2146799..8da476b9e 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -21,17 +21,14 @@ class SshMultiplexingHelper
];
}
- public static function ensureMultiplexedConnection(Server $server)
+ public static function ensureMultiplexedConnection(Server $server): bool
{
if (! self::isMultiplexingEnabled()) {
- return;
+ return false;
}
$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')) {
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
- self::establishNewMultiplexedConnection($server);
+ return self::establishNewMultiplexedConnection($server);
}
+
+ return true;
}
- public static function establishNewMultiplexedConnection(Server $server)
+ public static function establishNewMultiplexedConnection(Server $server): bool
{
$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');
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
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());
+ return false;
}
+
+ return true;
}
public static function removeMuxFile(Server $server)
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
+
+ self::validateSshKey($server->privateKey);
+
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
$ssh_command = "timeout $timeout ssh ";
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -151,16 +149,17 @@ class SshMultiplexingHelper
private static function isMultiplexingEnabled(): bool
{
- return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
+ return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
}
- private static function validateSshKey(string $sshKeyLocation): void
+ private static function validateSshKey(PrivateKey $privateKey): void
{
- $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
+ $keyLocation = $privateKey->getKeyLocation();
+ $checkKeyCommand = "ls $keyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
- throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
+ $privateKey->storeInFileSystem();
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 2a1f846d3..f02c4255d 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -25,26 +25,24 @@ class ApplicationsController extends Controller
{
private function removeSensitiveData($application)
{
- $token = auth()->user()->currentAccessToken();
$application->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($application);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $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',
+ ]);
}
- $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);
}
@@ -70,7 +68,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/Application')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -180,8 +179,10 @@ class ApplicationsController extends Controller
'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,
@@ -284,8 +285,10 @@ class ApplicationsController extends Controller
'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,
@@ -388,8 +391,10 @@ class ApplicationsController extends Controller
'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,
@@ -476,8 +481,10 @@ class ApplicationsController extends Controller
'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,
@@ -561,8 +568,10 @@ class ApplicationsController extends Controller
'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,
@@ -612,8 +621,10 @@ class ApplicationsController extends Controller
'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,
@@ -636,7 +647,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
+ $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();
@@ -676,6 +687,27 @@ class ApplicationsController extends Controller
$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) {
@@ -1213,7 +1245,6 @@ class ApplicationsController extends Controller
}
return response()->json(['message' => 'Invalid type.'], 400);
-
}
#[OA\Get(
@@ -1248,7 +1279,8 @@ class ApplicationsController extends Controller
ref: '#/components/schemas/Application'
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1320,7 +1352,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1446,8 +1479,10 @@ class ApplicationsController extends Controller
'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,
@@ -1462,7 +1497,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1501,7 +1537,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_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',
@@ -1513,6 +1549,7 @@ class ApplicationsController extends Controller
'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);
@@ -1531,6 +1568,25 @@ class ApplicationsController extends Controller
}
}
}
+ 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;
@@ -1551,16 +1607,33 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
- $errors = [];
+ $uuid = $request->uuid;
$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);
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ $domain = trim($domain);
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
+
+ return $domain;
+ });
+ 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);
}
- $request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -1579,11 +1652,16 @@ class ApplicationsController extends Controller
$request->offsetUnset('docker_compose_domains');
}
$instantDeploy = $request->instant_deploy;
+ $isStatic = $request->is_static;
+ $useBuildServer = $request->use_build_server;
- $use_build_server = $request->use_build_server;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
- if (isset($use_build_server)) {
- $application->settings->is_build_server_enabled = $use_build_server;
+ if (isset($isStatic)) {
+ $application->settings->is_static = $isStatic;
$application->settings->save();
}
@@ -1645,7 +1723,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1687,9 +1766,8 @@ class ApplicationsController extends Controller
'standalone_postgresql_id',
'standalone_redis_id',
]);
- $env = $this->removeSensitiveData($env);
- return $env;
+ return $this->removeSensitiveData($env);
});
return response()->json($envs);
@@ -1752,7 +1830,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1864,18 +1943,15 @@ class ApplicationsController extends Controller
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(
@@ -1943,7 +2019,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2124,7 +2201,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2220,14 +2298,12 @@ class ApplicationsController extends Controller
return response()->json([
'uuid' => $env->uuid,
])->setStatusCode(201);
-
}
}
return response()->json([
'message' => 'Something went wrong.',
], 500);
-
}
#[OA\Delete(
@@ -2275,7 +2351,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2367,9 +2444,11 @@ class ApplicationsController extends Controller
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',
@@ -2455,7 +2534,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2529,7 +2609,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
@@ -2575,7 +2656,6 @@ class ApplicationsController extends Controller
'deployment_uuid' => $deployment_uuid->toString(),
],
);
-
}
#[OA\Post(
@@ -2741,7 +2821,6 @@ class ApplicationsController extends Controller
'custom_labels' => 'The custom_labels should be base64 encoded.',
],
], 422);
-
}
}
if ($request->has('domains') && $server->isProxyShouldRun()) {
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 65873f818..917171e5c 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -19,26 +19,23 @@ class DatabasesController extends Controller
{
private function removeSensitiveData($database)
{
- $token = auth()->user()->currentAccessToken();
$database->makeHidden([
'id',
'laravel_through_key',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($database);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $database->makeHidden([
+ 'internal_db_url',
+ 'external_db_url',
+ 'postgres_password',
+ 'dragonfly_password',
+ 'redis_password',
+ 'mongo_initdb_root_password',
+ 'keydb_password',
+ 'clickhouse_admin_password',
+ ]);
}
- $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);
}
@@ -211,8 +208,9 @@ class DatabasesController extends Controller
'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'],
+ 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +239,7 @@ class DatabasesController extends Controller
)]
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'];
+ $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_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -413,12 +411,12 @@ class DatabasesController extends Controller
}
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'];
+ $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_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +441,10 @@ class DatabasesController extends Controller
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'];
+ $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_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -471,7 +470,6 @@ class DatabasesController extends Controller
$request->offsetSet('mysql_conf', $mysqlConf);
}
break;
-
}
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -506,7 +504,6 @@ class DatabasesController extends Controller
return response()->json([
'message' => 'Database updated.',
]);
-
}
#[OA\Post(
@@ -911,6 +908,7 @@ class DatabasesController extends Controller
'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_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1015,7 +1013,7 @@ class DatabasesController extends Controller
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'];
+ $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_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1165,7 +1163,6 @@ class DatabasesController extends Controller
}
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(), [
@@ -1223,9 +1220,10 @@ class DatabasesController extends Controller
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'];
+ $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_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -1459,12 +1457,12 @@ class DatabasesController extends Controller
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'];
+ $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_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1560,7 +1558,8 @@ class DatabasesController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1635,9 +1634,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1711,9 +1712,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1787,9 +1790,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1826,6 +1831,5 @@ class DatabasesController extends Controller
],
200
);
-
}
}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index d1c8f5ea6..73b452f86 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -16,15 +16,12 @@ class DeployController extends Controller
{
private function removeSensitiveData($deployment)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($deployment);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $deployment->makeHidden([
+ 'logs',
+ ]);
}
- $deployment->makeHidden([
- 'logs',
- ]);
-
return serializeApiResponse($deployment);
}
@@ -292,7 +289,7 @@ class DeployController extends Controller
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
- case 'App\Models\Application':
+ case \App\Models\Application::class:
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $resource,
@@ -301,7 +298,7 @@ class DeployController extends Controller
);
$message = "Application {$resource->name} deployment queued.";
break;
- case 'App\Models\Service':
+ case \App\Models\Service::class:
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index 2414b7a42..303e6535d 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -37,7 +37,7 @@ class OtherController extends Controller
)]
public function version(Request $request)
{
- return response(config('version'));
+ return response(config('constants.coolify.version'));
}
#[OA\Get(
@@ -147,7 +147,7 @@ class OtherController extends Controller
public function feedback(Request $request)
{
$content = $request->input('content');
- $webhook_url = config('coolify.feedback_discord_webhook');
+ $webhook_url = config('constants.webhooks.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,
@@ -160,7 +160,7 @@ class OtherController extends Controller
#[OA\Get(
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
- path: '/healthcheck',
+ path: '/health',
operationId: 'healthcheck',
responses: [
new OA\Response(
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index f1958de2c..1d89c82ed 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Project details',
+ description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response(
response: 401,
@@ -356,7 +356,6 @@ class ProjectController extends Controller
'name' => $project->name,
'description' => $project->description,
])->setStatusCode(201);
-
}
#[OA\Delete(
@@ -423,7 +422,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
- if ($project->resource_count() > 0) {
+ if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php
index 1fd5792e0..4180cef9a 100644
--- a/app/Http/Controllers/Api/ResourcesController.php
+++ b/app/Http/Controllers/Api/ResourcesController.php
@@ -53,7 +53,7 @@ class ResourcesController extends Controller
$resources = $resources->flatten();
$resources = $resources->map(function ($resource) {
$payload = $resource->toArray();
- if ($resource->getMorphClass() === 'App\Models\Service') {
+ if ($resource->getMorphClass() === \App\Models\Service::class) {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index bb474aed3..a14b0da20 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -11,13 +11,11 @@ class SecurityController extends Controller
{
private function removeSensitiveData($team)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($team);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $team->makeHidden([
+ 'private_key',
+ ]);
}
- $team->makeHidden([
- 'private_key',
- ]);
return serializeApiResponse($team);
}
@@ -81,15 +79,8 @@ class SecurityController extends Controller
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')
- )
- ),
- ]),
+ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index a49515579..f37040bdd 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
+use App\Actions\Server\DeleteServer;
use App\Actions\Server\ValidateServer;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
@@ -18,25 +19,22 @@ class ServersController extends Controller
{
private function removeSensitiveDataFromSettings($settings)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($settings);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $settings = $settings->makeHidden([
+ 'sentinel_token',
+ ]);
}
- $settings = $settings->makeHidden([
- 'metrics_token',
- ]);
return serializeApiResponse($settings);
}
private function removeSensitiveData($server)
{
- $token = auth()->user()->currentAccessToken();
$server->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($server);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ // Do nothing
}
return serializeApiResponse($server);
@@ -248,7 +246,6 @@ class ServersController extends Controller
return $payload;
});
$server = $this->removeSensitiveData($server);
- ray($server);
return response()->json(serializeApiResponse(data_get($server, 'resources')));
}
@@ -426,6 +423,7 @@ class ServersController extends Controller
'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.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
],
),
),
@@ -461,7 +459,7 @@ class ServersController extends Controller
)]
public function create_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -481,6 +479,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +511,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
@@ -521,6 +528,8 @@ class ServersController extends Controller
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
+ $proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
+
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
@@ -530,7 +539,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
- 'type' => ProxyTypes::TRAEFIK->value,
+ 'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]);
@@ -555,6 +564,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
+ ],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -571,6 +583,7 @@ class ServersController extends Controller
'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.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
],
),
),
@@ -583,8 +596,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/Server')
+ ref: '#/components/schemas/Server'
)
),
]),
@@ -604,7 +616,7 @@ class ServersController extends Controller
)]
public function update_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -624,6 +636,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +657,16 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ $server->changeProxy($request->proxy_type, async: true);
+ } else {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
@@ -654,7 +677,9 @@ class ServersController extends Controller
ValidateServer::dispatch($server);
}
- return response()->json(serializeApiResponse($server))->setStatusCode(201);
+ return response()->json([
+ 'uuid' => $server->uuid,
+ ])->setStatusCode(201);
}
#[OA\Delete(
@@ -726,6 +751,7 @@ class ServersController extends Controller
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.']);
}
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 89418517b..e6b7e9854 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -18,19 +18,16 @@ class ServicesController extends Controller
{
private function removeSensitiveData($service)
{
- $token = auth()->user()->currentAccessToken();
$service->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($service);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $service->makeHidden([
+ 'docker_compose_raw',
+ 'docker_compose',
+ ]);
}
- $service->makeHidden([
- 'docker_compose_raw',
- 'docker_compose',
- ]);
-
return serializeApiResponse($service);
}
@@ -566,9 +563,8 @@ class ServicesController extends Controller
'standalone_postgresql_id',
'standalone_redis_id',
]);
- $env = $this->removeSensitiveData($env);
- return $env;
+ return $this->removeSensitiveData($env);
});
return response()->json($envs);
@@ -1238,6 +1234,5 @@ class ServicesController extends Controller
],
200
);
-
}
}
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index 3f951c6f7..d4b24d8ab 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -10,20 +10,18 @@ class TeamController extends Controller
{
private function removeSensitiveData($team)
{
- $token = auth()->user()->currentAccessToken();
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($team);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $team->makeHidden([
+ 'smtp_username',
+ 'smtp_password',
+ 'resend_api_key',
+ 'telegram_token',
+ ]);
}
- $team->makeHidden([
- 'smtp_username',
- 'smtp_password',
- 'resend_api_key',
- 'telegram_token',
- ]);
return serializeApiResponse($team);
}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 38d9e2272..522683efa 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -42,15 +42,13 @@ class Controller extends BaseController
public function email_verify(EmailVerificationRequest $request)
{
$request->fulfill();
- $name = request()->user()?->name;
- // send_internal_notification("User {$name} verified their email address.");
return redirect(RouteServiceProvider::HOME);
}
public function forgot_password(Request $request)
{
- if (is_transactional_emails_active()) {
+ if (is_transactional_emails_enabled()) {
$arrayOfRequest = $request->only(Fortify::email());
$request->merge([
'email' => Str::lower($arrayOfRequest['email']),
@@ -110,59 +108,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 630d01045..3a3f18c9c 100644
--- a/app/Http/Controllers/OauthController.php
+++ b/app/Http/Controllers/OauthController.php
@@ -35,8 +35,6 @@ 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([__($errorCode)]);
diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php
index 21fdd2ef8..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;
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index ef85d59e3..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,13 +93,11 @@ 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;
queue_application_deployment(
application: $application,
@@ -126,7 +121,6 @@ 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;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
@@ -171,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();
@@ -191,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 e042b74c9..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,7 +115,6 @@ 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;
queue_application_deployment(
application: $application,
@@ -182,7 +174,6 @@ class Gitea extends Controller
'pull_request_html_url' => $pull_request_html_url,
]);
}
-
}
queue_application_deployment(
application: $application,
@@ -228,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 5f3ba933b..ac1d4ded2 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,7 +121,6 @@ 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;
queue_application_deployment(
application: $application,
@@ -232,12 +225,9 @@ class Github extends Controller
}
}
}
- ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
@@ -249,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 = [
@@ -313,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');
@@ -322,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.');
@@ -356,7 +341,6 @@ 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;
queue_application_deployment(
application: $application,
@@ -460,8 +444,6 @@ class Github extends Controller
return response($return_payloads);
} catch (Exception $e) {
- ray($e->getMessage());
-
return handleError($e);
}
}
@@ -481,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
- 'name' => $slug,
+ 'name' => "github-app-{$slug}",
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,
@@ -505,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 ec7f51a0d..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,7 +141,6 @@ 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;
queue_application_deployment(
application: $application,
@@ -171,7 +175,6 @@ class Gitlab extends Controller
'application_uuid' => $application->uuid,
'application_name' => $application->name,
]);
- ray('Deployments disabled for '.$application->name);
}
}
if ($x_gitlab_event === 'merge_request') {
@@ -207,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',
@@ -219,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();
@@ -253,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 164322586..83ba16699 100644
--- a/app/Http/Controllers/Webhook/Stripe.php
+++ b/app/Http/Controllers/Webhook/Stripe.php
@@ -3,26 +3,27 @@
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\Jobs\StripeProcessJob;
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
{
+ protected $webhook;
+
public function events(Request $request)
{
try {
+ $webhookSecret = config('subscription.stripe_webhook_secret');
+ $signature = $request->header('Stripe-Signature');
+ $event = \Stripe\Webhook::constructEvent(
+ $request->getContent(),
+ $signature,
+ $webhookSecret
+ );
if (app()->isDownForMaintenance()) {
- ray('Maintenance mode is on');
$epoch = now()->valueOf();
$data = [
'attributes' => $request->attributes->all(),
@@ -37,265 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
- return;
+ return response('Webhook received. Cool cool cool cool cool.', 200);
}
- $webhookSecret = config('subscription.stripe_webhook_secret');
- $signature = $request->header('Stripe-Signature');
- $excludedPlans = config('subscription.stripe_excluded_plans');
- $event = \Stripe\Webhook::constructEvent(
- $request->getContent(),
- $signature,
- $webhookSecret
- );
- $webhook = Webhook::create([
+ $this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
- $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) {
- Sleep::for(5)->seconds();
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- }
- if (! $subscription) {
- Sleep::for(5)->seconds();
- $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}");
- break;
- case 'checkout.session.completed':
- $clientReferenceId = data_get($data, 'client_reference_id');
- if (is_null($clientReferenceId)) {
- send_internal_notification('Checkout session completed without client reference id.');
- break;
- }
- $userId = Str::before($clientReferenceId, ':');
- $teamId = Str::after($clientReferenceId, ':');
- $subscriptionId = data_get($data, 'subscription');
- $customerId = data_get($data, 'customer');
- $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}, subscriptionid: {$subscriptionId}.");
- throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- // 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);
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- }
- break;
- case 'invoice.paid':
- $customerId = data_get($data, 'customer');
- $planId = data_get($data, 'lines.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // 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();
- }
- $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);
+ StripeProcessJob::dispatch($event);
- 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);
-
- return response('No team found in Coolify.');
- }
- if (! $subscription->stripe_invoice_paid) {
- SubscriptionInvoiceFailedJob::dispatch($team);
- // send_internal_notification('Invoice payment failed: '.$customerId);
- } else {
- // 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);
-
- 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);
-
- return;
- }
- send_internal_notification('Subscription payment failed for customer: '.$customerId);
- break;
- case 'customer.subscription.updated':
- $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.');
- 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);
- }
- $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);
- }
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->update([
- 'custom_server_limit' => $quantity,
- ]);
- }
- ServerLimitCheckJob::dispatch($team);
- }
- $subscription->update([
- 'stripe_feedback' => $feedback,
- 'stripe_comment' => $comment,
- 'stripe_plan_id' => $planId,
- 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
- ]);
- if ($status === 'paused' || $status === 'incomplete_expired') {
- $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':
- // End subscription
- $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' => false,
- ]);
- // 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);
- break;
- default:
- // Unhandled event type
- }
+ return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
- if ($type !== 'payment_intent.payment_failed') {
- send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
- }
- $webhook->update([
+ $this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php
deleted file mode 100644
index ea635836c..000000000
--- a/app/Http/Controllers/Webhook/Waitlist.php
+++ /dev/null
@@ -1,66 +0,0 @@
-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) {
- if (! $found->verified) {
- if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
- $found->verified = true;
- $found->save();
- send_internal_notification('Waitlist confirmed: '.$email);
-
- return 'Thank you for confirming your email address. We will notify you when you are next in line.';
- } else {
- $found->delete();
- send_internal_notification('Waitlist expired: '.$email);
-
- return 'Your confirmation code has expired. Please sign up again.';
- }
- }
- }
-
- return redirect()->route('dashboard');
- } catch (Exception $e) {
- send_internal_notification('Waitlist confirmation failed: '.$e->getMessage());
- ray($e->getMessage());
-
- return redirect()->route('dashboard');
- }
- }
-
- public function cancel(Request $request)
- {
- $email = request()->get('email');
- $confirmation_code = request()->get('confirmation_code');
- try {
- $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
- if ($found && ! $found->verified) {
- $found->delete();
- send_internal_notification('Waitlist cancelled: '.$email);
-
- return 'Your email address has been removed from the waitlist.';
- }
-
- 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 5f1731071..a1ce20295 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -69,5 +69,7 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
+ 'api.ability' => \App\Http\Middleware\ApiAbility::class,
+ 'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class,
];
}
diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php
new file mode 100644
index 000000000..324eeebaa
--- /dev/null
+++ b/app/Http/Middleware/ApiAbility.php
@@ -0,0 +1,27 @@
+user()->tokenCan('root')) {
+ return $next($request);
+ }
+
+ return parent::handle($request, $next, ...$abilities);
+ } catch (\Illuminate\Auth\AuthenticationException $e) {
+ return response()->json([
+ 'message' => 'Unauthenticated.',
+ ], 401);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'message' => 'Missing required permissions: '.implode(', ', $abilities),
+ ], 403);
+ }
+ }
+}
diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php
index 471e6d602..dc6be5da3 100644
--- a/app/Http/Middleware/ApiAllowed.php
+++ b/app/Http/Middleware/ApiAllowed.php
@@ -10,7 +10,6 @@ class ApiAllowed
{
public function handle(Request $request, Closure $next): Response
{
- ray()->clearAll();
if (isCloud()) {
return $next($request);
}
diff --git a/app/Http/Middleware/ApiSensitiveData.php b/app/Http/Middleware/ApiSensitiveData.php
new file mode 100644
index 000000000..49584ddb3
--- /dev/null
+++ b/app/Http/Middleware/ApiSensitiveData.php
@@ -0,0 +1,21 @@
+user()->currentAccessToken();
+
+ // Allow access to sensitive data if token has root or read:sensitive permission
+ $request->attributes->add([
+ 'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
+ ]);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php
deleted file mode 100644
index bd6cd1f8a..000000000
--- a/app/Http/Middleware/IgnoreReadOnlyApiToken.php
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 8ff1fa0e5..000000000
--- a/app/Http/Middleware/OnlyRootApiToken.php
+++ /dev/null
@@ -1,25 +0,0 @@
-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 9ae383a9f..6b677fa0e 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
+ private bool $disableBuildCache = false;
+
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(int $application_deployment_queue_id)
{
+ $this->onQueue('high');
+
$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');
@@ -176,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
+ if ($this->disableBuildCache) {
+ $this->force_rebuild = true;
+ }
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -208,7 +216,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
}
}
- ray('New container name: ', $this->container_name)->green();
$this->saved_outputs = collect();
@@ -226,12 +233,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.');
@@ -298,7 +310,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 {
@@ -346,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
- GetContainersStatus::dispatch($this->server)->onQueue('high');
- // dispatch(new ContainerStatusJob($this->server));
+ GetContainersStatus::dispatch($this->server);
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
@@ -389,7 +399,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();
@@ -460,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$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']);
+ $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
@@ -712,38 +721,26 @@ 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('----------------------------------------');
@@ -775,7 +772,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($forceFail) {
throw new RuntimeException($e->getMessage(), 69420);
}
- ray($e);
}
}
@@ -1334,7 +1330,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
@@ -1386,8 +1382,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) {
@@ -1854,7 +1848,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
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) {
if (! $this->application->settings->custom_internal_name) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
@@ -1988,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
+ if ($this->disableBuildCache) {
+ $this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
+ }
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {
@@ -2008,22 +2005,11 @@ 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);
@@ -2086,23 +2072,11 @@ 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);
@@ -2449,7 +2423,6 @@ 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
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php
index 6120d1cba..ef8e6efb6 100755
--- a/app/Jobs/ApplicationPullRequestUpdateJob.php
+++ b/app/Jobs/ApplicationPullRequestUpdateJob.php
@@ -25,14 +25,14 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
try {
if ($this->application->is_public_repository()) {
- ray('Public repository. Skipping comment update.');
-
return;
}
if ($this->status === ProcessStatus::CLOSED) {
@@ -53,16 +53,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;
}
}
@@ -73,7 +69,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/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
index f2348118a..1d3a345e1 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
- $current_version = config('version');
+ $current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
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/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php
deleted file mode 100644
index b55ae9967..000000000
--- a/app/Jobs/CheckResaleLicenseJob.php
+++ /dev/null
@@ -1,29 +0,0 @@
-getMessage());
- ray($e);
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index b8ca8b7ed..f185ab781 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -20,18 +20,15 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
public function handle(): void
{
try {
- ray('Cleaning up helper containers on '.$this->server->name);
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {
- ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}
}
} 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 d9de3f6fe..84f14ed02 100644
--- a/app/Jobs/CleanupInstanceStuffsJob.php
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -3,14 +3,15 @@
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
{
@@ -18,36 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
public function __construct() {}
- // public function uniqueId(): string
- // {
- // return $this->container_name;
- // }
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
+ }
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/CoolifyTask.php b/app/Jobs/CoolifyTask.php
index c3692c30b..49a5ba8dd 100755
--- a/app/Jobs/CoolifyTask.php
+++ b/app/Jobs/CoolifyTask.php
@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
public bool $ignore_errors,
public $call_event_on_finish,
public $call_event_data,
- ) {}
+ ) {
+
+ $this->onQueue('high');
+ }
/**
* Execute the job.
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 769739d5e..06aec5e49 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -60,19 +60,22 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
+ $this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
{
try {
+ $databasesToBackup = null;
+
$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') {
+ 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;
@@ -92,11 +95,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$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();
@@ -131,7 +132,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
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;
@@ -200,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
-
- if (is_null($databasesToBackup)) {
+ if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -222,7 +221,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);
@@ -244,7 +242,6 @@ 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';
@@ -309,7 +306,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
- $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
+
+ $this->team->notify(new BackupSuccess($this->backup, $this->database, $database));
+
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
@@ -324,12 +323,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null,
]);
}
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
if ($this->team) {
@@ -377,10 +374,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;
}
}
@@ -400,16 +395,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
$commands[] = $backupCommand;
- ray($commands);
$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;
}
}
@@ -428,10 +420,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;
}
}
@@ -445,16 +435,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
}
- ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
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;
}
}
@@ -498,14 +485,12 @@ 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;
}
- $this->ensureHelperImageAvailable();
-
$fullImageName = $this->getFullImageName();
if (isDev()) {
@@ -538,39 +523,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function ensureHelperImageAvailable(): void
- {
- $fullImageName = $this->getFullImageName();
-
- $imageExists = $this->checkImageExists($fullImageName);
-
- if (! $imageExists) {
- $this->pullHelperImage($fullImageName);
- }
- }
-
- private function checkImageExists(string $fullImageName): bool
- {
- $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
-
- return trim($result) === 'exists';
- }
-
- private function pullHelperImage(string $fullImageName): void
- {
- try {
- instant_remote_process(["docker pull {$fullImageName}"], $this->server);
- } catch (\Exception $e) {
- $errorMessage = 'Failed to pull helper image: '.$e->getMessage();
- $this->add_to_backup_output($errorMessage);
- throw new \RuntimeException($errorMessage);
- }
- }
-
private function getFullImageName(): string
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index 2442d5b06..8b9228e5f 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public bool $deleteVolumes = true,
public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
- send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 900bae99c..103c137b9 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -4,14 +4,15 @@ namespace App\Jobs;
use App\Actions\Server\CleanupDocker;
use App\Models\Server;
-use App\Notifications\Server\DockerCleanup;
+use App\Notifications\Server\DockerCleanupFailed;
+use App\Notifications\Server\DockerCleanupSuccess;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Log;
class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ }
+
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
@@ -32,35 +38,36 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
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();
- if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
- Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
+
+ if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
CleanupDocker::run(server: $this->server);
+ $usageAfter = $this->server->getDiskUsage();
+ $this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
return;
}
+
+ if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
+ CleanupDocker::run(server: $this->server);
+ $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.'));
+ }
+
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.'));
- Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name);
+ $diskSaved = $this->usageBefore - $usageAfter;
+
+ if ($diskSaved > 0) {
+ $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
} else {
- Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name);
+ $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'));
}
} else {
- Log::info('No need to clean up '.$this->server->name);
+ $this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name));
}
} catch (\Throwable $e) {
- CleanupDocker::run(server: $this->server);
- Log::error('DockerCleanupJob failed: '.$e->getMessage());
+ $this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
throw $e;
}
}
diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php
index 9c0a2b55b..d483fe4c2 100644
--- a/app/Jobs/GithubAppPermissionJob.php
+++ b/app/Jobs/GithubAppPermissionJob.php
@@ -42,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/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index 4b208fc31..b92886d38 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Http;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -17,29 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function __construct() {}
+ public function __construct(public Server $server)
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
- try {
- $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
- if ($response->successful()) {
- $versions = $response->json();
- $settings = instanceSettings();
- $latest_version = data_get($versions, 'coolify.helper.version');
- $current_version = $settings->helper_version;
- if (version_compare($latest_version, $current_version, '>')) {
- // New version available
- // $helperImage = config('coolify.helper_image');
- // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
- $settings->update(['helper_version' => $latest_version]);
- }
- }
-
- } catch (\Throwable $e) {
- send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
- ray($e->getMessage());
- throw $e;
- }
+ $helperImage = config('constants.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 32f84e6d5..000000000
--- a/app/Jobs/PullSentinelImageJob.php
+++ /dev/null
@@ -1,47 +0,0 @@
-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 72c971033..45c536e06 100644
--- a/app/Jobs/PullTemplatesFromCDN.php
+++ b/app/Jobs/PullTemplatesFromCDN.php
@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10;
- public function __construct() {}
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
@@ -25,7 +28,6 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
if (isDev() || isCloud()) {
return;
}
- ray('PullTemplatesAndVersions service-templates');
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
@@ -35,7 +37,6 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
}
} 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 79ebad7a8..000000000
--- a/app/Jobs/PullVersionsFromCDN.php
+++ /dev/null
@@ -1,39 +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..24f8d1e6b
--- /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);
+ }
+ }
+}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 6850ae98a..90a10f3e9 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;
@@ -9,6 +10,7 @@ use App\Models\Server;
use App\Models\Service;
use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed;
+use App\Notifications\ScheduledTask\TaskSuccess;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -19,7 +21,7 @@ class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public ?Team $team = null;
+ public Team $team;
public Server $server;
@@ -39,6 +41,8 @@ class ScheduledTaskJob implements ShouldQueue
public function __construct($task)
{
+ $this->onQueue('high');
+
$this->task = $task;
if ($service = $task->service()->first()) {
$this->resource = $service;
@@ -47,20 +51,16 @@ 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();
}
private function getServerTimezone(): string
{
if ($this->resource instanceof Application) {
- $timezone = $this->resource->destination->server->settings->server_timezone;
-
- return $timezone;
+ return $this->resource->destination->server->settings->server_timezone;
} elseif ($this->resource instanceof Service) {
- $timezone = $this->resource->server->settings->server_timezone;
-
- return $timezone;
+ return $this->resource->server->settings->server_timezone;
}
return 'UTC';
@@ -68,7 +68,6 @@ class ScheduledTaskJob implements ShouldQueue
public function handle(): void
{
-
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -76,14 +75,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');
@@ -113,6 +112,8 @@ class ScheduledTaskJob implements ShouldQueue
'message' => $this->task_output,
]);
+ $this->team?->notify(new TaskSuccess($this->task, $this->task_output));
+
return;
}
}
@@ -127,9 +128,9 @@ 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
deleted file mode 100755
index 070598e71..000000000
--- a/app/Jobs/SendConfirmationForWaitlistJob.php
+++ /dev/null
@@ -1,38 +0,0 @@
-email.'&confirmation_code='.$this->uuid;
- $cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid;
- $mail->view('emails.waitlist-confirmation',
- [
- 'confirmation_url' => $confirmation_url,
- 'cancel_url' => $cancel_url,
- ]);
- $mail->subject('You are on the waitlist!');
- 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 f38cf823c..99aeaeea2 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,18 +30,17 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public int $maxExceptions = 5;
public function __construct(
- public string $text,
+ public DiscordMessage $message,
public string $webhookUrl
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* 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/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php
new file mode 100644
index 000000000..470002d23
--- /dev/null
+++ b/app/Jobs/SendMessageToSlackJob.php
@@ -0,0 +1,59 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ Http::post($this->webhookUrl, [
+ 'blocks' => [
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => 'Coolify Notification',
+ ],
+ ],
+ ],
+ 'attachments' => [
+ [
+ 'color' => $this->message->color,
+ 'blocks' => [
+ [
+ 'type' => 'header',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => $this->message->title,
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $this->message->description,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index bf52b782f..85f4fc934 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token,
public string $chatId,
public ?string $topicId = null,
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* Execute the job.
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
}
$response = Http::post($url, $payload);
if ($response->failed()) {
- throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
+ throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
}
}
}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 39d4aa0c0..49d8dfe08 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -2,22 +2,19 @@
namespace App\Jobs;
-use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
-use App\Actions\Server\InstallLogDrain;
-use App\Models\ApplicationPreview;
+use App\Actions\Server\StartLogDrain;
use App\Models\Server;
-use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Arr;
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -29,90 +26,69 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public $containers;
- public $applications;
-
- public $databases;
-
- public $services;
-
- public $previews;
-
- public function backoff(): int
+ public function middleware(): array
{
- return isDev() ? 1 : 3;
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server)
+ {
+ if (isDev()) {
+ $this->handle();
+ }
+ }
public function handle()
{
try {
- $this->applications = $this->server->applications();
- $this->databases = $this->server->databases();
- $this->services = $this->server->services()->get();
- $this->previews = $this->server->previews();
-
- $up = $this->serverStatus();
- if (! $up) {
- ray('Server is not reachable.');
-
- return 'Server is not reachable.';
+ if ($this->server->serverStatus() === false) {
+ return 'Server is not reachable or not ready.';
}
- if (! $this->server->isFunctional()) {
- ray('Server is not ready.');
- return 'Server is 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) {
- ray($e->getMessage());
-
return handleError($e);
}
-
- }
-
- private function serverStatus()
- {
- ['uptime' => $uptime] = $this->server->validateConnection(false);
- if ($uptime) {
- if ($this->server->unreachable_notification_sent === true) {
- $this->server->update(['unreachable_notification_sent' => false]);
- }
- } else {
- // $this->server->team?->notify(new Unreachable($this->server));
- foreach ($this->applications as $application) {
- $application->update(['status' => 'exited']);
- }
- foreach ($this->databases as $database) {
- $database->update(['status' => 'exited']);
- }
- foreach ($this->services 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']);
- }
- }
-
- return false;
- }
-
- return true;
-
}
private function checkLogDrainContainer()
@@ -123,295 +99,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') {
- InstallLogDrain::dispatch($this->server);
+ StartLogDrain::dispatch($this->server);
}
} else {
- InstallLogDrain::dispatch($this->server);
- }
- }
-
- private function containerStatus()
- {
-
- $foundApplications = [];
- $foundApplicationPreviews = [];
- $foundDatabases = [];
- $foundServices = [];
-
- foreach ($this->containers as $container) {
- if ($this->server->isSwarm()) {
- $labels = data_get($container, 'Spec.Labels');
- $uuid = data_get($labels, 'coolify.name');
- } else {
- $labels = data_get($container, 'Config.Labels');
- }
- $containerStatus = data_get($container, 'State.Status');
- $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
- $containerStatus = "$containerStatus ($containerHealth)";
- $labels = Arr::undot(format_docker_labels_to_json($labels));
- $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 = data_get($service_db, 'service.uuid');
- if ($uuid) {
- $isPublic = data_get($service_db, 'is_public');
- if ($isPublic) {
- $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
- 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_db);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
- }
- }
- }
- }
- }
- } else {
- $database = $this->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 = $this->containers->filter(function ($value, $key) use ($uuid) {
- 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));
- }
- }
- } 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 = $this->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 ($this->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 = $this->previews->pluck('id')->diff($foundApplicationPreviews);
- foreach ($notRunningApplicationPreviews as $previewId) {
- $preview = $this->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 = $this->databases->pluck('id')->diff($foundDatabases);
- foreach ($notRunningDatabases as $database) {
- $database = $this->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 = $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) {
- 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);
+ StartLogDrain::dispatch($this->server);
}
}
}
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 769dfc004..58455df2f 100644
--- a/app/Jobs/ServerFilesFromServerJob.php
+++ b/app/Jobs/ServerFilesFromServerJob.php
@@ -16,7 +16,10 @@ 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)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 1f09d5a3b..aa82c6dad 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -30,11 +30,8 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
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) {
@@ -51,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 fcc33c859..000000000
--- a/app/Jobs/ServerStatusJob.php
+++ /dev/null
@@ -1,60 +0,0 @@
-server->isServerReady($this->tries)) {
- throw new \RuntimeException('Server is not ready.');
- }
- try {
- if ($this->server->isFunctional()) {
- $this->remove_unnecessary_coolify_yaml();
- if ($this->server->isSentinelEnabled()) {
- $this->server->checkSentinel();
- }
- }
- } catch (\Throwable $e) {
- // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
- 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);
- }
- }
-}
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
index 376cb8532..9a8d86be1 100644
--- a/app/Jobs/ServerStorageCheckJob.php
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -3,12 +3,14 @@
namespace App\Jobs;
use App\Models\Server;
+use App\Notifications\Server\HighDiskUsage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\RateLimiter;
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -18,42 +20,46 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 60;
- public $containers;
-
- public $applications;
-
- public $databases;
-
- public $services;
-
- public $previews;
-
public function backoff(): int
{
return isDev() ? 1 : 3;
}
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server, public int|string|null $percentage = null) {}
public function handle()
{
try {
- if (! $this->server->isFunctional()) {
- ray('Server is not ready.');
-
- return 'Server is not ready.';
- }
- $team = $this->server->team;
- $percentage = $this->server->storageCheck();
- if ($percentage > 1) {
- ray('Server storage is at '.$percentage.'%');
+ if ($this->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) {
- ray($e->getMessage());
-
return handleError($e);
}
-
}
}
diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php
index 526cd5375..17a293f94 100644
--- a/app/Jobs/ServerStorageSaveJob.php
+++ b/app/Jobs/ServerStorageSaveJob.php
@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public LocalFileVolume $localFileVolume) {}
+ public function __construct(public LocalFileVolume $localFileVolume)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php
new file mode 100644
index 000000000..00c9b6d18
--- /dev/null
+++ b/app/Jobs/StripeProcessJob.php
@@ -0,0 +1,246 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ try {
+ $excludedPlans = config('subscription.stripe_excluded_plans');
+
+ $type = data_get($this->event, 'type');
+ $this->type = $type;
+ $data = data_get($this->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}");
+ throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ }
+ break;
+ case 'checkout.session.completed':
+ $clientReferenceId = data_get($data, 'client_reference_id');
+ if (is_null($clientReferenceId)) {
+ send_internal_notification('Checkout session completed without client reference id.');
+ break;
+ }
+ $userId = Str::before($clientReferenceId, ':');
+ $teamId = Str::after($clientReferenceId, ':');
+ $subscriptionId = data_get($data, 'subscription');
+ $customerId = data_get($data, 'customer');
+ $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}, subscriptionid: {$subscriptionId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ 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);
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ }
+ break;
+ case 'invoice.paid':
+ $customerId = data_get($data, 'customer');
+ $planId = data_get($data, 'lines.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscription->update([
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ 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);
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ $team = data_get($subscription, 'team');
+ if (! $team) {
+ send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
+ }
+ if (! $subscription->stripe_invoice_paid) {
+ SubscriptionInvoiceFailedJob::dispatch($team);
+ send_internal_notification('Invoice payment failed: '.$customerId);
+ } else {
+ 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);
+ throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
+ }
+ if ($subscription->stripe_invoice_paid) {
+ 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) {
+ throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
+ }
+ throw new \RuntimeException('No team id or user id found');
+ }
+ $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}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification("Subscription already exists for team: {$teamId}");
+ throw new \RuntimeException("Subscription already exists for team: {$teamId}");
+ } else {
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ 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.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ if ($status === 'incomplete_expired') {
+ send_internal_notification('Subscription incomplete expired');
+ throw new \RuntimeException('Subscription incomplete expired');
+ }
+ if ($teamId) {
+ $subscription = Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ } else {
+ send_internal_notification('No subscription and team id found');
+ throw new \RuntimeException('No subscription and team id found');
+ }
+ }
+ $cancelAtPeriodEnd = data_get($data, '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('dynamic')) {
+ $quantity = data_get($data, 'items.data.0.quantity', 2);
+ $team = data_get($subscription, 'team');
+ if ($team) {
+ $team->update([
+ 'custom_server_limit' => $quantity,
+ ]);
+ }
+ ServerLimitCheckJob::dispatch($team);
+ }
+ $subscription->update([
+ 'stripe_feedback' => $feedback,
+ 'stripe_comment' => $comment,
+ 'stripe_plan_id' => $planId,
+ 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
+ ]);
+ if ($status === 'paused' || $status === 'incomplete_expired') {
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ if ($feedback) {
+ $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
+ if ($comment) {
+ $reason .= ' with comment: \''.$comment."'";
+ }
+ }
+ break;
+ case 'customer.subscription.deleted':
+ // End subscription
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
+ $team = data_get($subscription, 'team');
+ $team?->subscriptionEnded();
+ break;
+ default:
+ throw new \RuntimeException("Unhandled event type: {$type}");
+ }
+ } catch (\Exception $e) {
+ send_internal_notification('StripeProcessJob error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php
index b4ef7baa0..dc511f445 100755
--- a/app/Jobs/SubscriptionInvoiceFailedJob.php
+++ b/app/Jobs/SubscriptionInvoiceFailedJob.php
@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(protected Team $team) {}
+ public function __construct(protected Team $team)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
@@ -27,14 +30,12 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
]);
$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 8635b439c..000000000
--- a/app/Jobs/SubscriptionTrialEndedJob.php
+++ /dev/null
@@ -1,44 +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 244624749..000000000
--- a/app/Jobs/SubscriptionTrialEndsSoonJob.php
+++ /dev/null
@@ -1,44 +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
index 2cc705e4a..f0e43cbc0 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 600;
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
+
public function handle(): void
{
try {
@@ -41,7 +46,6 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
$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 c7cd1bcde..6c3ab83d8 100644
--- a/app/Listeners/MaintenanceModeDisabledNotification.php
+++ b/app/Listeners/MaintenanceModeDisabledNotification.php
@@ -13,7 +13,6 @@ class MaintenanceModeDisabledNotification
public function handle(EventsMaintenanceModeDisabled $event): void
{
- ray('Maintenance mode disabled!');
$files = Storage::disk('webhooks-during-maintenance')->files();
$files = collect($files);
$files = $files->sort();
@@ -41,7 +40,6 @@ class MaintenanceModeDisabledNotification
$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 d0541b162..9045b1e5c 100644
--- a/app/Listeners/ProxyStartedNotification.php
+++ b/app/Listeners/ProxyStartedNotification.php
@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
- $this->server->setupDefault404Redirect();
+ $this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
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 52d4674ee..eadabba7c 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -66,15 +66,17 @@ class Index extends Component
public bool $serverReachable = true;
+ public ?string $minDockerVersion = null;
+
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard');
}
+
+ $this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
- $this->remoteServerPort = $this->remoteServerPort;
- $this->remoteServerUser = $this->remoteServerUser;
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -87,26 +89,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()
@@ -190,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
- // Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
- // $proxyTypeSet = $this->createdServer->proxy->type;
- // if (!$proxyTypeSet) {
- // $this->currentState = 'select-proxy';
- // return;
- // }
$this->getProjects();
}
@@ -207,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
- $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
+ $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index d18a7689e..69ba19e40 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -16,28 +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()
{
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',
diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php
deleted file mode 100644
index 87ae83931..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('destination.all');
- } 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 4fc938df8..337f1d067 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->servers->first();
+ $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;
- }
- 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/Dev/Compose.php b/app/Livewire/Dev/Compose.php
deleted file mode 100644
index a5cd53fc2..000000000
--- a/app/Livewire/Dev/Compose.php
+++ /dev/null
@@ -1,37 +0,0 @@
-services = get_service_templates();
- }
-
- public function setService(string $selected)
- {
- $this->base64 = data_get($this->services, $selected.'.compose');
- if ($this->base64) {
- $this->compose = base64_decode($this->base64);
- }
- }
-
- public function updatedCompose($value)
- {
- $this->base64 = base64_encode($value);
- }
-
- public function render()
- {
- return view('livewire.dev.compose');
- }
-}
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 934e81661..f51527fbe 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -5,55 +5,39 @@ namespace App\Livewire;
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}";
+ $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();
$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.'`',
]);
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index 988add7c8..e97cceb0d 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -2,6 +2,7 @@
namespace App\Livewire;
+use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@@ -18,17 +19,19 @@ class NavbarDeleteTeam extends Component
public function delete($password)
{
- if (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ 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;
+ return;
+ }
}
$currentTeam = currentTeam();
$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/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 65c202b7d..57007813e 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -2,62 +2,163 @@
namespace App\Livewire\Notifications;
+use App\Models\DiscordNotificationSettings;
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',
- ];
+ public DiscordNotificationSettings $settings;
- protected $validationAttributes = [
- 'team.discord_webhook_url' => 'Discord Webhook',
- ];
+ #[Validate(['boolean'])]
+ public bool $discordEnabled = false;
+
+ #[Validate(['url', 'nullable'])]
+ public ?string $discordWebhookUrl = null;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentSuccessDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentFailureDiscordNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $statusChangeDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupSuccessDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupFailureDiscordNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskSuccessDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskFailureDiscordNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupSuccessDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupFailureDiscordNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverDiskUsageDiscordNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverReachableDiscordNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $serverUnreachableDiscordNotifications = true;
public function mount()
{
- $this->team = auth()->user()->currentTeam();
+ try {
+ $this->team = auth()->user()->currentTeam();
+ $this->settings = $this->team->discordNotificationSettings;
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->settings->discord_enabled = $this->discordEnabled;
+ $this->settings->discord_webhook_url = $this->discordWebhookUrl;
+
+ $this->settings->deployment_success_discord_notifications = $this->deploymentSuccessDiscordNotifications;
+ $this->settings->deployment_failure_discord_notifications = $this->deploymentFailureDiscordNotifications;
+ $this->settings->status_change_discord_notifications = $this->statusChangeDiscordNotifications;
+ $this->settings->backup_success_discord_notifications = $this->backupSuccessDiscordNotifications;
+ $this->settings->backup_failure_discord_notifications = $this->backupFailureDiscordNotifications;
+ $this->settings->scheduled_task_success_discord_notifications = $this->scheduledTaskSuccessDiscordNotifications;
+ $this->settings->scheduled_task_failure_discord_notifications = $this->scheduledTaskFailureDiscordNotifications;
+ $this->settings->docker_cleanup_success_discord_notifications = $this->dockerCleanupSuccessDiscordNotifications;
+ $this->settings->docker_cleanup_failure_discord_notifications = $this->dockerCleanupFailureDiscordNotifications;
+ $this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications;
+ $this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
+ $this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
+
+ $this->settings->save();
+ refreshSession();
+ } else {
+ $this->discordEnabled = $this->settings->discord_enabled;
+ $this->discordWebhookUrl = $this->settings->discord_webhook_url;
+
+ $this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
+ $this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
+ $this->statusChangeDiscordNotifications = $this->settings->status_change_discord_notifications;
+ $this->backupSuccessDiscordNotifications = $this->settings->backup_success_discord_notifications;
+ $this->backupFailureDiscordNotifications = $this->settings->backup_failure_discord_notifications;
+ $this->scheduledTaskSuccessDiscordNotifications = $this->settings->scheduled_task_success_discord_notifications;
+ $this->scheduledTaskFailureDiscordNotifications = $this->settings->scheduled_task_failure_discord_notifications;
+ $this->dockerCleanupSuccessDiscordNotifications = $this->settings->docker_cleanup_success_discord_notifications;
+ $this->dockerCleanupFailureDiscordNotifications = $this->settings->docker_cleanup_failure_discord_notifications;
+ $this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
+ $this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
+ $this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
+ }
+ }
+
+ 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(channel: 'discord'));
+ $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 53673292e..dc2a95e84 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -2,152 +2,272 @@
namespace App\Livewire\Notifications;
+use App\Models\EmailNotificationSettings;
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
{
+ protected $listeners = ['refresh' => '$refresh'];
+
public Team $team;
+ public EmailNotificationSettings $settings;
+
+ #[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(['nullable', 'email'])]
+ public ?string $smtpFromAddress = null;
- 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', 'string'])]
+ public ?string $smtpFromName = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpRecipients = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpHost = null;
+
+ #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
+ public ?int $smtpPort = null;
+
+ #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
+ public ?string $smtpEncryption = 'tls';
+
+ #[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 $resendEnabled = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $resendApiKey = null;
+
+ #[Validate(['boolean'])]
+ public bool $useInstanceEmailSettings = false;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentSuccessEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentFailureEmailNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $statusChangeEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupSuccessEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupFailureEmailNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskSuccessEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskFailureEmailNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupSuccessEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupFailureEmailNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverDiskUsageEmailNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverReachableEmailNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $serverUnreachableEmailNotifications = true;
+
+ #[Validate(['nullable', 'email'])]
+ public ?string $testEmailAddress = 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->settings = $this->team->emailNotificationSettings;
+ $this->syncData();
+ $this->testEmailAddress = auth()->user()->email;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function sendTestNotification()
+ public function syncData(bool $toModel = false)
{
- $this->team?->notify(new Test($this->emails));
- $this->dispatch('success', 'Test Email sent.');
- }
+ if ($toModel) {
+ $this->validate();
+ $this->settings->smtp_enabled = $this->smtpEnabled;
+ $this->settings->smtp_from_address = $this->smtpFromAddress;
+ $this->settings->smtp_from_name = $this->smtpFromName;
+ $this->settings->smtp_recipients = $this->smtpRecipients;
+ $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;
- 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.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
+ $this->settings->resend_enabled = $this->resendEnabled;
+ $this->settings->resend_api_key = $this->resendApiKey;
+
+ $this->settings->use_instance_email_settings = $this->useInstanceEmailSettings;
+
+ $this->settings->deployment_success_email_notifications = $this->deploymentSuccessEmailNotifications;
+ $this->settings->deployment_failure_email_notifications = $this->deploymentFailureEmailNotifications;
+ $this->settings->status_change_email_notifications = $this->statusChangeEmailNotifications;
+ $this->settings->backup_success_email_notifications = $this->backupSuccessEmailNotifications;
+ $this->settings->backup_failure_email_notifications = $this->backupFailureEmailNotifications;
+ $this->settings->scheduled_task_success_email_notifications = $this->scheduledTaskSuccessEmailNotifications;
+ $this->settings->scheduled_task_failure_email_notifications = $this->scheduledTaskFailureEmailNotifications;
+ $this->settings->docker_cleanup_success_email_notifications = $this->dockerCleanupSuccessEmailNotifications;
+ $this->settings->docker_cleanup_failure_email_notifications = $this->dockerCleanupFailureEmailNotifications;
+ $this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications;
+ $this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
+ $this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
+ $this->settings->save();
+
+ } else {
+ $this->smtpEnabled = $this->settings->smtp_enabled;
+ $this->smtpFromAddress = $this->settings->smtp_from_address;
+ $this->smtpFromName = $this->settings->smtp_from_name;
+ $this->smtpRecipients = $this->settings->smtp_recipients;
+ $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->resendEnabled = $this->settings->resend_enabled;
+ $this->resendApiKey = $this->settings->resend_api_key;
+
+ $this->useInstanceEmailSettings = $this->settings->use_instance_email_settings;
+
+ $this->deploymentSuccessEmailNotifications = $this->settings->deployment_success_email_notifications;
+ $this->deploymentFailureEmailNotifications = $this->settings->deployment_failure_email_notifications;
+ $this->statusChangeEmailNotifications = $this->settings->status_change_email_notifications;
+ $this->backupSuccessEmailNotifications = $this->settings->backup_success_email_notifications;
+ $this->backupFailureEmailNotifications = $this->settings->backup_failure_email_notifications;
+ $this->scheduledTaskSuccessEmailNotifications = $this->settings->scheduled_task_success_email_notifications;
+ $this->scheduledTaskFailureEmailNotifications = $this->settings->scheduled_task_failure_email_notifications;
+ $this->dockerCleanupSuccessEmailNotifications = $this->settings->docker_cleanup_success_email_notifications;
+ $this->dockerCleanupFailureEmailNotifications = $this->settings->docker_cleanup_failure_email_notifications;
+ $this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications;
+ $this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
+ $this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
}
}
- public function instantSaveResend()
- {
- try {
- $this->team->smtp_enabled = false;
- $this->submitResend();
- } 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;
-
- return handleError($e, $this);
- }
- }
-
- public function saveModel()
- {
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
- }
-
public function submit()
{
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 saveModel()
+ {
+ $this->syncData(true);
+ $this->dispatch('success', 'Email notifications settings updated.');
+ }
+
+ public function instantSave(?string $type = null)
+ {
+ try {
+ $this->resetErrorBag();
+
+ if ($type === 'SMTP') {
+ $this->submitSmtp();
+ } elseif ($type === 'Resend') {
+ $this->submitResend();
+ } else {
+ $this->smtpEnabled = false;
+ $this->resendEnabled = false;
+ $this->saveModel();
+
+ return;
+ }
+ } catch (\Throwable $e) {
+ if ($type === 'SMTP') {
+ $this->smtpEnabled = false;
+ } elseif ($type === 'Resend') {
+ $this->resendEnabled = false;
+ }
return handleError($e, $this);
+ } finally {
+ $this->dispatch('refresh');
+ }
+ }
+
+ public function submitSmtp()
+ {
+ try {
+ $this->resetErrorBag();
+ $this->validate([
+ 'smtpEnabled' => 'boolean',
+ 'smtpFromAddress' => 'required|email',
+ 'smtpFromName' => 'required|string',
+ 'smtpHost' => 'required|string',
+ 'smtpPort' => 'required|numeric',
+ 'smtpEncryption' => 'required|string|in:tls,ssl,none',
+ 'smtpUsername' => 'nullable|string',
+ 'smtpPassword' => 'nullable|string',
+ 'smtpTimeout' => 'nullable|numeric',
+ ], [
+ 'smtpFromAddress.required' => 'From Address is required.',
+ 'smtpFromAddress.email' => 'Please enter a valid email address.',
+ 'smtpFromName.required' => 'From Name is required.',
+ 'smtpHost.required' => 'SMTP Host is required.',
+ 'smtpPort.required' => 'SMTP Port is required.',
+ 'smtpPort.numeric' => 'SMTP Port must be a number.',
+ 'smtpEncryption.required' => 'Encryption type is required.',
+ ]);
+
+ $this->settings->resend_enabled = false;
+ $this->settings->use_instance_email_settings = false;
+ $this->resendEnabled = false;
+ $this->useInstanceEmailSettings = false;
+
+ $this->settings->smtp_enabled = $this->smtpEnabled;
+ $this->settings->smtp_from_address = $this->smtpFromAddress;
+ $this->settings->smtp_from_name = $this->smtpFromName;
+ $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->save();
+ $this->dispatch('success', 'SMTP settings updated.');
+ } catch (\Throwable $e) {
+ $this->smtpEnabled = false;
+
+ return handleError($e);
}
}
@@ -156,16 +276,58 @@ class Email extends Component
try {
$this->resetErrorBag();
$this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- 'team.resend_api_key' => 'required',
+ 'resendEnabled' => 'boolean',
+ 'resendApiKey' => 'required|string',
+ 'smtpFromAddress' => 'required|email',
+ 'smtpFromName' => 'required|string',
+ ], [
+ 'resendApiKey.required' => 'Resend API Key is required.',
+ 'smtpFromAddress.required' => 'From Address is required.',
+ 'smtpFromAddress.email' => 'Please enter a valid email address.',
+ 'smtpFromName.required' => 'From Name is required.',
]);
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->team->resend_enabled = false;
+ $this->settings->smtp_enabled = false;
+ $this->settings->use_instance_email_settings = false;
+ $this->smtpEnabled = false;
+ $this->useInstanceEmailSettings = false;
+
+ $this->settings->resend_enabled = $this->resendEnabled;
+ $this->settings->resend_api_key = $this->resendApiKey;
+ $this->settings->smtp_from_address = $this->smtpFromAddress;
+ $this->settings->smtp_from_name = $this->smtpFromName;
+
+ $this->settings->save();
+ $this->dispatch('success', 'Resend settings updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function sendTestEmail()
+ {
+ try {
+ $this->validate([
+ 'testEmailAddress' => 'required|email',
+ ], [
+ 'testEmailAddress.required' => 'Test email address is required.',
+ 'testEmailAddress.email' => 'Please enter a valid email address.',
+ ]);
+
+ $executed = RateLimiter::attempt(
+ 'test-email:'.$this->team->id,
+ $perMinute = 0,
+ function () {
+ $this->team?->notify(new Test($this->testEmailAddress, 'email'));
+ $this->dispatch('success', 'Test Email sent.');
+ },
+ $decaySeconds = 10,
+ );
+
+ if (! $executed) {
+ throw new \Exception('Too many messages sent!');
+ }
+ } catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -173,35 +335,28 @@ class Email extends Component
public function copyFromInstanceSettings()
{
$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/Slack.php b/app/Livewire/Notifications/Slack.php
new file mode 100644
index 000000000..97464fa1c
--- /dev/null
+++ b/app/Livewire/Notifications/Slack.php
@@ -0,0 +1,168 @@
+team = auth()->user()->currentTeam();
+ $this->settings = $this->team->slackNotificationSettings;
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->settings->slack_enabled = $this->slackEnabled;
+ $this->settings->slack_webhook_url = $this->slackWebhookUrl;
+
+ $this->settings->deployment_success_slack_notifications = $this->deploymentSuccessSlackNotifications;
+ $this->settings->deployment_failure_slack_notifications = $this->deploymentFailureSlackNotifications;
+ $this->settings->status_change_slack_notifications = $this->statusChangeSlackNotifications;
+ $this->settings->backup_success_slack_notifications = $this->backupSuccessSlackNotifications;
+ $this->settings->backup_failure_slack_notifications = $this->backupFailureSlackNotifications;
+ $this->settings->scheduled_task_success_slack_notifications = $this->scheduledTaskSuccessSlackNotifications;
+ $this->settings->scheduled_task_failure_slack_notifications = $this->scheduledTaskFailureSlackNotifications;
+ $this->settings->docker_cleanup_success_slack_notifications = $this->dockerCleanupSuccessSlackNotifications;
+ $this->settings->docker_cleanup_failure_slack_notifications = $this->dockerCleanupFailureSlackNotifications;
+ $this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications;
+ $this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
+ $this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
+
+ $this->settings->save();
+ refreshSession();
+ } else {
+ $this->slackEnabled = $this->settings->slack_enabled;
+ $this->slackWebhookUrl = $this->settings->slack_webhook_url;
+
+ $this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
+ $this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
+ $this->statusChangeSlackNotifications = $this->settings->status_change_slack_notifications;
+ $this->backupSuccessSlackNotifications = $this->settings->backup_success_slack_notifications;
+ $this->backupFailureSlackNotifications = $this->settings->backup_failure_slack_notifications;
+ $this->scheduledTaskSuccessSlackNotifications = $this->settings->scheduled_task_success_slack_notifications;
+ $this->scheduledTaskFailureSlackNotifications = $this->settings->scheduled_task_failure_slack_notifications;
+ $this->dockerCleanupSuccessSlackNotifications = $this->settings->docker_cleanup_success_slack_notifications;
+ $this->dockerCleanupFailureSlackNotifications = $this->settings->docker_cleanup_failure_slack_notifications;
+ $this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications;
+ $this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
+ $this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
+ }
+ }
+
+ public function instantSaveSlackEnabled()
+ {
+ try {
+ $this->validate([
+ 'slackWebhookUrl' => 'required',
+ ], [
+ 'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->slackEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave()
+ {
+ try {
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function saveModel()
+ {
+ $this->syncData(true);
+ refreshSession();
+ $this->dispatch('success', 'Settings saved.');
+ }
+
+ public function sendTestNotification()
+ {
+ try {
+ $this->team->notify(new Test(channel: 'slack'));
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.notifications.slack');
+ }
+}
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index e163a25e0..de2fa9cdc 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -3,68 +3,231 @@
namespace App\Livewire\Notifications;
use App\Models\Team;
+use App\Models\TelegramNotificationSettings;
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',
- ];
+ public TelegramNotificationSettings $settings;
- protected $validationAttributes = [
- 'team.telegram_token' => 'Token',
- 'team.telegram_chat_id' => 'Chat ID',
- ];
+ #[Validate(['boolean'])]
+ public bool $telegramEnabled = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramToken = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramChatId = null;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentSuccessTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $deploymentFailureTelegramNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $statusChangeTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupSuccessTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $backupFailureTelegramNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskSuccessTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $scheduledTaskFailureTelegramNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupSuccessTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $dockerCleanupFailureTelegramNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverDiskUsageTelegramNotifications = true;
+
+ #[Validate(['boolean'])]
+ public bool $serverReachableTelegramNotifications = false;
+
+ #[Validate(['boolean'])]
+ public bool $serverUnreachableTelegramNotifications = true;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDeploymentSuccessTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDeploymentFailureTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsStatusChangeTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsBackupSuccessTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsBackupFailureTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsScheduledTaskSuccessTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsScheduledTaskFailureTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDockerCleanupSuccessTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDockerCleanupFailureTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsServerDiskUsageTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsServerReachableTopicId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsServerUnreachableTopicId = null;
public function mount()
{
- $this->team = auth()->user()->currentTeam();
+ try {
+ $this->team = auth()->user()->currentTeam();
+ $this->settings = $this->team->telegramNotificationSettings;
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->settings->telegram_enabled = $this->telegramEnabled;
+ $this->settings->telegram_token = $this->telegramToken;
+ $this->settings->telegram_chat_id = $this->telegramChatId;
+
+ $this->settings->deployment_success_telegram_notifications = $this->deploymentSuccessTelegramNotifications;
+ $this->settings->deployment_failure_telegram_notifications = $this->deploymentFailureTelegramNotifications;
+ $this->settings->status_change_telegram_notifications = $this->statusChangeTelegramNotifications;
+ $this->settings->backup_success_telegram_notifications = $this->backupSuccessTelegramNotifications;
+ $this->settings->backup_failure_telegram_notifications = $this->backupFailureTelegramNotifications;
+ $this->settings->scheduled_task_success_telegram_notifications = $this->scheduledTaskSuccessTelegramNotifications;
+ $this->settings->scheduled_task_failure_telegram_notifications = $this->scheduledTaskFailureTelegramNotifications;
+ $this->settings->docker_cleanup_success_telegram_notifications = $this->dockerCleanupSuccessTelegramNotifications;
+ $this->settings->docker_cleanup_failure_telegram_notifications = $this->dockerCleanupFailureTelegramNotifications;
+ $this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications;
+ $this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
+ $this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
+
+ $this->settings->telegram_notifications_deployment_success_topic_id = $this->telegramNotificationsDeploymentSuccessTopicId;
+ $this->settings->telegram_notifications_deployment_failure_topic_id = $this->telegramNotificationsDeploymentFailureTopicId;
+ $this->settings->telegram_notifications_status_change_topic_id = $this->telegramNotificationsStatusChangeTopicId;
+ $this->settings->telegram_notifications_backup_success_topic_id = $this->telegramNotificationsBackupSuccessTopicId;
+ $this->settings->telegram_notifications_backup_failure_topic_id = $this->telegramNotificationsBackupFailureTopicId;
+ $this->settings->telegram_notifications_scheduled_task_success_topic_id = $this->telegramNotificationsScheduledTaskSuccessTopicId;
+ $this->settings->telegram_notifications_scheduled_task_failure_topic_id = $this->telegramNotificationsScheduledTaskFailureTopicId;
+ $this->settings->telegram_notifications_docker_cleanup_success_topic_id = $this->telegramNotificationsDockerCleanupSuccessTopicId;
+ $this->settings->telegram_notifications_docker_cleanup_failure_topic_id = $this->telegramNotificationsDockerCleanupFailureTopicId;
+ $this->settings->telegram_notifications_server_disk_usage_topic_id = $this->telegramNotificationsServerDiskUsageTopicId;
+ $this->settings->telegram_notifications_server_reachable_topic_id = $this->telegramNotificationsServerReachableTopicId;
+ $this->settings->telegram_notifications_server_unreachable_topic_id = $this->telegramNotificationsServerUnreachableTopicId;
+
+ $this->settings->save();
+ refreshSession();
+ } else {
+ $this->telegramEnabled = $this->settings->telegram_enabled;
+ $this->telegramToken = $this->settings->telegram_token;
+ $this->telegramChatId = $this->settings->telegram_chat_id;
+
+ $this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
+ $this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
+ $this->statusChangeTelegramNotifications = $this->settings->status_change_telegram_notifications;
+ $this->backupSuccessTelegramNotifications = $this->settings->backup_success_telegram_notifications;
+ $this->backupFailureTelegramNotifications = $this->settings->backup_failure_telegram_notifications;
+ $this->scheduledTaskSuccessTelegramNotifications = $this->settings->scheduled_task_success_telegram_notifications;
+ $this->scheduledTaskFailureTelegramNotifications = $this->settings->scheduled_task_failure_telegram_notifications;
+ $this->dockerCleanupSuccessTelegramNotifications = $this->settings->docker_cleanup_success_telegram_notifications;
+ $this->dockerCleanupFailureTelegramNotifications = $this->settings->docker_cleanup_failure_telegram_notifications;
+ $this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications;
+ $this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
+ $this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
+
+ $this->telegramNotificationsDeploymentSuccessTopicId = $this->settings->telegram_notifications_deployment_success_topic_id;
+ $this->telegramNotificationsDeploymentFailureTopicId = $this->settings->telegram_notifications_deployment_failure_topic_id;
+ $this->telegramNotificationsStatusChangeTopicId = $this->settings->telegram_notifications_status_change_topic_id;
+ $this->telegramNotificationsBackupSuccessTopicId = $this->settings->telegram_notifications_backup_success_topic_id;
+ $this->telegramNotificationsBackupFailureTopicId = $this->settings->telegram_notifications_backup_failure_topic_id;
+ $this->telegramNotificationsScheduledTaskSuccessTopicId = $this->settings->telegram_notifications_scheduled_task_success_topic_id;
+ $this->telegramNotificationsScheduledTaskFailureTopicId = $this->settings->telegram_notifications_scheduled_task_failure_topic_id;
+ $this->telegramNotificationsDockerCleanupSuccessTopicId = $this->settings->telegram_notifications_docker_cleanup_success_topic_id;
+ $this->telegramNotificationsDockerCleanupFailureTopicId = $this->settings->telegram_notifications_docker_cleanup_failure_topic_id;
+ $this->telegramNotificationsServerDiskUsageTopicId = $this->settings->telegram_notifications_server_disk_usage_topic_id;
+ $this->telegramNotificationsServerReachableTopicId = $this->settings->telegram_notifications_server_reachable_topic_id;
+ $this->telegramNotificationsServerUnreachableTopicId = $this->settings->telegram_notifications_server_unreachable_topic_id;
+ }
}
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(channel: 'telegram'));
+ $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 a3a688f7c..cb63f0e1a 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -3,120 +3,205 @@
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 $disableBuildCache = false;
+
+ #[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->disable_build_cache = $this->disableBuildCache;
+ $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;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
+ }
}
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->oldRawParser();
- } else {
- $this->application->parse();
- }
- $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 (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
- $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;
}
- if (is_null($this->application->settings->custom_internal_name)) {
- $this->application->settings->save();
+ if (is_null($this->customInternalName)) {
+ $this->syncData(true);
$this->dispatch('success', 'Custom name saved.');
return;
}
- $customInternalName = $this->application->settings->custom_internal_name;
+ $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->application->settings->custom_internal_name;
+ 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->application->settings->custom_internal_name = $customInternalName;
- $this->application->settings->refresh();
+ $this->customInternalName = $customInternalName;
+ $this->syncData(true);
return;
}
- $this->application->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Custom name saved.');
}
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index d4ec8f581..5261a0800 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -16,24 +16,30 @@ class Configuration extends Component
public function mount()
{
- $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
- if (! $project) {
- return redirect()->route('dashboard');
- }
- $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
- if (! $environment) {
- return redirect()->route('dashboard');
- }
- $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
- if (! $application) {
- return redirect()->route('dashboard');
- }
+ $project = currentTeam()
+ ->projects()
+ ->select('id', 'uuid', 'team_id')
+ ->where('uuid', request()->route('project_uuid'))
+ ->firstOrFail();
+ $environment = $project->environments()
+ ->select('id', 'name', 'project_id')
+ ->where('name', request()->route('environment_name'))
+ ->firstOrFail();
+ $application = $environment->applications()
+ ->with(['destination'])
+ ->where('uuid', request()->route('application_uuid'))
+ ->firstOrFail();
+
$this->application = $application;
- $mainServer = $this->application->destination->server;
- $servers = Server::ownedByCurrentTeam()->get();
- $this->servers = $servers->filter(function ($server) use ($mainServer) {
- return $server->id != $mainServer->id;
- });
+ if ($application->destination && $application->destination->server) {
+ $mainServer = $application->destination->server;
+ $this->servers = Server::ownedByCurrentTeam()
+ ->select('id', 'name')
+ ->where('id', '!=', $mainServer->id)
+ ->get();
+ } else {
+ $this->servers = collect();
+ }
}
public function render()
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index 3de895f8c..04170fa28 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -64,7 +64,7 @@ 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;
}
}
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index 5fccce792..6a6fa2482 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -46,8 +46,6 @@ class DeploymentNavbar extends Component
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
- ray($e);
-
return handleError($e, $this);
}
}
@@ -81,8 +79,6 @@ 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([
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 2e327d80f..ff29b74e9 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -84,6 +84,7 @@ 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',
@@ -121,6 +122,7 @@ class General extends Component
'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',
@@ -241,6 +243,12 @@ class General extends Component
}
}
+ public function updatedApplicationSettingsIsStatic($value)
+ {
+ if ($value) {
+ $this->generateNginxConfiguration();
+ }
+ }
public function updatedApplicationBuildPack()
{
@@ -258,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');
@@ -275,10 +284,17 @@ class General extends Component
}
}
- public function resetDefaultLabels()
+ public function generateNginxConfiguration()
+ {
+ $this->application->custom_nginx_configuration = defaultNginxConfiguration();
+ $this->application->save();
+ $this->dispatch('success', 'Nginx configuration generated.');
+ }
+
+ public function resetDefaultLabels($manualReset = false)
{
try {
- if ($this->application->settings->is_container_label_readonly_enabled) {
+ if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
return;
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
@@ -314,7 +330,7 @@ class General extends Component
public function set_redirect()
{
try {
- $has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count();
+ $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.
Please add www to your domain list and as an A DNS record (if applicable).');
@@ -335,9 +351,15 @@ class General extends Component
$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')) {
@@ -403,17 +425,19 @@ class General extends Component
}
$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);
@@ -423,7 +447,7 @@ class General extends Component
echo $config;
}, $fileName, [
'Content-Type' => 'application/json',
- 'Content-Disposition' => 'attachment; filename=' . $fileName,
+ 'Content-Disposition' => 'attachment; filename='.$fileName,
]);
}
}
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 1082b48cd..19a6145b7 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -36,7 +36,11 @@ class Heading extends Component
public function mount()
{
- $this->parameters = get_route_parameters();
+ $this->parameters = [
+ 'project_uuid' => $this->application->project()->uuid,
+ 'environment_name' => $this->application->environment->name,
+ 'application_uuid' => $this->application->uuid,
+ ];
$lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
@@ -45,13 +49,11 @@ class Heading extends Component
public function check_status($showNotification = false)
{
if ($this->application->destination->server->isFunctional()) {
- GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high');
+ GetContainersStatus::dispatch($this->application->destination->server);
}
if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.');
}
- // Removed because it caused flickering
- // $this->dispatch('configurationChanged');
}
public function force_deploy_without_cache()
diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php
index 9a0b9b851..edcab44c8 100644
--- a/app/Livewire/Project/Application/Preview/Form.php
+++ b/app/Livewire/Project/Application/Preview/Form.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Application\Preview;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
@@ -10,49 +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')) {
- try {
- $firstFqdn = str($this->application->fqdn)->before(',');
- $url = Url::fromString($firstFqdn);
- $host = $url->getHost();
- $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
- } catch (\Exception $e) {
- $this->dispatch('error', 'Invalid FQDN.');
- }
- }
- }
+ #[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 b1ba035dc..d42bf03d7 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
@@ -239,7 +240,7 @@ class Previews extends Component
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
- $startTime = time();
+ $startTime = Carbon::now()->getTimestamp();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
@@ -249,7 +250,7 @@ class Previews extends Component
$this->removeContainer($containerName, $server);
}
- if (time() - $startTime >= $timeout) {
+ if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
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/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 7e2e4a12b..b3a54f0ab 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -2,66 +2,100 @@
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',
- 'backup.dump_all' => 'required|boolean',
- ];
+ #[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',
- 'backup.dump_all' => 'Backup All Databases',
- ];
+ #[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 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 (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ 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;
+ return;
+ }
}
try {
@@ -74,7 +108,7 @@ class BackupEdit extends Component
$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');
@@ -93,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;
@@ -117,25 +149,20 @@ 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());
}
}
- public function deleteAssociatedBackupsLocally()
+ private function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
- if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
@@ -149,17 +176,17 @@ class BackupEdit extends Component
$execution->delete();
}
- if ($backupFolder) {
+ if (str($backupFolder)->isNotEmpty()) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
- public function deleteAssociatedBackupsS3()
+ private function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
- public function deleteAssociatedBackupsSftp()
+ private function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index c8c33a022..f91b8bfaf 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -2,10 +2,10 @@
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\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
@@ -28,7 +28,6 @@ class BackupExecutions extends Component
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
- 'deleteBackup',
];
}
@@ -41,13 +40,14 @@ class BackupExecutions extends Component
}
}
- #[On('deleteBackup')]
public function deleteBackup($executionId, $password)
{
- if (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ 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;
+ return;
+ }
}
$execution = $this->backup->executions()->where('id', $executionId)->first();
@@ -57,7 +57,7 @@ class BackupExecutions extends Component
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);
@@ -119,9 +119,8 @@ class BackupExecutions extends Component
if (! $server) {
return 'UTC';
}
- $serverTimezone = $server->settings->server_timezone;
- return $serverTimezone;
+ return $server->settings->server_timezone;
}
public function formatDateInServerTimezone($date)
@@ -130,7 +129,7 @@ class BackupExecutions extends Component
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
- } catch (\Exception $e) {
+ } catch (\Exception) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index 7a6446815..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',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[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',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[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->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ 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;
+ }
}
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,16 +127,16 @@ 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;
}
@@ -92,28 +146,28 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
- $this->db_url_public = $this->database->external_db_url;
- $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..01108c290 100644
--- a/app/Livewire/Project/Database/CreateScheduledBackup.php
+++ b/app/Livewire/Project/Database/CreateScheduledBackup.php
@@ -4,59 +4,62 @@ 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(['nullable', 'integer'])]
+ public ?int $s3StorageId = null;
- 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.');
return;
}
+
$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,
];
+
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
} elseif ($this->database->type() === 'standalone-mysql') {
@@ -66,16 +69,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;
}
}
}
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index 394ba6c9a..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',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[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',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[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->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ 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;
+ }
}
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,45 +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->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $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;
-
- 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 49884ff9a..fc0febd02 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -6,6 +6,7 @@ use App\Actions\Database\RestartDatabase;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Heading extends Component
@@ -18,7 +19,7 @@ class Heading extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished',
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 f976e1edd..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',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[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',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[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->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ 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->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,45 +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->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $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;
-
- 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 12d4882f3..c9d473223 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -57,7 +57,6 @@ class General extends Component
$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()
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index ac40e7dfa..e19895dae 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -55,7 +55,6 @@ class General extends Component
$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()
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index 72fd95de8..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->internal_db_url;
- $this->db_url_public = $this->database->external_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');
}
}
@@ -119,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 8021e25d3..412240bd4 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -29,7 +29,7 @@ class ScheduledBackups extends Component
$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';
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index e01741770..1ee5de269 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -7,18 +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();
- $this->environmentName = Environment::findOrFail($this->environment_id)->name;
+ try {
+ $this->environmentName = Environment::findOrFail($this->environment_id)->name;
+ $this->parameters = get_route_parameters();
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
}
public function delete()
@@ -30,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 360fad10a..f320a19b0 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -27,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/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index d3f5b5261..417fb2ea0 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -46,7 +46,6 @@ 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,
'repository_project_id' => 0,
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index fbeb5601f..2f4f5a25c 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -153,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) {
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index 0edafd040..b46c4a794 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -198,7 +198,7 @@ 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;
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 971d4700b..bd35dccef 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -99,7 +99,6 @@ class PublicGitRepository extends Component
$this->base_directory = '/'.$this->base_directory;
}
}
-
}
public function updatedDockerComposeLocation()
@@ -174,7 +173,7 @@ class PublicGitRepository extends Component
return;
}
- if (! $this->branchFound && $this->git_branch == 'main') {
+ if (! $this->branchFound && $this->git_branch === 'main') {
try {
$this->git_branch = 'master';
$this->getBranch();
@@ -197,7 +196,7 @@ class PublicGitRepository extends Component
} else {
$this->git_branch = 'main';
}
- if ($this->git_host == 'github.com') {
+ if ($this->git_host === 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
return;
@@ -213,7 +212,7 @@ class PublicGitRepository extends Component
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->branchFound = true;
@@ -317,6 +316,7 @@ class PublicGitRepository extends Component
// $application->setConfig($config);
// }
}
+
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index 7f8247597..0b6d075a4 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -91,9 +91,16 @@ class Select extends Component
{
$services = get_service_templates(true);
$services = collect($services)->map(function ($service, $key) {
+ $default_logo = 'images/default.webp';
+ $logo = data_get($service, 'logo', $default_logo);
+ $local_logo_path = public_path($logo);
+
return [
'name' => str($key)->headline(),
- 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
+ 'logo' => asset($logo),
+ 'logo_github_url' => file_exists($local_logo_path)
+ ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
+ : asset($default_logo),
] + (array) $service;
})->all();
$gitBasedApplications = [
@@ -141,14 +148,14 @@ class Select extends Component
'id' => 'postgresql',
'name' => 'PostgreSQL',
'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
- 'logo' => '
+ 'logo' => '
',
],
[
'id' => 'mysql',
'name' => 'MySQL',
'description' => 'MySQL is an open-source relational database management system. ',
- 'logo' => '