Merge branch 'next' into useless-variable-assignments

This commit is contained in:
🏔️ Peak
2024-10-28 13:32:34 +01:00
committed by GitHub
320 changed files with 9153 additions and 3569 deletions

15
.env.dusk.ci Normal file
View File

@@ -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

View File

@@ -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

65
.github/workflows/browser-tests.yml vendored Normal file
View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -0,0 +1,143 @@
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/service-templates.json
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [self-hosted, arm64]
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app 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 }}

View File

@@ -1,17 +1,18 @@
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/soketi-entrypoint.sh
env:
REGISTRY: ghcr.io
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
@@ -22,27 +23,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 +63,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 +103,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 }}

View File

@@ -1,4 +1,4 @@
name: Coolify Realtime (v4)
name: Coolify Realtime
on:
push:
@@ -11,7 +11,8 @@ on:
- 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 +23,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 +63,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 +103,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:

View File

@@ -0,0 +1,129 @@
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/service-templates.json
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
labels: |
coolify.managed=true
aarch64:
runs-on: [self-hosted, arm64]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: 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 }}

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -12,6 +12,7 @@ class GenerateConfig
public function handle(Application $application, bool $is_json = false)
{
ray()->clearAll();
return $application->generateConfig(is_json: $is_json);
}
}

View File

@@ -21,8 +21,6 @@ class StartRedis
{
$this->database = $database;
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
@@ -37,6 +35,8 @@ class StartRedis
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
$startCommand = $this->buildStartCommand();
$docker_compose = [
'services' => [
$container_name => [
@@ -105,7 +105,6 @@ 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
@@ -160,12 +159,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 +186,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)) {

View File

@@ -3,8 +3,6 @@
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;
@@ -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)) {
@@ -650,32 +333,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);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteServer
{
use AsAction;
public function handle(Server $server)
{
StopSentinel::run($server);
$server->forceDelete();
}
}

View File

@@ -17,7 +17,7 @@ class InstallDocker
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
}
ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
$dockerVersion = '24.0';
$dockerVersion = '26.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {

View File

@@ -209,6 +209,8 @@ Files:
];
$command = array_merge($command, $add_envs_command, $restart_command);
StopLogDrain::run($server);
return instant_remote_process($command, $server);
} catch (\Throwable $e) {
return handleError($e);

View File

@@ -9,18 +9,55 @@ 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');
$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,
'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();
}
}

View File

@@ -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);
}
}

View File

@@ -15,7 +15,6 @@ use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use App\Notifications\Database\DailyBackup;
use App\Notifications\Test;
use Exception;
use Illuminate\Console\Command;
@@ -121,23 +120,6 @@ 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) {

View File

@@ -2,17 +2,16 @@
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\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
@@ -20,14 +19,15 @@ 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;
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
$this->allServers = Server::all();
$settings = instanceSettings();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
@@ -37,56 +37,51 @@ class Kernel extends ConsoleKernel
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule);
$this->checkScheduledTasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new CheckHelperImageJob)->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->scheduleUpdates($schedule);
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->pull_images($schedule);
$this->check_scheduled_tasks($schedule);
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule);
$this->pullImages($schedule);
$this->checkScheduledTasks($schedule);
$schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
}
}
private function pull_images($schedule)
private function pullImages($schedule): void
{
$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->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$schedule->job(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);
}
CheckAndStartSentinelJob::dispatch($server);
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
}
$schedule->job(new PullHelperImageJob)
$schedule->job(new CheckHelperImageJob)
->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
private function schedule_updates($schedule)
private function scheduleUpdates($schedule): void
{
$settings = instanceSettings();
@@ -105,19 +100,21 @@ class Kernel extends ConsoleKernel
}
}
private function check_resources($schedule)
private function checkResources($schedule): void
{
if (isCloud()) {
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
$servers = $this->allServers->where('ip', '!=', '1.2.3.4');
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
$lastSentinelUpdate = $server->sentinel_updated_at;
$serverTimezone = $server->settings->server_timezone;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
}
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
@@ -126,7 +123,7 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_backups($schedule)
private function checkScheduledBackups($schedule): void
{
$scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) {
@@ -159,7 +156,7 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_tasks($schedule)
private function checkScheduledTasks($schedule): void
{
$scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) {

View File

@@ -1579,11 +1579,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();
}

View File

@@ -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(

View File

@@ -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;
@@ -23,7 +24,7 @@ class ServersController extends Controller
return serializeApiResponse($settings);
}
$settings = $settings->makeHidden([
'metrics_token',
'sentinel_token',
]);
return serializeApiResponse($settings);
@@ -726,6 +727,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.']);
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Jobs;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
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;
class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 120;
public function __construct(public Server $server) {}
public function handle(): void
{
try {
$latestVersion = get_latest_sentinel_version();
// Check if sentinel is running
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->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;
}
}
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Jobs;
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\Http;
class CheckHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct() {}
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, '>')) {
$settings->update(['helper_version' => $latest_version]);
}
}
} catch (\Throwable $e) {
send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -504,8 +504,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$network = $this->database->destination->network;
}
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName();
if (isDev()) {
@@ -538,35 +536,6 @@ 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();

View File

@@ -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,28 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function __construct() {}
public function __construct(public Server $server) {}
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]);
}
}
$helperImage = config('coolify.helper_image');
$latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
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;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct(public Server $server) {}
public function handle(): void
{
try {
$version = get_latest_sentinel_version();
if (! $version) {
ray('Failed to get latest Sentinel version');
return;
}
$local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($local_version)) {
$local_version = '0.0.0';
}
if (version_compare($local_version, $version, '<')) {
StartSentinel::run($this->server, $version, true);
return;
}
ray('Sentinel image is up to date');
} catch (\Throwable $e) {
// send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,404 @@
<?php
namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceApplication;
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\SerializesModels;
use Illuminate\Support\Collection;
class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 30;
public Collection $containers;
public Collection $applications;
public Collection $previews;
public Collection $databases;
public Collection $services;
public Collection $allApplicationIds;
public Collection $allDatabaseUuids;
public Collection $allTcpProxyUuids;
public Collection $allServiceApplicationIds;
public Collection $allApplicationPreviewsIds;
public Collection $allServiceDatabaseIds;
public Collection $allApplicationsWithAdditionalServers;
public Collection $foundApplicationIds;
public Collection $foundDatabaseUuids;
public Collection $foundServiceApplicationIds;
public Collection $foundServiceDatabaseIds;
public Collection $foundApplicationPreviewsIds;
public bool $foundProxy = false;
public bool $foundLogDrainContainer = false;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server, public $data)
{
$this->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
try {
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);
});
});
ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]);
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) {
ray()->error($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)) {
ray("Service: $uuid, $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();
} catch (\Exception $e) {
throw $e;
}
}
private function updateApplicationStatus(string $applicationId, string $containerStatus)
{
$application = $this->applications->where('id', $applicationId)->first();
if (! $application) {
return;
}
$application->status = $containerStatus;
$application->save();
ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
}
private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
{
$application = $this->previews->where('id', $applicationId)->first();
if (! $application) {
return;
}
$application->status = $containerStatus;
$application->save();
ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
}
private function updateNotFoundApplicationStatus()
{
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
if ($notFoundApplicationIds->isNotEmpty()) {
ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
$notFoundApplicationIds->each(function ($applicationId) {
ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
$application = Application::find($applicationId);
if ($application) {
$application->status = 'exited';
$application->save();
ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
}
});
}
}
private function updateNotFoundApplicationPreviewStatus()
{
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]);
$notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
$applicationPreview = ApplicationPreview::find($applicationPreviewId);
if ($applicationPreview) {
$applicationPreview->status = 'exited';
$applicationPreview->save();
ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
}
});
}
}
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();
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]);
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) {
ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]);
StartDatabaseProxy::dispatch($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
} else {
ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]);
}
}
}
private function updateNotFoundDatabaseStatus()
{
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
if ($notFoundDatabaseUuids->isNotEmpty()) {
ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]);
$notFoundDatabaseUuids->each(function ($databaseUuid) {
ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
$database->status = 'exited';
$database->save();
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]);
if ($database->is_public) {
ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]);
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();
ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
} elseif ($subType === 'database') {
$database = $service->databases()->where('id', $subId)->first();
$database->status = $containerStatus;
$database->save();
ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
} else {
ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
}
}
private function updateNotFoundServiceStatus()
{
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
if ($notFoundServiceApplicationIds->isNotEmpty()) {
ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]);
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
$application = ServiceApplication::find($serviceApplicationId);
if ($application) {
$application->status = 'exited';
$application->save();
ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
}
});
}
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]);
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
$database = ServiceDatabase::find($serviceDatabaseId);
if ($database) {
$database->status = 'exited';
$database->save();
ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
}
});
}
}
private function updateAdditionalServersStatus()
{
$this->allApplicationsWithAdditionalServers->each(function ($application) {
ray('Updating additional servers status for application', ['application_id' => $application->id]);
ComplexStatusCheck::run($application);
});
}
private function isRunning(string $containerStatus)
{
return str($containerStatus)->contains('running');
}
private function checkLogDrainContainer()
{
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
InstallLogDrain::dispatch($this->server);
}
}
}

View File

@@ -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,7 +30,7 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public int $maxExceptions = 5;
public function __construct(
public string $text,
public DiscordMessage $message,
public string $webhookUrl
) {}
@@ -38,9 +39,6 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
*/
public function handle(): void
{
$payload = [
'content' => $this->text,
];
Http::post($this->webhookUrl, $payload);
Http::post($this->webhookUrl, $this->message->toPayload());
}
}

View File

@@ -2,14 +2,11 @@
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\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -17,7 +14,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -47,31 +43,57 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
if ($this->server->serverStatus() === false) {
return 'Server is not reachable or not ready.';
}
$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->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.';
}
ServerStorageCheckJob::dispatch($this->server);
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) {
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);
}
}
}
} catch (\Throwable $e) {
@@ -82,39 +104,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
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()
{
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
@@ -129,289 +118,4 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
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);
}
}
}

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Server;
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;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int|string|null $disk_usage = null;
public $tries = 3;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server) {}
public function handle()
{
if (! $this->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);
}
}
}

View File

@@ -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,40 +20,47 @@ 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 $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.'%');
$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();
loggy('Server storage check percentage: '.$this->percentage);
}
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);
}

View File

@@ -14,6 +14,18 @@ class Index extends Component
public $search = '';
public function mount()
{
if (! isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function submitSearch()
{
if ($this->search !== '') {
@@ -38,17 +50,6 @@ class Index extends Component
}
}
public function mount()
{
if (! isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function getSubscribers()
{
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {

View File

@@ -85,26 +85,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerDescription = 'Created by Coolify';
$this->remoteServerHost = 'coolify-testing-host';
}
// if ($this->currentState === 'create-project') {
// $this->getProjects();
// }
// if ($this->currentState === 'create-resource') {
// $this->selectExistingServer();
// $this->selectExistingProject();
// }
// if ($this->currentState === 'private-key') {
// $this->setServerType('remote');
// }
// if ($this->currentState === 'create-server') {
// $this->selectExistingPrivateKey();
// }
// if ($this->currentState === 'validate-server') {
// $this->selectExistingServer();
// }
// if ($this->currentState === 'select-existing-server') {
// $this->selectExistingServer();
// }
}
public function explanation()

View File

@@ -66,7 +66,7 @@ class Show extends Component
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
});
if ($this->networks->count() === 0) {
$this->dispatch('success', 'No new networks found.');
$this->dispatch('success', 'No new destinations found on this server.');
return;
}

View File

@@ -24,6 +24,9 @@ class ForcePasswordReset extends Component
public function mount()
{
if (auth()->user()->force_password_reset === false) {
return redirect()->route('dashboard');
}
$this->email = auth()->user()->email;
}
@@ -34,6 +37,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();

View File

@@ -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,10 +19,12 @@ 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();

View File

@@ -18,6 +18,7 @@ class Discord extends Component
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
'team.discord_notifications_server_disk_usage' => 'nullable|boolean',
];
protected $validationAttributes = [

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
class Email extends Component
@@ -30,6 +31,7 @@ class Email extends Component
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
'team.smtp_notifications_server_disk_usage' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
@@ -74,8 +76,23 @@ class Email extends Component
public function sendTestNotification()
{
$this->team?->notify(new Test($this->emails));
$this->dispatch('success', 'Test Email sent.');
try {
$executed = RateLimiter::attempt(
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
$this->team?->notify(new Test($this->emails));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
);
if (! $executed) {
throw new \Exception('Too many messages sent!');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSaveInstance()

View File

@@ -24,6 +24,7 @@ class Telegram extends Component
'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',
'team.telegram_notifications_server_disk_usage' => 'nullable|boolean',
];
protected $validationAttributes = [

View File

@@ -6,6 +6,7 @@ use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component
@@ -183,9 +184,7 @@ class General extends Component
$storage->save();
});
}
}
}
public function loadComposeFile($isInit = false)
@@ -242,24 +241,6 @@ class General extends Component
}
}
public function updatedApplicationFqdn()
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
$this->application->fqdn = $originalFqdn;
return handleError($e, $this);
}
$this->resetDefaultLabels();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@@ -293,10 +274,10 @@ class General extends Component
}
}
public function resetDefaultLabels()
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");
@@ -349,15 +330,24 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
}
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$warning = sslipDomainWarning($this->application->fqdn);
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
$this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) {
$this->set_redirect();
}
$this->checkFqdns();
@@ -418,13 +408,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);
@@ -434,7 +430,7 @@ class General extends Component
echo $config;
}, $fileName, [
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename=' . $fileName,
'Content-Disposition' => 'attachment; filename='.$fileName,
]);
}
}

View File

@@ -31,10 +31,14 @@ class Form extends Component
public function generate_real_url()
{
if (data_get($this->application, 'fqdn')) {
$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);
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.');
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Database;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
@@ -58,10 +59,12 @@ class BackupEdit 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;
}
}
try {

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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,7 +34,7 @@ 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.');

View File

@@ -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();
}

View File

@@ -317,6 +317,7 @@ class PublicGitRepository extends Component
// $application->setConfig($config);
// }
}
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,

View File

@@ -32,8 +32,11 @@ class Index extends Component
public $services = [];
public array $parameters;
public function mount()
{
$this->parameters = get_route_parameters();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
@@ -44,7 +47,6 @@ class Index extends Component
}
$this->project = $project;
$this->environment = $environment;
$this->applications = $this->environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
use Spatie\Url\Url;
class EditDomain extends Component
{
@@ -21,24 +22,21 @@ class EditDomain extends Component
$this->application = ServiceApplication::find($this->applicationId);
}
public function updatedApplicationFqdn()
public function submit()
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$warning = sslipDomainWarning($this->application->fqdn);
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -46,14 +44,18 @@ class EditDomain extends Component
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
$this->dispatch('success', 'Service saved.');
! $warning && $this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
return handleError($e, $this);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Service;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\LocalFileVolume;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -87,10 +88,12 @@ class FileStorage 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;
}
}
try {

View File

@@ -39,6 +39,7 @@ class Navbar extends Component
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
'envsUpdated' => '$refresh',
];
}

View File

@@ -2,10 +2,12 @@
namespace App\Livewire\Project\Service;
use App\Models\InstanceSettings;
use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
class ServiceApplicationView extends Component
{
@@ -29,17 +31,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$this->application->save();
}
public function instantSave()
{
$this->submit();
@@ -59,10 +50,12 @@ class ServiceApplicationView 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;
}
}
try {
@@ -83,6 +76,18 @@ class ServiceApplicationView extends Component
public function submit()
{
try {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$warning = sslipDomainWarning($this->application->fqdn);
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -90,12 +95,16 @@ class ServiceApplicationView extends Component
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
$this->dispatch('success', 'Service saved.');
! $warning && $this->dispatch('success', 'Service saved.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('generateDockerCompose');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
return handleError($e, $this);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
use App\Models\InstanceSettings;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -91,7 +92,7 @@ class Danger extends Component
public function delete($password)
{
if (isProduction()) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Facades\Auth;
@@ -119,10 +119,12 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
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;
}
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {

View File

@@ -37,6 +37,7 @@ class Show extends Component
'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
'env.is_required' => 'required|boolean',
];
protected $validationAttributes = [
@@ -46,6 +47,7 @@ class Show extends Component
'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once',
'env.is_required' => 'Required',
];
public function refresh()
@@ -109,15 +111,21 @@ class Show extends Component
} else {
$this->validate();
}
// if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
// $type = str($this->env->value)->after('{{')->before('.')->value;
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
// return;
// }
// }
if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) {
$oldValue = $this->env->getOriginal('value');
$this->env->value = $oldValue;
$this->dispatch('error', 'Required environment variable cannot be empty.');
return;
}
$this->serialize();
if ($this->isSharedVariable) {
unset($this->env->is_required);
}
$this->env->save();
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('envsUpdated');

View File

@@ -52,6 +52,7 @@ class ExecuteContainerCommand extends Component
$this->servers = $this->servers->push($server);
}
}
$this->loadContainers();
} elseif (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
@@ -62,12 +63,18 @@ class ExecuteContainerCommand extends Component
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
$this->loadContainers();
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
$this->loadContainers();
} elseif (data_get($this->parameters, 'server_uuid')) {
$this->type = 'server';
$this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail();
$this->server = $this->resource;
}
}
@@ -130,6 +137,28 @@ class ExecuteContainerCommand extends Component
if ($this->containers->count() > 0) {
$this->container = $this->containers->first();
}
if ($this->containers->count() === 1) {
$this->selected_container = data_get($this->containers->first(), 'container.Names');
}
}
#[On('connectToServer')]
public function connectToServer()
{
try {
if ($this->server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
$this->dispatch(
'send-terminal-command',
false,
data_get($this->server, 'name'),
data_get($this->server, 'uuid')
);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
#[On('connectToContainer')]

View File

@@ -39,7 +39,7 @@ class GetLogs extends Component
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public ?int $numberOfLines = 100;
public function mount()
{
@@ -98,7 +98,7 @@ class GetLogs extends Component
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
return;
}
if ($this->numberOfLines <= 0) {
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
$this->numberOfLines = 1000;
}
if ($this->container) {

View File

@@ -31,13 +31,8 @@ class Metrics extends Component
public function loadData()
{
try {
$metrics = $this->resource->getMetrics($this->interval);
$cpuMetrics = collect($metrics)->map(function ($metric) {
return [$metric[0], $metric[1]];
});
$memoryMetrics = collect($metrics)->map(function ($metric) {
return [$metric[0], $metric[2]];
});
$cpuMetrics = $this->resource->getCpuMetrics($this->interval);
$memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]);

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\InstanceSettings;
use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
@@ -40,10 +41,12 @@ class Show 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;
}
}
$this->storage->delete();

View File

@@ -8,8 +8,11 @@ use Livewire\Component;
class UploadConfig extends Component
{
public $config;
public $applicationId;
public function mount() {
public function mount()
{
if (isDev()) {
$this->config = '{
"build_pack": "nixpacks",
@@ -22,6 +25,7 @@ class UploadConfig extends Component
}';
}
}
public function uploadConfig()
{
try {
@@ -30,10 +34,12 @@ class UploadConfig extends Component
$this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
return;
}
}
public function render()
{
return view('livewire.project.shared.upload-config');

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Livewire\Server;
use App\Jobs\DockerCleanupJob;
use App\Models\Server;
use Livewire\Component;
class Advanced extends Component
{
public Server $server;
protected $rules = [
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
'server.settings.server_disk_usage_notification_threshold' => 'required|integer|min:50|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency',
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold',
'server.settings.server_disk_usage_notification_threshold' => 'Server Disk Usage Notification Threshold',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
public function instantSave()
{
try {
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this);
}
}
public function manualCleanup()
{
try {
DockerCleanupJob::dispatch($this->server, true);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$frequency = $this->server->settings->docker_cleanup_frequency;
if (empty($frequency) || ! validate_cron_expression($frequency)) {
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
}
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.advanced');
}
}

View File

@@ -34,12 +34,6 @@ class Charts extends Component
try {
$cpuMetrics = $this->server->getCpuMetrics($this->interval);
$memoryMetrics = $this->server->getMemoryMetrics($this->interval);
$cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
return [$metric[0], $metric[1]];
});
$memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
return [$metric[0], $metric[1]];
});
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]);

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class CloudflareTunnels extends Component
{
public Server $server;
protected $rules = [
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
];
protected $validationAttributes = [
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
];
public function instantSave()
{
try {
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
public function render()
{
return view('livewire.server.cloudflare-tunnels');
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Livewire\Server;
use App\Actions\Server\DeleteServer;
use App\Models\InstanceSettings;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
@@ -15,10 +17,12 @@ class Delete 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;
}
}
try {
$this->authorize('delete', $this->server);
@@ -28,6 +32,7 @@ class Delete extends Component
return;
}
$this->server->delete();
DeleteServer::dispatch($this->server);
return redirect()->route('server.index');
} catch (\Throwable $e) {

View File

@@ -4,8 +4,6 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
use Livewire\Component;
@@ -46,25 +44,19 @@ class Form extends Component
'server.ip' => 'required',
'server.user' => 'required',
'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean',
'server.settings.metrics_token' => 'required',
'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean',
'server.settings.sentinel_token' => 'required',
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
'server.settings.sentinel_custom_url' => 'nullable|url',
'server.settings.is_sentinel_enabled' => 'required|boolean',
'server.settings.server_timezone' => 'required|string|timezone',
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
@@ -73,21 +65,18 @@ class Form extends Component
'server.ip' => 'IP address/Domain',
'server.user' => 'User',
'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager',
'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server',
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.is_metrics_enabled' => 'Metrics',
'server.settings.metrics_token' => 'Metrics Token',
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
'server.settings.sentinel_token' => 'Metrics Token',
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.sentinel_metrics_history_days' => 'Metrics History',
'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
'server.settings.is_sentinel_enabled' => 'Server API',
'server.settings.sentinel_custom_url' => 'Coolify URL',
'server.settings.server_timezone' => 'Server Timezone',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
public function mount(Server $server)
@@ -97,6 +86,25 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
}
public function checkSyncStatus()
{
$this->server->refresh();
$this->server->settings->refresh();
}
public function regenerateSentinelToken()
{
try {
$this->server->settings->generateSentinelToken();
$this->server->settings->refresh();
// $this->restartSentinel(notification: false);
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updated($field)
@@ -129,21 +137,35 @@ class Form extends Component
$this->dispatch('proxyStatusUpdated');
}
public function checkPortForServerApi()
public function updatedServerSettingsIsSentinelEnabled($value)
{
try {
if ($this->server->settings->is_server_api_enabled === true) {
$this->server->checkServerApi();
$this->dispatch('success', 'Server API is reachable.');
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
if ($value === false) {
StopSentinel::dispatch($this->server);
$this->server->settings->is_metrics_enabled = false;
$this->server->settings->save();
$this->server->sentinelHeartbeat(isReset: true);
} else {
try {
StartSentinel::run($this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedServerSettingsIsMetricsEnabled()
{
$this->restartSentinel();
}
public function instantSave()
{
try {
$this->validate();
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
@@ -151,33 +173,39 @@ class Form extends Component
$this->server->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
if ($this->server->isSentinelEnabled()) {
PullSentinelImageJob::dispatchSync($this->server);
ray('Sentinel is enabled');
if ($this->server->settings->isDirty('is_metrics_enabled')) {
$this->dispatch('reloadWindow');
}
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
ray('Starting sentinel');
}
} else {
ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server);
}
$this->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this);
} finally {
}
}
public function restartSentinel()
public function saveSentinel()
{
try {
$version = get_latest_sentinel_version();
StartSentinel::run($this->server, $version, true);
$this->dispatch('success', 'Sentinel restarted.');
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Sentinel updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->checkSyncStatus();
}
}
public function restartSentinel($notification = true)
{
try {
$this->validate();
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
$this->server->settings->save();
$this->server->restartSentinel(async: false);
if ($notification) {
$this->dispatch('success', 'Sentinel restarted.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -238,7 +266,6 @@ class Form extends Component
$newTimezone = $this->server->settings->server_timezone;
if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
$this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save();
}
$this->server->settings->save();
$this->server->save();
@@ -248,29 +275,4 @@ class Form extends Component
return handleError($e, $this);
}
}
public function updatedServerSettingsServerTimezone($value)
{
$this->server->settings->server_timezone = $value;
$this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.');
}
public function manualCleanup()
{
try {
DockerCleanupJob::dispatch($this->server, true);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Modal extends Component
{
public Server $server;
public function proxyStatusUpdated()
{
$this->dispatch('proxyStatusUpdated');
}
}

View File

@@ -22,10 +22,7 @@ class Show extends Component
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -15,7 +15,9 @@ class Resources extends Component
public $parameters = [];
public Collection $unmanagedContainers;
public Collection $containers;
public $activeTab = 'managed';
public function getListeners()
{
@@ -50,14 +52,29 @@ class Resources extends Component
public function refreshStatus()
{
$this->server->refresh();
$this->loadUnmanagedContainers();
if ($this->activeTab === 'managed') {
$this->loadManagedContainers();
} else {
$this->loadUnmanagedContainers();
}
$this->dispatch('success', 'Resource statuses refreshed.');
}
public function loadManagedContainers()
{
try {
$this->activeTab = 'managed';
$this->containers = $this->server->refresh()->definedResources();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function loadUnmanagedContainers()
{
$this->activeTab = 'unmanaged';
try {
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
$this->containers = $this->server->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -65,13 +82,14 @@ class Resources extends Component
public function mount()
{
$this->unmanagedContainers = collect();
$this->containers = collect();
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$this->loadManagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -10,20 +10,17 @@ class Show extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public Server $server;
public $parameters = [];
public array $parameters;
protected $listeners = ['refreshServerShow'];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
$this->parameters = get_route_parameters();
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -14,15 +14,36 @@ class ShowPrivateKey extends Component
public $parameters;
public function mount()
{
$this->parameters = get_route_parameters();
}
public function setPrivateKey($privateKeyId)
{
$ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId);
if (is_null($ownedPrivateKey)) {
$this->dispatch('error', 'You are not allowed to use this private key.');
return;
}
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try {
$privateKey = PrivateKey::findOrFail($privateKeyId);
$this->server->update(['private_key_id' => $privateKey->id]);
$this->server->refresh();
$this->dispatch('success', 'Private key updated successfully.');
$this->server->update(['private_key_id' => $privateKeyId]);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Private key updated successfully.');
} else {
throw new \Exception('Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
}
} catch (\Exception $e) {
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
$this->server->validateConnection();
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
}
}
@@ -33,18 +54,15 @@ class ShowPrivateKey extends Component
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
} else {
ray($error);
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
return;
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
}
}
public function mount()
{
$this->parameters = get_route_parameters();
}
}

View File

@@ -5,62 +5,95 @@ namespace App\Livewire\Settings;
use App\Jobs\CheckForUpdatesJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component;
class Index extends Component
{
public InstanceSettings $settings;
public bool $do_not_track;
public bool $is_auto_update_enabled;
public bool $is_registration_enabled;
public bool $is_dns_validation_enabled;
public bool $is_api_enabled;
public string $auto_update_frequency;
public string $update_check_frequency;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
protected $rules = [
'settings.fqdn' => 'nullable',
'settings.resale_license' => 'nullable',
'settings.public_port_min' => 'required',
'settings.public_port_max' => 'required',
'settings.custom_dns_servers' => 'nullable',
'settings.instance_name' => 'nullable',
'settings.allowed_ips' => 'nullable',
'settings.is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
'settings.instance_timezone' => 'required|string|timezone',
];
protected $validationAttributes = [
'settings.fqdn' => 'FQDN',
'settings.resale_license' => 'Resale License',
'settings.public_port_min' => 'Public port min',
'settings.public_port_max' => 'Public port max',
'settings.custom_dns_servers' => 'Custom DNS servers',
'settings.allowed_ips' => 'Allowed IPs',
'settings.is_auto_update_enabled' => 'Auto Update Enabled',
'auto_update_frequency' => 'Auto Update Frequency',
'update_check_frequency' => 'Update Check Frequency',
];
#[Locked]
public $timezones;
#[Rule('boolean')]
public bool $is_auto_update_enabled;
#[Rule('nullable|string|max:255')]
public ?string $fqdn = null;
#[Rule('nullable|string|max:255')]
public ?string $resale_license = null;
#[Rule('required|integer|min:1025|max:65535')]
public int $public_port_min;
#[Rule('required|integer|min:1025|max:65535')]
public int $public_port_max;
#[Rule('nullable|string')]
public ?string $custom_dns_servers = null;
#[Rule('nullable|string|max:255')]
public ?string $instance_name = null;
#[Rule('nullable|string')]
public ?string $allowed_ips = null;
#[Rule('nullable|string')]
public ?string $public_ipv4 = null;
#[Rule('nullable|string')]
public ?string $public_ipv6 = null;
#[Rule('string')]
public string $auto_update_frequency;
#[Rule('string')]
public string $update_check_frequency;
#[Rule('required|string|timezone')]
public string $instance_timezone;
#[Rule('boolean')]
public bool $do_not_track;
#[Rule('boolean')]
public bool $is_registration_enabled;
#[Rule('boolean')]
public bool $is_dns_validation_enabled;
#[Rule('boolean')]
public bool $is_api_enabled;
#[Rule('boolean')]
public bool $disable_two_step_confirmation;
public function render()
{
return view('livewire.settings.index');
}
public function mount()
{
if (isInstanceAdmin()) {
if (! isInstanceAdmin()) {
return redirect()->route('dashboard');
} else {
$this->settings = instanceSettings();
$this->fqdn = $this->settings->fqdn;
$this->resale_license = $this->settings->resale_license;
$this->public_port_min = $this->settings->public_port_min;
$this->public_port_max = $this->settings->public_port_max;
$this->custom_dns_servers = $this->settings->custom_dns_servers;
$this->instance_name = $this->settings->instance_name;
$this->allowed_ips = $this->settings->allowed_ips;
$this->public_ipv4 = $this->settings->public_ipv4;
$this->public_ipv6 = $this->settings->public_ipv6;
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -69,13 +102,22 @@ class Index extends Component
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
} else {
return redirect()->route('dashboard');
$this->instance_timezone = $this->settings->instance_timezone;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
public function instantSave()
public function instantSave($isSave = true)
{
$this->settings->fqdn = $this->fqdn;
$this->settings->resale_license = $this->resale_license;
$this->settings->public_port_min = $this->public_port_min;
$this->settings->public_port_max = $this->public_port_max;
$this->settings->custom_dns_servers = $this->custom_dns_servers;
$this->settings->instance_name = $this->instance_name;
$this->settings->allowed_ips = $this->allowed_ips;
$this->settings->public_ipv4 = $this->public_ipv4;
$this->settings->public_ipv6 = $this->public_ipv6;
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
@@ -83,8 +125,12 @@ class Index extends Component
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency;
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
$this->settings->instance_timezone = $this->instance_timezone;
if ($isSave) {
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
}
}
public function submit()
@@ -141,13 +187,8 @@ class Index extends Component
$this->settings->allowed_ips = $this->settings->allowed_ips->unique();
$this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency;
$this->instantSave(isSave: false);
$this->settings->save();
$this->server->setupDynamicProxyConfiguration();
if (! $error_show) {
@@ -170,15 +211,16 @@ class Index extends Component
}
}
public function updatedSettingsInstanceTimezone($value)
public function toggleTwoStepConfirmation($password)
{
$this->settings->instance_timezone = $value;
$this->settings->save();
$this->dispatch('success', 'Instance timezone updated.');
}
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
public function render()
{
return view('livewire.settings.index');
return;
}
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
$this->settings->save();
$this->dispatch('success', 'Two step confirmation has been disabled.');
}
}

View File

@@ -28,6 +28,9 @@ class License extends Component
if (! isCloud()) {
abort(404);
}
if (! isInstanceAdmin()) {
return redirect()->route('home');
}
$this->instance_id = config('app.id');
$this->settings = instanceSettings();
}

View File

@@ -2,50 +2,59 @@
namespace App\Livewire;
use App\Jobs\DatabaseBackupJob;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component;
class SettingsBackup extends Component
{
public InstanceSettings $settings;
public $s3s;
public ?StandalonePostgresql $database = null;
public ScheduledDatabaseBackup|null|array $backup = [];
#[Locked]
public $s3s;
#[Locked]
public $executions = [];
protected $rules = [
'database.uuid' => 'required',
'database.name' => 'required',
'database.description' => 'nullable',
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
#[Rule(['required'])]
public string $uuid;
];
#[Rule(['required'])]
public string $name;
protected $validationAttributes = [
'database.uuid' => 'uuid',
'database.name' => 'name',
'database.description' => 'description',
'database.postgres_user' => 'postgres user',
'database.postgres_password' => 'postgres password',
];
#[Rule(['nullable'])]
public ?string $description = null;
#[Rule(['required'])]
public string $postgres_user;
#[Rule(['required'])]
public string $postgres_password;
public function mount()
{
if (isInstanceAdmin()) {
if (! isInstanceAdmin()) {
return redirect()->route('dashboard');
} else {
$settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
$this->uuid = $this->database->uuid;
$this->name = $this->database->name;
$this->description = $this->database->description;
$this->postgres_user = $this->database->postgres_user;
$this->postgres_password = $this->database->postgres_password;
if ($this->database->status !== 'running') {
$this->database->status = 'running';
$this->database->save();
@@ -55,13 +64,10 @@ class SettingsBackup extends Component
}
$this->settings = $settings;
$this->s3s = $s3s;
} else {
return redirect()->route('dashboard');
}
}
public function add_coolify_database()
public function addCoolifyDatabase()
{
try {
$server = Server::findOrFail(0);
@@ -98,16 +104,14 @@ class SettingsBackup extends Component
}
}
public function backup_now()
{
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
}
public function submit()
{
$this->database->update([
'name' => $this->name,
'description' => $this->description,
'postgres_user' => $this->postgres_user,
'postgres_password' => $this->postgres_password,
]);
$this->dispatch('success', 'Backup updated.');
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
use App\Notifications\TransactionalEmails\Test;
use Livewire\Component;
class SettingsEmail extends Component
@@ -124,10 +123,4 @@ class SettingsEmail extends Component
return handleError($e, $this);
}
}
public function sendTestNotification()
{
$this->settings?->notify(new Test($this->emails));
$this->dispatch('success', 'Test email sent.');
}
}

View File

@@ -24,6 +24,9 @@ class SettingsOauth extends Component
public function mount()
{
if (! isInstanceAdmin()) {
return redirect()->route('home');
}
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
$carry[$setting->provider] = $setting;

View File

@@ -93,52 +93,55 @@ class Change extends Component
// }
public function mount()
{
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
if (! $this->github_app) {
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
try {
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
if ($settings->public_ipv4) {
$this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
}
if ($settings->public_ipv6) {
$this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
}
if ($this->github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (! $source_id || $this->github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
if ($settings->public_ipv4) {
$this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
}
if ($settings->public_ipv6) {
$this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
}
if ($this->github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (! $source_id || $this->github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
}
}
$this->parameters = get_route_parameters();
if (isCloud() && ! isDev()) {
$this->webhook_endpoint = config('app.url');
} else {
$this->webhook_endpoint = $this->ipv4;
$this->is_system_wide = $this->github_app->is_system_wide;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->parameters = get_route_parameters();
if (isCloud() && ! isDev()) {
$this->webhook_endpoint = config('app.url');
} else {
$this->webhook_endpoint = $this->ipv4;
$this->is_system_wide = $this->github_app->is_system_wide;
}
}
public function submit()

View File

@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
$this->name = generate_random_name();
$this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
}
public function createGitHubApp()

View File

@@ -7,19 +7,19 @@ use Livewire\Component;
class Deployments extends Component
{
public $deployments_per_tag_per_server = [];
public $deploymentsPerTagPerServer = [];
public $resource_ids = [];
public $resourceIds = [];
public function render()
{
return view('livewire.tags.deployments');
}
public function get_deployments()
public function getDeployments()
{
try {
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([
$this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([
'id',
'application_id',
'application_name',
@@ -29,7 +29,7 @@ class Deployments extends Component
'server_id',
'status',
])->sortBy('id')->groupBy('server_name')->toArray();
$this->dispatch('deployments', $this->deployments_per_tag_per_server);
$this->dispatch('deployments', $this->deploymentsPerTagPerServer);
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -5,9 +5,11 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\Tag;
use Illuminate\Support\Collection;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
#[Title('Tags | Coolify')]
class Index extends Component
{
#[Url()]
@@ -21,33 +23,47 @@ class Index extends Component
public $webhook = null;
public $deployments_per_tag_per_server = [];
public $deploymentsPerTagPerServer = [];
protected $listeners = ['deployments' => 'update_deployments'];
protected $listeners = ['deployments' => 'updateDeployments'];
public function update_deployments($deployments)
public function render()
{
$this->deployments_per_tag_per_server = $deployments;
return view('livewire.tags.index');
}
public function tag_updated()
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
if ($this->tag) {
$this->tagUpdated();
}
}
public function updateDeployments($deployments)
{
$this->deploymentsPerTagPerServer = $deployments;
}
public function tagUpdated()
{
if ($this->tag == '') {
return;
}
$tag = $this->tags->where('name', $this->tag)->first();
$sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8');
$tag = $this->tags->where('name', $sanitizedTag)->first();
if (! $tag) {
$this->dispatch('error', "Tag ({$this->tag}) not found.");
$this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.');
$this->tag = '';
return;
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
}
public function redeploy_all()
public function redeployAll()
{
try {
$this->applications->each(function ($resource) {
@@ -63,17 +79,4 @@ class Index extends Component
return handleError($e, $this);
}
}
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
if ($this->tag) {
$this->tag_updated();
}
}
public function render()
{
return view('livewire.tags.index');
}
}

View File

@@ -5,8 +5,10 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Tags | Coolify')]
class Show extends Component
{
public $tags;
@@ -28,7 +30,7 @@ class Show extends Component
if (! $tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Team;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
@@ -77,10 +78,12 @@ class AdminView extends Component
public function delete($id, $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;
}
}
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');

View File

@@ -13,17 +13,18 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id)
{
$initiation_found = TeamInvitation::find($invitation_id);
if (! $initiation_found) {
try {
$initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
$initiation_found->delete();
$this->refreshInvitations();
$this->dispatch('success', 'Invitation revoked.');
} catch (\Exception $e) {
return $this->dispatch('error', 'Invitation not found.');
}
$initiation_found->delete();
$this->refreshInvitations();
$this->dispatch('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
$this->invitations = TeamInvitation::ownedByCurrentTeam()->get();
}
}

View File

@@ -41,6 +41,9 @@ class InviteLink extends Component
{
try {
$this->validate();
if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
throw new \Exception('Admins cannot invite owners.');
}
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');

View File

@@ -12,29 +12,57 @@ class Member extends Component
public function makeAdmin()
{
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
$this->dispatch('reloadWindow');
try {
if (! auth()->user()->isAdmin()) {
throw new \Exception('You are not authorized to perform this action.');
}
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
$this->dispatch('reloadWindow');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function makeOwner()
{
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
$this->dispatch('reloadWindow');
try {
if (! auth()->user()->isOwner()) {
throw new \Exception('You are not authorized to perform this action.');
}
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']);
$this->dispatch('reloadWindow');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function makeReadonly()
{
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
$this->dispatch('reloadWindow');
try {
if (! auth()->user()->isAdmin()) {
throw new \Exception('You are not authorized to perform this action.');
}
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
$this->dispatch('reloadWindow');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function remove()
{
$this->member->teams()->detach(currentTeam());
Cache::forget("team:{$this->member->id}");
Cache::remember('team:'.$this->member->id, 3600, function () {
return $this->member->teams()->first();
});
$this->dispatch('reloadWindow');
try {
if (! auth()->user()->isAdmin()) {
throw new \Exception('You are not authorized to perform this action.');
}
$this->member->teams()->detach(currentTeam());
Cache::forget("team:{$this->member->id}");
Cache::remember('team:'.$this->member->id, 3600, function () {
return $this->member->teams()->first();
});
$this->dispatch('reloadWindow');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
}

View File

@@ -1400,13 +1400,13 @@ class Application extends BaseModel
return [];
}
public function getMetrics(int $mins = 5)
public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -1415,14 +1415,33 @@ class Application extends BaseModel
}
throw new \Exception($error);
}
$metrics = str($metrics)->explode("\n")->skip(1)->all();
$parsedCollection = collect($metrics)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
[$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
$cpu_usage_percent = number_format($cpu_usage_percent, 2);
$metrics = json_decode($metrics, true);
$parsedCollection = collect($metrics)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['percent']];
});
return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
});
return $parsedCollection->toArray();
}
}
public function getMemoryMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
if ($error == 'Unauthorized') {
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
}
throw new \Exception($error);
}
$metrics = json_decode($metrics, true);
$parsedCollection = collect($metrics)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['used']];
});
return $parsedCollection->toArray();
@@ -1459,7 +1478,7 @@ class Application extends BaseModel
return $config;
}
public function setConfig($config)
{
$validator = Validator::make(['config' => $config], [

View File

@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
protected $appends = ['real_value', 'is_shared'];
protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
@@ -74,6 +74,9 @@ class EnvironmentVariable extends Model
'version' => config('version'),
]);
});
static::saving(function (EnvironmentVariable $environmentVariable) {
$environmentVariable->updateIsShared();
});
}
public function service()
@@ -130,6 +133,13 @@ class EnvironmentVariable extends Model
);
}
protected function isReallyRequired(): Attribute
{
return Attribute::make(
get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
);
}
protected function isShared(): Attribute
{
return Attribute::make(
@@ -210,4 +220,11 @@ class EnvironmentVariable extends Model
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
protected function updateIsShared(): void
{
$type = str($this->value)->after('{{')->before('.')->value;
$isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
$this->is_shared = $isShared;
}
}

View File

@@ -31,6 +31,11 @@ class GithubApp extends BaseModel
});
}
public static function ownedByCurrentTeam()
{
return GithubApp::whereTeamId(currentTeam()->id);
}
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();

View File

@@ -9,6 +9,11 @@ class GitlabApp extends BaseModel
'app_secret',
];
public static function ownedByCurrentTeam()
{
return GitlabApp::whereTeamId(currentTeam()->id);
}
public function applications()
{
return $this->morphMany(Application::class, 'source');

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -21,8 +22,23 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
'sentinel_token' => 'encrypted',
];
protected static function booted(): void
{
static::updated(function ($settings) {
if ($settings->isDirty('helper_version')) {
Server::chunkById(100, function ($servers) {
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
});
}
});
}
public function fqdn(): Attribute
{
return Attribute::make(
@@ -86,16 +102,16 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]";
}
public function helperVersion(): Attribute
{
return Attribute::make(
get: function ($value) {
if (isDev()) {
return 'latest';
}
// public function helperVersion(): Attribute
// {
// return Attribute::make(
// get: function ($value) {
// if (isDev()) {
// return 'latest';
// }
return $value;
}
);
}
// return $value;
// }
// );
// }
}

View File

@@ -51,7 +51,6 @@ class ScheduledDatabaseBackup extends BaseModel
}
}
return null;
}
}

View File

@@ -3,10 +3,15 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\CheckAndStartSentinelJob;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
@@ -43,7 +48,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
use SchemalessAttributesTrait;
use SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -58,6 +63,7 @@ class Server extends BaseModel
$payload['ip'] = str($server->ip)->trim();
}
$server->forceFill($payload);
});
static::created(function ($server) {
ServerSetting::create([
@@ -95,7 +101,8 @@ class Server extends BaseModel
}
}
});
static::deleting(function ($server) {
static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -103,12 +110,15 @@ class Server extends BaseModel
});
}
public $casts = [
protected $casts = [
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
'delete_unused_networks' => 'boolean',
'unreachable_notification_sent' => 'boolean',
'is_build_server' => 'boolean',
'force_disabled' => 'boolean',
];
protected $schemalessAttributes = [
@@ -127,6 +137,11 @@ class Server extends BaseModel
protected $guarded = [];
public function type()
{
return 'server';
}
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -209,10 +224,13 @@ respond 404
1 => 'https',
],
'service' => 'noop',
'rule' => 'HostRegexp(`{catchall:.*}`)',
'rule' => 'HostRegexp(`.+`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp@file',
0 => 'redirect-regexp',
],
],
],
@@ -507,24 +525,48 @@ $schema://$host {
public function forceEnableServer()
{
$this->settings->update([
'force_disabled' => false,
]);
$this->settings->force_disabled = false;
$this->settings->save();
}
public function forceDisableServer()
{
$this->settings->update([
'force_disabled' => true,
]);
$this->settings->force_disabled = true;
$this->settings->save();
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
public function sentinelHeartbeat(bool $isReset = false)
{
$this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
$this->save();
}
/**
* Get the wait time for Sentinel to push before performing an SSH check.
*
* @return int The wait time in seconds.
*/
public function waitBeforeDoingSshCheck(): int
{
$wait = $this->settings->sentinel_push_interval_seconds * 3;
if ($wait < 120) {
$wait = 120;
}
return $wait;
}
public function isSentinelLive()
{
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
}
public function isSentinelEnabled()
{
return $this->isMetricsEnabled() || $this->isServerApiEnabled();
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
}
public function isMetricsEnabled()
@@ -534,49 +576,19 @@ $schema://$host {
public function isServerApiEnabled()
{
return $this->settings->is_server_api_enabled;
}
public function checkServerApi()
{
if ($this->isServerApiEnabled()) {
$server_ip = $this->ip;
if (isDev()) {
if ($this->id === 0) {
$server_ip = 'localhost';
}
}
$command = "curl -s http://{$server_ip}:12172/api/health";
$process = Process::timeout(5)->run($command);
if ($process->failed()) {
ray($process->exitCode(), $process->output(), $process->errorOutput());
throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
}
}
return $this->settings->is_sentinel_enabled;
}
public function checkSentinel()
{
// ray("Checking sentinel on server: {$this->name}");
if ($this->isSentinelEnabled()) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
// ray('Sentinel is not running, starting it...');
PullSentinelImageJob::dispatch($this);
} else {
// ray('Sentinel is running');
}
}
CheckAndStartSentinelJob::dispatch($this);
}
public function getCpuMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -585,17 +597,12 @@ $schema://$host {
}
throw new \Exception($error);
}
$cpu = str($cpu)->explode("\n")->skip(1)->all();
$parsedCollection = collect($cpu)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
[$time, $cpu_usage_percent] = explode(',', trim($line));
$cpu_usage_percent = number_format($cpu_usage_percent, 0);
return [(int) $time, (float) $cpu_usage_percent];
});
$cpu = json_decode($cpu, true);
$parsedCollection = collect($cpu)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['percent']];
});
return $parsedCollection->toArray();
return $parsedCollection;
}
}
@@ -603,7 +610,7 @@ $schema://$host {
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
if (str($memory)->contains('error')) {
$error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -612,89 +619,19 @@ $schema://$host {
}
throw new \Exception($error);
}
$memory = str($memory)->explode("\n")->skip(1)->all();
$parsedCollection = collect($memory)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
[$time, $used, $free, $usedPercent] = explode(',', trim($line));
$usedPercent = number_format($usedPercent, 0);
return [(int) $time, (float) $usedPercent];
});
$memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['usedPercent']];
});
return $parsedCollection->toArray();
}
}
public function isServerReady(int $tries = 3)
{
if ($this->skipServer()) {
return false;
}
$serverUptimeCheckNumber = $this->unreachable_count;
if ($this->unreachable_count < $tries) {
$serverUptimeCheckNumber = $this->unreachable_count + 1;
}
if ($this->unreachable_count > $tries) {
$serverUptimeCheckNumber = $tries;
}
$serverUptimeCheckNumberMax = $tries;
// ray('server: ' . $this->name);
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
['uptime' => $uptime] = $this->validateConnection();
if ($uptime) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
}
return true;
} else {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
// Reached max number of retries
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
// $this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
if ($this->settings->is_reachable === true) {
$this->settings()->update([
'is_reachable' => false,
]);
}
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
} else {
$this->update([
'unreachable_count' => $this->unreachable_count + 1,
]);
}
return false;
}
}
public function getDiskUsage(): ?string
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
// return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
@@ -974,7 +911,8 @@ $schema://$host {
public function isProxyShouldRun()
{
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
// TODO: Do we need "|| $this->proxy->force_stop" here?
if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
@@ -1038,39 +976,111 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
public function serverStatus(): bool
{
if ($this->status() === false) {
return false;
}
if ($this->isFunctional() === false) {
return false;
}
return true;
}
public function status(): bool
{
if ($this->skipServer()) {
return false;
}
['uptime' => $uptime] = $this->validateConnection(false);
if ($uptime === false) {
foreach ($this->applications() as $application) {
$application->status = 'exited';
$application->save();
}
foreach ($this->databases() as $database) {
$database->status = 'exited';
$database->save();
}
foreach ($this->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->status = 'exited';
$app->save();
}
foreach ($dbs as $db) {
$db->status = 'exited';
$db->save();
}
}
return false;
}
return true;
}
public function isReachableChanged()
{
$this->refresh();
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable;
loggy('Server setting is_reachable changed to '.$isReachable.' for server '.$this->id.'. Unreachable notification sent: '.$unreachableNotificationSent);
// If the server is reachable, send the reachable notification if it was sent before
if ($isReachable === true) {
if ($unreachableNotificationSent === true) {
$this->sendReachableNotification();
}
} else {
// If the server is unreachable, send the unreachable notification if it was not sent before
if ($unreachableNotificationSent === false) {
$this->sendUnreachableNotification();
}
}
}
public function sendReachableNotification()
{
$this->unreachable_notification_sent = false;
$this->save();
$this->refresh();
$this->team->notify(new Reachable($this));
}
public function sendUnreachableNotification()
{
$this->unreachable_notification_sent = true;
$this->save();
$this->refresh();
$this->team->notify(new Unreachable($this));
}
public function validateConnection($isManualCheck = true)
{
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
// ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
$server = Server::find($this->id);
if (! $server) {
return ['uptime' => false, 'error' => 'Server not found.'];
}
if ($server->skipServer()) {
if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
// Make sure the private key is stored
if ($server->privateKey) {
$server->privateKey->storeInFileSystem();
if ($this->privateKey) {
$this->privateKey->storeInFileSystem();
}
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,
]);
$server->update([
'unreachable_count' => 0,
]);
if (data_get($server, 'unreachable_notification_sent') === true) {
$server->update(['unreachable_notification_sent' => false]);
instant_remote_process(['ls /'], $this);
if ($this->settings->is_reachable === false) {
$this->settings->is_reachable = true;
$this->settings->save();
}
return ['uptime' => true, 'error' => null];
} catch (\Throwable $e) {
$server->settings()->update([
'is_reachable' => false,
]);
if ($this->settings->is_reachable === true) {
$this->settings->is_reachable = false;
$this->settings->save();
}
return ['uptime' => false, 'error' => $e->getMessage()];
}
@@ -1225,4 +1235,22 @@ $schema://$host {
{
return str($this->ip)->contains(':');
}
public function restartSentinel(bool $async = true): void
{
try {
if ($async) {
StartSentinel::dispatch($this, true);
} else {
StartSentinel::run($this, true);
}
} catch (\Throwable $e) {
loggy('Error restarting Sentinel: '.$e->getMessage());
}
}
public function url()
{
return base_url().'/server/'.$this->uuid;
}
}

View File

@@ -24,7 +24,7 @@ use OpenApi\Attributes as OA;
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
'is_server_api_enabled' => ['type' => 'boolean'],
'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
@@ -35,9 +35,9 @@ use OpenApi\Attributes as OA;
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
'metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'],
'sentinel_metrics_history_days' => ['type' => 'integer'],
'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
@@ -53,8 +53,78 @@ class ServerSetting extends Model
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
'is_reachable' => 'boolean',
'is_usable' => 'boolean',
];
protected static function booted()
{
static::creating(function ($setting) {
try {
if (str($setting->sentinel_token)->isEmpty()) {
$setting->generateSentinelToken(save: false);
}
if (str($setting->sentinel_custom_url)->isEmpty()) {
$setting->generateSentinelUrl(save: false);
}
} catch (\Throwable $e) {
loggy('Error creating server setting: '.$e->getMessage());
}
});
static::updated(function ($settings) {
if (
$settings->isDirty('sentinel_token') ||
$settings->isDirty('sentinel_custom_url') ||
$settings->isDirty('sentinel_metrics_refresh_rate_seconds') ||
$settings->isDirty('sentinel_metrics_history_days') ||
$settings->isDirty('sentinel_push_interval_seconds')
) {
$settings->server->restartSentinel();
}
if ($settings->isDirty('is_reachable')) {
$settings->server->isReachableChanged();
}
});
}
public function generateSentinelToken(bool $save = true)
{
$data = [
'server_uuid' => $this->server->uuid,
];
$token = json_encode($data);
$encrypted = encrypt($token);
$this->sentinel_token = $encrypted;
if ($save) {
$this->save();
}
return $token;
}
public function generateSentinelUrl(bool $save = true)
{
$domain = null;
$settings = InstanceSettings::get();
if ($this->server->isLocalhost()) {
$domain = 'http://host.docker.internal:8000';
} elseif ($settings->fqdn) {
$domain = $settings->fqdn;
} elseif ($settings->public_ipv4) {
$domain = 'http://'.$settings->public_ipv4.':8000';
} elseif ($settings->public_ipv6) {
$domain = 'http://'.$settings->public_ipv6.':8000';
}
$this->sentinel_custom_url = $domain;
loggy('Sentinel URL: '.$domain);
if ($save) {
$this->save();
}
return $domain;
}
public function server()
{
return $this->belongsTo(Server::class);

View File

@@ -297,7 +297,7 @@ class Service extends BaseModel
'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'),
'rules' => 'required',
'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
],
]);
}
@@ -319,7 +319,7 @@ class Service extends BaseModel
if ($password) {
$data = $data->merge([
'Password' => [
'key' => 'LABEL_STUDIO_PASSWORD',
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -359,7 +359,7 @@ class Service extends BaseModel
if ($email) {
$data = $data->merge([
'Admin Email' => [
'key' => 'LANGFUSE_INIT_USER_EMAIL',
'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
@@ -370,7 +370,7 @@ class Service extends BaseModel
if ($password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'LANGFUSE_INIT_USER_PASSWORD',
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -384,7 +384,7 @@ class Service extends BaseModel
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
$data = $data->merge([
'Email' => [
'key' => 'IN_USER_EMAIL',
'key' => data_get($email, 'key'),
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
@@ -392,7 +392,7 @@ class Service extends BaseModel
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
$data = $data->merge([
'Password' => [
'key' => 'IN_PASSWORD',
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -487,7 +487,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'SERVICE_PASSWORD_TOLGEE',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -534,7 +534,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'SERVICE_PASSWORD_UNLEASH',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -557,7 +557,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => 'GF_SECURITY_ADMIN_PASSWORD',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -919,7 +919,7 @@ class Service extends BaseModel
if ($admin_user) {
$data = $data->merge([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
@@ -929,7 +929,7 @@ class Service extends BaseModel
if ($admin_password) {
$data = $data->merge([
'Password' => [
'key' => 'SERVICE_PASSWORD_ADMIN',
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -939,7 +939,7 @@ class Service extends BaseModel
if ($admin_email) {
$data = $data->merge([
'Email' => [
'key' => 'ADMIN_EMAIL',
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
@@ -997,8 +997,8 @@ class Service extends BaseModel
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -1232,7 +1232,6 @@ class Service extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
@@ -1316,4 +1315,20 @@ class Service extends BaseModel
return $networks;
}
protected function isDeployable(): Attribute
{
return Attribute::make(
get: function () {
$envs = $this->environment_variables()->where('is_required', true)->get();
foreach ($envs as $env) {
if ($env->is_really_required) {
return false;
}
}
return true;
}
);
}
}

Some files were not shown because too many files have changed in this diff Show More