Merge branch 'next' into proxy-fixes

This commit is contained in:
Andras Bacsai
2024-10-21 15:08:13 +02:00
committed by GitHub
212 changed files with 6844 additions and 1378 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_NAME=Coolify
APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80= APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80=
DB_USERNAME=coolify
DB_PASSWORD=coolify DB_PASSWORD=coolify
REDIS_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: on:
push: push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
env: env:
REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper" IMAGE_NAME: "coollabsio/coolify-helper"
jobs: jobs:
@@ -19,25 +20,36 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-helper/Dockerfile file: docker/coolify-helper/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
aarch64: aarch64:
@@ -47,27 +59,39 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-helper/Dockerfile file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -75,25 +99,42 @@ jobs:
packages: write packages: write
needs: [ amd64, aarch64 ] needs: [ amd64, aarch64 ]
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3 - name: Login to ${{ env.GITHUB_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: | 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 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -1,4 +1,4 @@
name: Coolify Helper Image (v4) name: Coolify Helper Image
on: on:
push: push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
env: env:
REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper" IMAGE_NAME: "coollabsio/coolify-helper"
jobs: jobs:
@@ -19,25 +20,36 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-helper/Dockerfile file: docker/coolify-helper/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
aarch64: aarch64:
@@ -47,25 +59,36 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-helper/Dockerfile file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
merge-manifest: merge-manifest:
@@ -75,25 +98,43 @@ jobs:
packages: write packages: write
needs: [ amd64, aarch64 ] needs: [ amd64, aarch64 ]
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up QEMU - uses: docker/setup-buildx-action@v3
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: | 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 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: 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: on:
push: push:
branches: [ "next" ] branches: [ "next" ]
paths: paths:
- .github/workflows/coolify-realtime.yml - .github/workflows/coolify-realtime-next.yml
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js - docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json - docker/coolify-realtime/package.json
- docker/coolify-realtime/soketi-entrypoint.sh - docker/coolify-realtime/soketi-entrypoint.sh
env: env:
REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime" IMAGE_NAME: "coollabsio/coolify-realtime"
jobs: jobs:
@@ -22,27 +23,39 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-realtime/Dockerfile file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
aarch64: aarch64:
runs-on: [ self-hosted, arm64 ] runs-on: [ self-hosted, arm64 ]
permissions: permissions:
@@ -50,27 +63,39 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-realtime/Dockerfile file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -78,26 +103,44 @@ jobs:
packages: write packages: write
needs: [ amd64, aarch64 ] needs: [ amd64, aarch64 ]
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up QEMU - uses: docker/setup-buildx-action@v3
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: | 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 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: 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: on:
push: push:
@@ -11,7 +11,8 @@ on:
- docker/coolify-realtime/soketi-entrypoint.sh - docker/coolify-realtime/soketi-entrypoint.sh
env: env:
REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime" IMAGE_NAME: "coollabsio/coolify-realtime"
jobs: jobs:
@@ -22,27 +23,39 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-realtime/Dockerfile file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
aarch64: aarch64:
runs-on: [ self-hosted, arm64 ] runs-on: [ self-hosted, arm64 ]
permissions: permissions:
@@ -50,27 +63,39 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: with:
no-cache: true
context: . context: .
file: docker/coolify-realtime/Dockerfile file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true 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: | labels: |
coolify.managed=true coolify.managed=true
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -78,25 +103,43 @@ jobs:
packages: write packages: write
needs: [ amd64, aarch64 ] needs: [ amd64, aarch64 ]
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up QEMU - uses: docker/setup-buildx-action@v3
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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 - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT 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: | 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 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: 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: on:
push: push:
branches: [ "main", "next" ] branches: [ "next" ]
paths: paths:
- .github/workflows/coolify-testing-host.yml - .github/workflows/coolify-testing-host.yml
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
env: env:
REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-testing-host" IMAGE_NAME: "coollabsio/coolify-testing-host"
jobs: jobs:
@@ -19,21 +20,34 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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: with:
no-cache: true
context: . context: .
file: docker/testing-host/Dockerfile file: docker/testing-host/Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true 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: aarch64:
runs-on: [ self-hosted, arm64 ] runs-on: [ self-hosted, arm64 ]
permissions: permissions:
@@ -41,21 +55,34 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Login to ghcr.io
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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: with:
no-cache: true
context: . context: .
file: docker/testing-host/Dockerfile file: docker/testing-host/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true 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: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -63,21 +90,36 @@ jobs:
packages: write packages: write
needs: [ amd64, aarch64 ] needs: [ amd64, aarch64 ]
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up QEMU - uses: docker/setup-buildx-action@v3
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} 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: | 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 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: 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) public function handle(Application $application, bool $is_json = false)
{ {
ray()->clearAll(); ray()->clearAll();
return $application->generateConfig(is_json: $is_json); return $application->generateConfig(is_json: $is_json);
} }
} }

View File

@@ -21,8 +21,6 @@ class StartRedis
{ {
$this->database = $database; $this->database = $database;
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
@@ -37,6 +35,8 @@ class StartRedis
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$this->add_custom_redis(); $this->add_custom_redis();
$startCommand = $this->buildStartCommand();
$docker_compose = [ $docker_compose = [
'services' => [ 'services' => [
$container_name => [ $container_name => [
@@ -105,7 +105,6 @@ class StartRedis
'target' => '/usr/local/etc/redis/redis.conf', 'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true, '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 // Add custom docker run options
@@ -160,12 +159,26 @@ class StartRedis
private function generate_environment_variables() private function generate_environment_variables()
{ {
$environment_variables = collect(); $environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) { foreach ($this->database->runtime_environment_variables as $env) {
if ($env->is_shared) {
$environment_variables->push("$env->key=$env->real_value"); $environment_variables->push("$env->key=$env->real_value");
if ($env->key === 'REDIS_PASSWORD') {
$this->database->update(['redis_password' => $env->real_value]);
} }
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { if ($env->key === 'REDIS_USERNAME') {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); $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); add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
@@ -173,6 +186,27 @@ class StartRedis
return $environment_variables->all(); 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() private function add_custom_redis()
{ {
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {

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>.'); 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); ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
$dockerVersion = '24.0'; $dockerVersion = '26.0';
$config = base64_encode('{ $config = base64_encode('{
"log-driver": "json-file", "log-driver": "json-file",
"log-opts": { "log-opts": {

View File

@@ -9,18 +9,48 @@ class StartSentinel
{ {
use AsAction; use AsAction;
public function handle(Server $server, $version = 'latest', bool $restart = false) public function handle(Server $server, $version = 'next', bool $restart = false)
{ {
if ($restart) { if ($restart) {
StopSentinel::run($server); StopSentinel::run($server);
} }
$metrics_history = $server->settings->metrics_history_days; $metrics_history = data_get($server, 'settings.sentinel_metrics_history_days');
$refresh_rate = $server->settings->metrics_refresh_rate_seconds; $refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
$token = $server->settings->metrics_token; $push_interval = data_get($server, 'settings.sentinel_push_interval_seconds');
$token = data_get($server, 'settings.sentinel_token');
$endpoint = data_get($server, 'settings.sentinel_custom_url');
$mount_dir = '/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' => $push_interval,
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
];
if (isDev()) {
// data_set($environments, 'DEBUG', 'true');
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
// $image = 'sentinel';
}
$docker_environments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
$docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/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 $image";
instant_remote_process([ 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", 'docker rm -f coolify-sentinel || true',
'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', "mkdir -p $mount_dir",
'chmod -R 700 /data/coolify/metrics /data/coolify/logs', $docker_command,
], $server, true); "chown -R 9999:root $mount_dir",
"chmod -R 700 $mount_dir",
], $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) public function handle(Server $server)
{ {
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
$server->sentinelHeartbeat(isReset: true);
} }
} }

View File

@@ -3,16 +3,15 @@
namespace App\Console; namespace App\Console;
use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN; use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob; use App\Jobs\ServerCheckJob;
use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob; use App\Jobs\UpdateCoolifyJob;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
@@ -20,6 +19,7 @@ use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@@ -44,7 +44,7 @@ class Kernel extends ConsoleKernel
$schedule->command('telescope:prune')->daily(); $schedule->command('telescope:prune')->daily();
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer(); $schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -80,7 +80,7 @@ class Kernel extends ConsoleKernel
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
} }
} }
$schedule->job(new PullHelperImageJob) $schedule->job(new CheckHelperImageJob)
->cron($settings->update_check_frequency) ->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone) ->timezone($settings->instance_timezone)
->onOneServer(); ->onOneServer();
@@ -115,7 +115,10 @@ class Kernel extends ConsoleKernel
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); $servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$last_sentinel_update = $server->sentinel_updated_at;
if (Carbon::parse($last_sentinel_update)->isBefore(now()->subMinutes(4))) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
}
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer(); // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) { if ($server->settings->force_docker_cleanup) {

View File

@@ -1579,11 +1579,16 @@ class ApplicationsController extends Controller
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
$instantDeploy = $request->instant_deploy; $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)) { if (isset($isStatic)) {
$application->settings->is_build_server_enabled = $use_build_server; $application->settings->is_static = $isStatic;
$application->settings->save(); $application->settings->save();
} }

View File

@@ -160,7 +160,7 @@ class OtherController extends Controller
#[OA\Get( #[OA\Get(
summary: 'Healthcheck', summary: 'Healthcheck',
description: 'Healthcheck endpoint.', description: 'Healthcheck endpoint.',
path: '/healthcheck', path: '/health',
operationId: 'healthcheck', operationId: 'healthcheck',
responses: [ responses: [
new OA\Response( new OA\Response(

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Server\DeleteServer;
use App\Actions\Server\ValidateServer; use App\Actions\Server\ValidateServer;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
@@ -23,7 +24,7 @@ class ServersController extends Controller
return serializeApiResponse($settings); return serializeApiResponse($settings);
} }
$settings = $settings->makeHidden([ $settings = $settings->makeHidden([
'metrics_token', 'sentinel_token',
]); ]);
return serializeApiResponse($settings); 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); return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
} }
$server->delete(); $server->delete();
DeleteServer::dispatch($server);
return response()->json(['message' => 'Server deleted.']); return response()->json(['message' => 'Server deleted.']);
} }

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; $network = $this->database->destination->network;
} }
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName(); $fullImageName = $this->getFullImageName();
if (isDev()) { 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 private function getFullImageName(): string
{ {
$settings = instanceSettings(); $settings = instanceSettings();

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{ {
@@ -17,28 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000; public $timeout = 1000;
public function __construct() {} public function __construct(public Server $server) {}
public function handle(): void public function handle(): void
{ {
try { try {
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); $helperImage = config('coolify.helper_image');
if ($response->successful()) { $latest_version = instanceSettings()->helper_version;
$versions = $response->json(); instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
$settings = instanceSettings();
$latest_version = data_get($versions, 'coolify.helper.version');
$current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) {
// New version available
// $helperImage = config('coolify.helper_image');
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]);
}
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e; throw $e;
} }
} }

View File

@@ -0,0 +1,407 @@
<?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 Illuminate\Bus\Queueable;
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 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()
{
try {
if (! $this->data) {
throw new \Exception('No data provided');
}
$data = collect($this->data);
$this->serverStatus();
$this->server->sentinelHeartbeat();
$this->containers = collect(data_get($data, 'containers'));
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 serverStatus()
{
if ($this->server->isFunctional() === false) {
throw new \Exception('Server is not ready.');
}
if ($this->server->status() === false) {
throw new \Exception('Server is not reachable.');
}
}
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);
}
} 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);
} 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

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

View File

@@ -241,7 +241,6 @@ class General extends Component
} }
} }
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
@@ -314,7 +313,7 @@ class General extends Component
public function set_redirect() public function set_redirect()
{ {
try { try {
$has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count(); $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') { if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).'); $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
@@ -335,9 +334,15 @@ class General extends Component
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']); Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
}); });
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$warning = sslipDomainWarning($this->application->fqdn);
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
$this->resetDefaultLabels(); $this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) { if ($this->application->isDirty('redirect')) {
@@ -403,17 +408,19 @@ class General extends Component
} }
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
$showToaster && $this->dispatch('success', 'Application settings updated!'); $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn'); $originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) { if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn; $this->application->fqdn = $originalFqdn;
} }
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
} }
} }
public function downloadConfig() public function downloadConfig()
{ {
$config = GenerateConfig::run($this->application, true); $config = GenerateConfig::run($this->application, true);
@@ -423,7 +430,7 @@ class General extends Component
echo $config; echo $config;
}, $fileName, [ }, $fileName, [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename=' . $fileName, 'Content-Disposition' => 'attachment; filename='.$fileName,
]); ]);
} }
} }

View File

@@ -11,12 +11,21 @@ use Livewire\Component;
class General extends Component class General extends Component
{ {
protected $listeners = ['refresh']; protected $listeners = [
'envsUpdated' => 'refresh',
'refresh',
];
public Server $server; public Server $server;
public StandaloneRedis $database; public StandaloneRedis $database;
public string $redis_username;
public string $redis_password;
public string $redis_version;
public ?string $db_url = null; public ?string $db_url = null;
public ?string $db_url_public = null; public ?string $db_url_public = null;
@@ -25,33 +34,33 @@ class General extends Component
'database.name' => 'required', 'database.name' => 'required',
'database.description' => 'nullable', 'database.description' => 'nullable',
'database.redis_conf' => 'nullable', 'database.redis_conf' => 'nullable',
'database.redis_password' => 'required',
'database.image' => 'required', 'database.image' => 'required',
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean', 'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable', 'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
'database.description' => 'Description', 'database.description' => 'Description',
'database.redis_conf' => 'Redis Configuration', 'database.redis_conf' => 'Redis Configuration',
'database.redis_password' => 'Redis Password',
'database.image' => 'Image', 'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping', 'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options', 'database.custom_docker_run_options' => 'Custom Docker Options',
'redis_username' => 'Redis Username',
'redis_password' => 'Redis Password',
]; ];
public function mount() 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->server = data_get($this->database, 'destination.server');
$this->refreshView();
} }
public function instantSaveAdvanced() public function instantSaveAdvanced()
@@ -75,13 +84,24 @@ class General extends Component
{ {
try { try {
$this->validate(); $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->database->save();
$this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'Database updated.');
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->dispatch('refreshEnvs');
} }
} }
@@ -119,10 +139,25 @@ class General extends Component
public function refresh(): void public function refresh(): void
{ {
$this->database->refresh(); $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() public function render()
{ {
return view('livewire.project.database.redis.general'); 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 class DeleteEnvironment extends Component
{ {
public array $parameters;
public int $environment_id; public int $environment_id;
public bool $disabled = false; public bool $disabled = false;
public string $environmentName = ''; public string $environmentName = '';
public array $parameters;
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); try {
$this->environmentName = Environment::findOrFail($this->environment_id)->name; $this->environmentName = Environment::findOrFail($this->environment_id)->name;
$this->parameters = get_route_parameters();
} catch (\Exception $e) {
return handleError($e, $this);
}
} }
public function delete() public function delete()
@@ -30,7 +34,7 @@ class DeleteEnvironment extends Component
if ($environment->isEmpty()) { if ($environment->isEmpty()) {
$environment->delete(); $environment->delete();
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
} }
return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); return $this->dispatch('error', 'Environment has defined resources, please delete them first.');

View File

@@ -18,7 +18,11 @@ class Index extends Component
public function mount() public function mount()
{ {
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); $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(); $this->servers = Server::ownedByCurrentTeam()->count();
} }

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ class EditDomain extends Component
{ {
$this->application = ServiceApplication::find($this->applicationId); $this->application = ServiceApplication::find($this->applicationId);
} }
public function submit() public function submit()
{ {
try { try {
@@ -28,9 +29,14 @@ class EditDomain extends Component
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']); Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
}); });
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $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); check_domain_usage(resource: $this->application);
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
@@ -38,7 +44,7 @@ class EditDomain extends Component
if (str($this->application->fqdn)->contains(',')) { 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.'); $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 { } else {
$this->dispatch('success', 'Service saved.'); ! $warning && $this->dispatch('success', 'Service saved.');
} }
$this->application->service->parse(); $this->application->service->parse();
$this->dispatch('refresh'); $this->dispatch('refresh');
@@ -48,6 +54,7 @@ class EditDomain extends Component
if ($originalFqdn !== $this->application->fqdn) { if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn; $this->application->fqdn = $originalFqdn;
} }
return handleError($e, $this); return handleError($e, $this);
} }
} }

View File

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

View File

@@ -30,11 +30,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean', 'application.is_stripprefix_enabled' => 'nullable|boolean',
]; ];
public function updatedApplicationFqdn()
{
}
public function instantSave() public function instantSave()
{ {
$this->submit(); $this->submit();
@@ -82,10 +77,14 @@ class ServiceApplicationView extends Component
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
Url::fromString($domain, ['http', 'https']); Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
}); });
$this->application->fqdn = $this->application->fqdn->unique()->implode(','); $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); check_domain_usage(resource: $this->application);
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
@@ -93,7 +92,7 @@ class ServiceApplicationView extends Component
if (str($this->application->fqdn)->contains(',')) { 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.'); $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 { } else {
$this->dispatch('success', 'Service saved.'); ! $warning && $this->dispatch('success', 'Service saved.');
} }
$this->dispatch('generateDockerCompose'); $this->dispatch('generateDockerCompose');
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -101,6 +100,7 @@ class ServiceApplicationView extends Component
if ($originalFqdn !== $this->application->fqdn) { if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn; $this->application->fqdn = $originalFqdn;
} }
return handleError($e, $this); return handleError($e, $this);
} }
} }

View File

@@ -37,6 +37,7 @@ class Show extends Component
'env.is_literal' => 'required|boolean', 'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean', 'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable', 'env.real_value' => 'nullable',
'env.is_required' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -46,6 +47,7 @@ class Show extends Component
'env.is_multiline' => 'Multiline', 'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal', 'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once', 'env.is_shown_once' => 'Shown Once',
'env.is_required' => 'Required',
]; ];
public function refresh() public function refresh()
@@ -109,14 +111,14 @@ class Show extends Component
} else { } else {
$this->validate(); $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->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(); $this->serialize();
$this->env->save(); $this->env->save();
$this->dispatch('success', 'Environment variable updated.'); $this->dispatch('success', 'Environment variable updated.');

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
<?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.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.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,12 @@ class Charts extends Component
try { try {
$cpuMetrics = $this->server->getCpuMetrics($this->interval); $cpuMetrics = $this->server->getCpuMetrics($this->interval);
$memoryMetrics = $this->server->getMemoryMetrics($this->interval); $memoryMetrics = $this->server->getMemoryMetrics($this->interval);
$cpuMetrics = collect($cpuMetrics)->map(function ($metric) { // $cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
return [$metric[0], $metric[1]]; // return [$metric[0], $metric[1]];
}); // });
$memoryMetrics = collect($memoryMetrics)->map(function ($metric) { // $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
return [$metric[0], $metric[1]]; // return [$metric[0], $metric[1]];
}); // });
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [ $this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics, '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,7 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\DeleteServer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -28,6 +29,7 @@ class Delete extends Component
return; return;
} }
$this->server->delete(); $this->server->delete();
DeleteServer::dispatch($this->server);
return redirect()->route('server.index'); return redirect()->route('server.index');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -4,8 +4,6 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel; use App\Actions\Server\StopSentinel;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -46,25 +44,19 @@ class Form extends Component
'server.ip' => 'required', 'server.ip' => 'required',
'server.user' => 'required', 'server.user' => 'required',
'server.port' => 'required', 'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required|boolean', 'wildcard_domain' => 'nullable|url',
'server.settings.is_reachable' => 'required', 'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean', 'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => '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.is_metrics_enabled' => 'required|boolean',
'server.settings.metrics_token' => 'required', 'server.settings.sentinel_token' => 'required',
'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.metrics_history_days' => 'required|integer|min:1', 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url', 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
'server.settings.is_server_api_enabled' => 'required|boolean', 'server.settings.sentinel_custom_url' => 'nullable|url',
'server.settings.is_sentinel_enabled' => 'required|boolean',
'server.settings.server_timezone' => 'required|string|timezone', '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 = [ protected $validationAttributes = [
@@ -73,21 +65,18 @@ class Form extends Component
'server.ip' => 'IP address/Domain', 'server.ip' => 'IP address/Domain',
'server.user' => 'User', 'server.user' => 'User',
'server.port' => 'Port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'Is reachable', 'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager', 'server.settings.is_swarm_manager' => 'Swarm Manager',
'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server', '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.is_metrics_enabled' => 'Metrics',
'server.settings.metrics_token' => 'Metrics Token', 'server.settings.sentinel_token' => 'Metrics Token',
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History', 'server.settings.sentinel_metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API', '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.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) public function mount(Server $server)
@@ -95,10 +84,24 @@ class Form extends Component
$this->server = $server; $this->server = $server;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; }
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes; public function checkSyncStatus()
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks; {
$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) public function updated($field)
@@ -131,21 +134,35 @@ class Form extends Component
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
} }
public function checkPortForServerApi() public function updatedServerSettingsIsSentinelEnabled($value)
{ {
$this->validate();
$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 { try {
if ($this->server->settings->is_server_api_enabled === true) { StartSentinel::run($this->server);
$this->server->checkServerApi();
$this->dispatch('success', 'Server API is reachable.');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
}
public function updatedServerSettingsIsMetricsEnabled()
{
$this->restartSentinel();
}
public function instantSave() public function instantSave()
{ {
try { try {
$this->validate();
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
$this->validateServer(false); $this->validateServer(false);
@@ -153,33 +170,27 @@ class Form extends Component
$this->server->save(); $this->server->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow'); $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->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->server->settings->refresh();
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function restartSentinel() public function restartSentinel($notification = true)
{ {
try { try {
$this->validate();
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
$version = get_latest_sentinel_version(); $version = get_latest_sentinel_version();
StartSentinel::run($this->server, $version, true); StartSentinel::run($this->server, $version, true);
$this->dispatch('success', 'Sentinel restarted.'); if ($notification) {
$this->dispatch('success', 'Sentinel started.');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -236,16 +247,15 @@ class Form extends Component
} }
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
$this->server->settings->wildcard_domain = $this->wildcard_domain; $this->server->settings->wildcard_domain = $this->wildcard_domain;
if ($this->server->settings->force_docker_cleanup) { // if ($this->server->settings->force_docker_cleanup) {
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; // $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
} else { // } else {
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; // $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
} // }
$currentTimezone = $this->server->settings->getOriginal('server_timezone'); $currentTimezone = $this->server->settings->getOriginal('server_timezone');
$newTimezone = $this->server->settings->server_timezone; $newTimezone = $this->server->settings->server_timezone;
if ($currentTimezone !== $newTimezone || $currentTimezone === '') { if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
$this->server->settings->server_timezone = $newTimezone; $this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save();
} }
$this->server->settings->save(); $this->server->settings->save();
$this->server->save(); $this->server->save();
@@ -255,29 +265,4 @@ class Form extends Component
return handleError($e, $this); 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(); $this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -14,15 +13,29 @@ class ShowPrivateKey extends Component
public $parameters; public $parameters;
public function mount()
{
$this->parameters = get_route_parameters();
}
public function setPrivateKey($privateKeyId) public function setPrivateKey($privateKeyId)
{ {
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
try { try {
$privateKey = PrivateKey::findOrFail($privateKeyId); $this->server->update(['private_key_id' => $privateKeyId]);
$this->server->update(['private_key_id' => $privateKey->id]); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
$this->server->refresh(); if ($uptime) {
$this->dispatch('success', 'Private key updated successfully.'); $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) { } catch (\Exception $e) {
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
$this->server->validateConnection();
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage()); $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
} }
} }
@@ -33,18 +46,15 @@ class ShowPrivateKey extends Component
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } 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); $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; return;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->dispatch('refreshServerShow');
$this->server->refresh();
} }
} }
public function mount()
{
$this->parameters = get_route_parameters();
}
} }

View File

@@ -25,6 +25,10 @@ class Index extends Component
public string $update_check_frequency; public string $update_check_frequency;
public $timezones;
public bool $disable_two_step_confirmation;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server; protected Server $server;
@@ -38,6 +42,8 @@ class Index extends Component
'settings.instance_name' => 'nullable', 'settings.instance_name' => 'nullable',
'settings.allowed_ips' => 'nullable', 'settings.allowed_ips' => 'nullable',
'settings.is_auto_update_enabled' => 'boolean', 'settings.is_auto_update_enabled' => 'boolean',
'settings.public_ipv4' => 'nullable',
'settings.public_ipv6' => 'nullable',
'auto_update_frequency' => 'string', 'auto_update_frequency' => 'string',
'update_check_frequency' => 'string', 'update_check_frequency' => 'string',
'settings.instance_timezone' => 'required|string|timezone', 'settings.instance_timezone' => 'required|string|timezone',
@@ -51,16 +57,18 @@ class Index extends Component
'settings.custom_dns_servers' => 'Custom DNS servers', 'settings.custom_dns_servers' => 'Custom DNS servers',
'settings.allowed_ips' => 'Allowed IPs', 'settings.allowed_ips' => 'Allowed IPs',
'settings.is_auto_update_enabled' => 'Auto Update Enabled', 'settings.is_auto_update_enabled' => 'Auto Update Enabled',
'settings.public_ipv4' => 'IPv4',
'settings.public_ipv6' => 'IPv6',
'auto_update_frequency' => 'Auto Update Frequency', 'auto_update_frequency' => 'Auto Update Frequency',
'update_check_frequency' => 'Update Check Frequency', 'update_check_frequency' => 'Update Check Frequency',
'settings.instance_timezone' => 'Instance Timezone',
]; ];
public $timezones;
public function mount() public function mount()
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
$this->settings = instanceSettings(); $this->settings = instanceSettings();
loggy($this->settings);
$this->do_not_track = $this->settings->do_not_track; $this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -69,6 +77,7 @@ class Index extends Component
$this->auto_update_frequency = $this->settings->auto_update_frequency; $this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency; $this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
} else { } else {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -83,6 +92,7 @@ class Index extends Component
$this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency; $this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency; $this->settings->update_check_frequency = $this->update_check_frequency;
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
$this->settings->save(); $this->settings->save();
$this->dispatch('success', 'Settings updated!'); $this->dispatch('success', 'Settings updated!');
} }
@@ -170,15 +180,16 @@ class Index extends Component
} }
} }
public function updatedSettingsInstanceTimezone($value)
{
$this->settings->instance_timezone = $value;
$this->settings->save();
$this->dispatch('success', 'Instance timezone updated.');
}
public function render() public function render()
{ {
return view('livewire.settings.index'); return view('livewire.settings.index');
} }
public function toggleTwoStepConfirmation()
{
$this->settings->disable_two_step_confirmation = true;
$this->settings->save();
$this->disable_two_step_confirmation = true;
$this->dispatch('success', 'Two step confirmation has been disabled.');
}
} }

View File

@@ -23,7 +23,7 @@ class Create extends Component
public function mount() 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() public function createGitHubApp()

View File

@@ -1400,13 +1400,21 @@ class Application extends BaseModel
return []; return [];
} }
public function getMetrics(int $mins = 5) public function getCpuMetrics(int $mins = 5)
{ {
$server = $this->destination->server; $server = $this->destination->server;
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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); if (isDev() && $server->id === 0) {
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from");
if ($process->failed()) {
throw new \Exception($process->errorOutput());
}
$metrics = $process->output();
} else {
$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')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -1415,14 +1423,41 @@ class Application extends BaseModel
} }
throw new \Exception($error); throw new \Exception($error);
} }
$metrics = str($metrics)->explode("\n")->skip(1)->all(); $metrics = json_decode($metrics, true);
$parsedCollection = collect($metrics)->flatMap(function ($item) { $parsedCollection = collect($metrics)->map(function ($metric) {
return collect(explode("\n", trim($item)))->map(function ($line) { return [(int) $metric['time'], (float) $metric['percent']];
[$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
$cpu_usage_percent = number_format($cpu_usage_percent, 2);
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();
if (isDev() && $server->id === 0) {
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from");
if ($process->failed()) {
throw new \Exception($process->errorOutput());
}
$metrics = $process->output();
} else {
$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(); return $parsedCollection->toArray();
@@ -1459,7 +1494,9 @@ class Application extends BaseModel
return $config; return $config;
} }
public function setConfig($config) {
public function setConfig($config)
{
$config = $config; $config = $config;
$validator = Validator::make(['config' => $config], [ $validator = Validator::make(['config' => $config], [

View File

@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string', 'version' => 'string',
]; ];
protected $appends = ['real_value', 'is_shared']; protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted() protected static function booted()
{ {
@@ -74,6 +74,9 @@ class EnvironmentVariable extends Model
'version' => config('version'), 'version' => config('version'),
]); ]);
}); });
static::saving(function (EnvironmentVariable $environmentVariable) {
$environmentVariable->updateIsShared();
});
} }
public function service() 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 protected function isShared(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -210,4 +220,11 @@ class EnvironmentVariable extends Model
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value, 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

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -21,8 +22,23 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean', 'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string', 'auto_update_frequency' => 'string',
'update_check_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 public function fqdn(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -85,17 +101,4 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]"; return "[{$instanceName}]";
} }
public function helperVersion(): Attribute
{
return Attribute::make(
get: function ($value) {
if (isDev()) {
return 'latest';
}
return $value;
}
);
}
} }

View File

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

View File

@@ -7,6 +7,8 @@ use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
@@ -43,7 +45,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel class Server extends BaseModel
{ {
use SchemalessAttributesTrait; use SchemalessAttributesTrait,SoftDeletes;
public static $batch_counter = 0; public static $batch_counter = 0;
@@ -95,7 +97,8 @@ class Server extends BaseModel
} }
} }
}); });
static::deleting(function ($server) {
static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) { $server->destinations()->each(function ($destination) {
$destination->delete(); $destination->delete();
}); });
@@ -525,9 +528,20 @@ $schema://$host {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
public function sentinelHeartbeat(bool $isReset = false)
{
$this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
$this->save();
}
public function isSentinelLive()
{
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4));
}
public function isSentinelEnabled() public function isSentinelEnabled()
{ {
return $this->isMetricsEnabled() || $this->isServerApiEnabled(); return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
} }
public function isMetricsEnabled() public function isMetricsEnabled()
@@ -537,7 +551,7 @@ $schema://$host {
public function isServerApiEnabled() public function isServerApiEnabled()
{ {
return $this->settings->is_server_api_enabled; return $this->settings->is_sentinel_enabled;
} }
public function checkServerApi() public function checkServerApi()
@@ -555,7 +569,6 @@ $schema://$host {
ray($process->exitCode(), $process->output(), $process->errorOutput()); ray($process->exitCode(), $process->output(), $process->errorOutput());
throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
} }
} }
} }
@@ -579,7 +592,15 @@ $schema://$host {
{ {
if ($this->isMetricsEnabled()) { if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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); if (isDev() && $this->id === 0) {
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from");
if ($process->failed()) {
throw new \Exception($process->errorOutput());
}
$cpu = $process->output();
} else {
$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')) { if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true); $error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -588,17 +609,13 @@ $schema://$host {
} }
throw new \Exception($error); throw new \Exception($error);
} }
$cpu = str($cpu)->explode("\n")->skip(1)->all(); $cpu = json_decode($cpu, true);
$parsedCollection = collect($cpu)->flatMap(function ($item) { $parsedCollection = collect($cpu)->map(function ($metric) {
return collect(explode("\n", trim($item)))->map(function ($line) { return [(int) $metric['time'], (float) $metric['percent']];
[$time, $cpu_usage_percent] = explode(',', trim($line));
$cpu_usage_percent = number_format($cpu_usage_percent, 0);
return [(int) $time, (float) $cpu_usage_percent];
});
}); });
return $parsedCollection->toArray(); return $parsedCollection;
} }
} }
@@ -606,7 +623,15 @@ $schema://$host {
{ {
if ($this->isMetricsEnabled()) { if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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); if (isDev() && $this->id === 0) {
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from");
if ($process->failed()) {
throw new \Exception($process->errorOutput());
}
$memory = $process->output();
} else {
$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')) { if (str($memory)->contains('error')) {
$error = json_decode($memory, true); $error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -615,14 +640,9 @@ $schema://$host {
} }
throw new \Exception($error); throw new \Exception($error);
} }
$memory = str($memory)->explode("\n")->skip(1)->all(); $memory = json_decode($memory, true);
$parsedCollection = collect($memory)->flatMap(function ($item) { $parsedCollection = collect($memory)->map(function ($metric) {
return collect(explode("\n", trim($item)))->map(function ($line) { return [(int) $metric['time'], (float) $metric['usedPercent']];
[$time, $used, $free, $usedPercent] = explode(',', trim($line));
$usedPercent = number_format($usedPercent, 0);
return [(int) $time, (float) $usedPercent];
});
}); });
return $parsedCollection->toArray(); return $parsedCollection->toArray();
@@ -977,7 +997,8 @@ $schema://$host {
public function isProxyShouldRun() public function isProxyShouldRun()
{ {
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { // TODO: Do we need "|| $this->proxy->force_stop" here?
if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false; return false;
} }
@@ -1041,6 +1062,38 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker'); return data_get($this, 'settings.is_swarm_worker');
} }
public function status(): bool
{
['uptime' => $uptime] = $this->validateConnection(false);
if ($uptime) {
if ($this->unreachable_notification_sent === true) {
$this->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;
}
public function validateConnection($isManualCheck = true) public function validateConnection($isManualCheck = true)
{ {
config()->set('constants.ssh.mux_enabled', ! $isManualCheck); config()->set('constants.ssh.mux_enabled', ! $isManualCheck);

View File

@@ -24,7 +24,7 @@ use OpenApi\Attributes as OA;
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'], 'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'], 'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'], 'is_reachable' => ['type' => 'boolean'],
'is_server_api_enabled' => ['type' => 'boolean'], 'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'], 'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'], 'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'], 'is_usable' => ['type' => 'boolean'],
@@ -35,9 +35,9 @@ use OpenApi\Attributes as OA;
'logdrain_highlight_project_id' => ['type' => 'string'], 'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'], 'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'], 'logdrain_newrelic_license_key' => ['type' => 'string'],
'metrics_history_days' => ['type' => 'integer'], 'sentinel_metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'], 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'], 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'], 'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'], 'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'], 'server_id' => ['type' => 'integer'],
@@ -53,8 +53,66 @@ class ServerSetting extends Model
protected $casts = [ protected $casts = [
'force_docker_cleanup' => 'boolean', 'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer', 'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
]; ];
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()) {
$url = $setting->generateSentinelUrl(save: false);
if (str($url)->isEmpty()) {
$setting->is_sentinel_enabled = false;
} else {
$setting->is_sentinel_enabled = true;
}
}
} catch (\Throwable $e) {
loggy('Error creating server setting: '.$e->getMessage());
}
});
}
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 $encrypted;
}
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->ipv4) {
$domain = $settings->ipv4.':8000';
} elseif ($settings->ipv6) {
$domain = $settings->ipv6.':8000';
}
$this->sentinel_custom_url = $domain;
if ($save) {
$this->save();
}
return $domain;
}
public function server() public function server()
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);

View File

@@ -297,7 +297,7 @@ class Service extends BaseModel
'key' => 'CP_DISABLE_HTTPS', 'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'), 'value' => data_get($disable_https, 'value'),
'rules' => 'required', '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',
], ],
]); ]);
} }
@@ -997,8 +997,8 @@ class Service extends BaseModel
break; break;
case $image->contains('mysql'): case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER']; $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL']; $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT']; $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE']; $dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -1232,7 +1232,6 @@ class Service extends BaseModel
public function environment_variables(): HasMany public function environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); 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; 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;
}
);
}
} }

View File

@@ -272,7 +272,7 @@ class StandaloneClickhouse extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneDragonfly extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneKeydb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneMariadb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -292,7 +292,7 @@ class StandaloneMongodb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -273,7 +273,7 @@ class StandaloneMysql extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -274,7 +274,7 @@ class StandalonePostgresql extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -210,7 +210,12 @@ class StandaloneRedis extends BaseModel
protected function internalDbUrl(): Attribute protected function internalDbUrl(): Attribute
{ {
return new Attribute( return new Attribute(
get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0", get: function () {
$redis_version = $this->getRedisVersion();
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
}
); );
} }
@@ -219,7 +224,10 @@ class StandaloneRedis extends BaseModel
return new Attribute( return new Attribute(
get: function () { get: function () {
if ($this->is_public && $this->public_port) { if ($this->is_public && $this->public_port) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; $redis_version = $this->getRedisVersion();
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} }
return null; return null;
@@ -227,6 +235,13 @@ class StandaloneRedis extends BaseModel
); );
} }
public function getRedisVersion()
{
$image_parts = explode(':', $this->image);
return $image_parts[1] ?? '0.0';
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);
@@ -268,7 +283,7 @@ class StandaloneRedis extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $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}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -295,4 +310,33 @@ class StandaloneRedis extends BaseModel
{ {
return false; return false;
} }
public function redisPassword(): Attribute
{
return new Attribute(
get: function () {
$password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
if (! $password) {
return null;
}
return $password->value;
},
);
}
public function redisUsername(): Attribute
{
return new Attribute(
get: function () {
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
if (! $username) {
return null;
}
return $username->value;
}
);
}
} }

View File

@@ -1,5 +1,6 @@
<?php <?php
use App\Models\EnvironmentVariable;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneClickhouse; use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
@@ -48,7 +49,7 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
} }
$database = new StandaloneRedis; $database = new StandaloneRedis;
$database->name = generate_database_name('redis'); $database->name = generate_database_name('redis');
$database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id; $database->environment_id = $environment_id;
$database->destination_id = $destination->id; $database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass(); $database->destination_type = $destination->getMorphClass();
@@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
} }
$database->save(); $database->save();
EnvironmentVariable::create([
'key' => 'REDIS_PASSWORD',
'value' => $redis_password,
'standalone_redis_id' => $database->id,
'is_shared' => false,
]);
EnvironmentVariable::create([
'key' => 'REDIS_USERNAME',
'value' => 'default',
'standalone_redis_id' => $database->id,
'is_shared' => false,
]);
return $database; return $database;
} }

View File

@@ -335,6 +335,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
return explode(',', $matches[1]); return explode(',', $matches[1]);
} }
return null; return null;
})->flatten() })->flatten()
->filter() ->filter()
@@ -388,7 +389,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($path !== '/') { if ($path !== '/') {
// Middleware handling // Middleware handling
$middlewares = collect([]); $middlewares = collect([]);
if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix"); $middlewares->push("{$https_label}-stripprefix");
} }
@@ -402,7 +403,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name); $middlewares->push($to_non_www_name);
} }
if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }

View File

@@ -241,6 +241,7 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [ 'ports' => [
'80:80', '80:80',
'443:443', '443:443',
'443:443/udp',
], ],
'labels' => [ 'labels' => [
'coolify.managed=true', 'coolify.managed=true',

View File

@@ -126,7 +126,7 @@ function refreshSession(?Team $team = null): void
} }
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{ {
ray($error); loggy($error);
if ($error instanceof TooManyRequestsException) { if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -142,6 +142,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.'; return 'Duplicate entry found. Please use a different name.';
} }
if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
abort(404);
}
if ($error instanceof Throwable) { if ($error instanceof Throwable) {
$message = $error->getMessage(); $message = $error->getMessage();
} else { } else {
@@ -164,10 +168,10 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string function get_latest_sentinel_version(): string
{ {
try { try {
$response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json(); $versions = $response->json();
return data_get($versions, 'sentinel.version'); return data_get($versions, 'coolify.sentinel.version');
} catch (\Throwable $e) { } catch (\Throwable $e) {
//throw $e; //throw $e;
ray($e->getMessage()); ray($e->getMessage());
@@ -1338,13 +1342,6 @@ function isAnyDeploymentInprogress()
exit(0); exit(0);
} }
function generateSentinelToken()
{
$token = Str::random(64);
return $token;
}
function isBase64Encoded($strValue) function isBase64Encoded($strValue)
{ {
return base64_encode(base64_decode($strValue, true)) === $strValue; return base64_encode(base64_decode($strValue, true)) === $strValue;
@@ -3569,6 +3566,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
]); ]);
} else { } else {
if ($value->startsWith('$')) { if ($value->startsWith('$')) {
$isRequired = false;
if ($value->contains(':-')) { if ($value->contains(':-')) {
$value = replaceVariables($value); $value = replaceVariables($value);
$key = $value->before(':'); $key = $value->before(':');
@@ -3583,11 +3581,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$key = $value->before(':'); $key = $value->before(':');
$value = $value->after(':?'); $value = $value->after(':?');
$isRequired = true;
} elseif ($value->contains('?')) { } elseif ($value->contains('?')) {
$value = replaceVariables($value); $value = replaceVariables($value);
$key = $value->before('?'); $key = $value->before('?');
$value = $value->after('?'); $value = $value->after('?');
$isRequired = true;
} }
if ($originalValue->value() === $value->value()) { if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify // This means the variable does not have a default value, so it needs to be created in Coolify
@@ -3598,6 +3598,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
], [ ], [
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
'is_required' => $isRequired,
]); ]);
// Add the variable to the environment so it will be shown in the deployable compose file // Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value; $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
@@ -3611,6 +3612,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'value' => $value, 'value' => $value,
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
'is_required' => $isRequired,
]); ]);
} }
@@ -3787,7 +3789,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
service_name: $serviceName, service_name: $serviceName,
image: $image, image: $image,
predefinedPort: $predefinedPort predefinedPort: $predefinedPort
)); ));
} }
} }
@@ -3985,13 +3986,14 @@ function instanceSettings()
return InstanceSettings::get(); return InstanceSettings::get();
} }
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) { function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
{
$server = Server::find($server_id)->where('team_id', $team_id)->first(); $server = Server::find($server_id)->where('team_id', $team_id)->first();
if (!$server) { if (! $server) {
return; return;
} }
$uuid = new Cuid2(); $uuid = new Cuid2;
$cloneCommand = "git clone --no-checkout -b $branch $repository ."; $cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/'); $workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]); $fileList = collect([".$workdir/coolify.json"]);
@@ -4012,3 +4014,30 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
// continue // continue
} }
} }
function loggy($message = null, array $context = [])
{
if (! isDev()) {
return;
}
if (function_exists('ray') && config('app.debug')) {
ray($message, $context);
}
if (is_null($message)) {
return app('log');
}
return app('log')->debug($message, $context);
}
function sslipDomainWarning(string $domains)
{
$domains = str($domains)->trim()->explode(',');
$showSslipHttpsWarning = false;
$domains->each(function ($domain) use (&$showSslipHttpsWarning) {
if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
$showSslipHttpsWarning = true;
}
});
return $showSslipHttpsWarning;
}

View File

@@ -15,6 +15,7 @@
"laravel/fortify": "^v1.16.0", "laravel/fortify": "^v1.16.0",
"laravel/framework": "^v11", "laravel/framework": "^v11",
"laravel/horizon": "^5.29.1", "laravel/horizon": "^5.29.1",
"laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6", "laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v4.0", "laravel/sanctum": "^v4.0",
"laravel/socialite": "^v5.14.0", "laravel/socialite": "^v5.14.0",

79
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c47adf3684eb727e22503937435c0914", "content-hash": "943975ec232403b96a40d215253492d8",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@@ -3144,6 +3144,83 @@
}, },
"time": "2024-10-08T18:23:02+00:00" "time": "2024-10-08T18:23:02+00:00"
}, },
{
"name": "laravel/pail",
"version": "v1.1.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/pail.git",
"reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
"reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"illuminate/console": "^10.24|^11.0",
"illuminate/contracts": "^10.24|^11.0",
"illuminate/log": "^10.24|^11.0",
"illuminate/process": "^10.24|^11.0",
"illuminate/support": "^10.24|^11.0",
"nunomaduro/termwind": "^1.15|^2.0",
"php": "^8.2",
"symfony/console": "^6.0|^7.0"
},
"require-dev": {
"laravel/pint": "^1.13",
"orchestra/testbench": "^8.12|^9.0",
"pestphp/pest": "^2.20",
"pestphp/pest-plugin-type-coverage": "^2.3",
"phpstan/phpstan": "^1.10",
"symfony/var-dumper": "^6.3|^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Pail\\PailServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Pail\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Nuno Maduro",
"email": "enunomaduro@gmail.com"
}
],
"description": "Easily delve into your Laravel application's log files directly from the command line.",
"homepage": "https://github.com/laravel/pail",
"keywords": [
"laravel",
"logs",
"php",
"tail"
],
"support": {
"issues": "https://github.com/laravel/pail/issues",
"source": "https://github.com/laravel/pail"
},
"time": "2024-10-15T20:06:24+00:00"
},
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
"version": "v0.1.25", "version": "v0.1.25",

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.360', 'release' => '4.0.0-beta.361',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

6
config/testing.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
return [
'dusk_test_email' => env('DUSK_TEST_EMAIL', 'test@example.com'),
'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'),
];

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.360'; return '4.0.0-beta.361';

View File

@@ -18,7 +18,7 @@ return new class extends Migration
$table->boolean('is_metrics_enabled')->default(false); $table->boolean('is_metrics_enabled')->default(false);
$table->integer('metrics_refresh_rate_seconds')->default(5); $table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30); $table->integer('metrics_history_days')->default(30);
$table->string('metrics_token')->default(generateSentinelToken()); $table->string('metrics_token')->nullable();
}); });
} }

View File

@@ -4,6 +4,7 @@ use App\Models\EnvironmentVariable;
use App\Models\Server; use App\Models\Server;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -14,6 +15,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
try {
Schema::table('applications', function (Blueprint $table) { Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('docker_compose_pr_location'); $table->dropColumn('docker_compose_pr_location');
$table->dropColumn('docker_compose_pr'); $table->dropColumn('docker_compose_pr');
@@ -47,11 +49,11 @@ return new class extends Migration
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->integer('metrics_history_days')->default(7)->change(); $table->integer('metrics_history_days')->default(7)->change();
}); });
Server::all()->each(function (Server $server) {
$server->settings->update([ DB::table('server_settings')->update(['metrics_history_days' => 7]);
'metrics_history_days' => 7, } catch (\Exception $e) {
]); loggy($e);
}); }
} }
/** /**

View File

@@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('is_force_cleanup_enabled')->default(false)->after('is_sentinel_enabled'); $table->boolean('is_force_cleanup_enabled')->default(false);
}); });
} }

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->boolean('is_required')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('is_required');
});
}
};

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('metrics_token');
$table->dropColumn('metrics_refresh_rate_seconds');
$table->dropColumn('metrics_history_days');
$table->dropColumn('is_server_api_enabled');
$table->boolean('is_sentinel_enabled')->default(false);
$table->text('sentinel_token')->nullable();
$table->integer('sentinel_metrics_refresh_rate_seconds')->default(10);
$table->integer('sentinel_metrics_history_days')->default(7);
$table->integer('sentinel_push_interval_seconds')->default(60);
$table->string('sentinel_custom_url')->nullable();
});
Schema::table('servers', function (Blueprint $table) {
$table->dateTime('sentinel_updated_at')->default(now());
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->string('metrics_token')->nullable();
$table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30);
$table->boolean('is_server_api_enabled')->default(false);
$table->dropColumn('is_sentinel_enabled');
$table->dropColumn('sentinel_token');
$table->dropColumn('sentinel_metrics_refresh_rate_seconds');
$table->dropColumn('sentinel_metrics_history_days');
$table->dropColumn('sentinel_push_interval_seconds');
$table->dropColumn('sentinel_custom_url');
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('sentinel_updated_at');
});
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIsSharedToEnvironmentVariables extends Migration
{
public function up()
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->boolean('is_shared')->default(false);
});
}
public function down()
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('is_shared');
});
}
}

View File

@@ -0,0 +1,41 @@
<?php
use App\Models\EnvironmentVariable;
use App\Models\StandaloneRedis;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MoveRedisPasswordToEnvs extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
try {
StandaloneRedis::chunkById(100, function ($redisInstances) {
foreach ($redisInstances as $redis) {
$redis_password = DB::table('standalone_redis')->where('id', $redis->id)->value('redis_password');
EnvironmentVariable::create([
'standalone_redis_id' => $redis->id,
'key' => 'REDIS_PASSWORD',
'value' => $redis_password,
]);
EnvironmentVariable::create([
'standalone_redis_id' => $redis->id,
'key' => 'REDIS_USERNAME',
'value' => 'default',
]);
}
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('redis_password');
});
} catch (\Exception $e) {
echo 'Moving Redis passwords to envs failed.';
echo $e->getMessage();
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->boolean('disable_two_step_confirmation')->default(false);
});
}
public function down()
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('disable_two_step_confirmation');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@@ -26,6 +26,8 @@ class DatabaseSeeder extends Seeder
S3StorageSeeder::class, S3StorageSeeder::class,
StandalonePostgresqlSeeder::class, StandalonePostgresqlSeeder::class,
OauthSettingSeeder::class, OauthSettingSeeder::class,
DisableTwoStepConfirmationSeeder::class,
SentinelSeeder::class,
]); ]);
} }
} }

View File

@@ -0,0 +1,20 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class DisableTwoStepConfirmationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
DB::table('instance_settings')->updateOrInsert(
[],
['disable_two_step_confirmation' => true]
);
}
}

View File

@@ -186,6 +186,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->call(OauthSettingSeeder::class); $this->call(OauthSettingSeeder::class);
$this->call(PopulateSshKeysDirectorySeeder::class); $this->call(PopulateSshKeysDirectorySeeder::class);
$this->call(SentinelSeeder::class);
} }
} }

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use App\Models\Server;
use Illuminate\Database\Seeder;
class SentinelSeeder extends Seeder
{
public function run()
{
Server::chunk(100, function ($servers) {
foreach ($servers as $server) {
try {
if (str($server->settings->sentinel_token)->isEmpty()) {
$server->settings->generateSentinelToken();
}
if (str($server->settings->sentinel_custom_url)->isEmpty()) {
$url = $server->settings->generateSentinelUrl();
if (str($url)->isEmpty()) {
$server->settings->is_sentinel_enabled = false;
$server->settings->save();
}
}
} catch (\Throwable $e) {
loggy("Error: {$e->getMessage()}\n");
}
}
});
}
}

View File

@@ -4,7 +4,7 @@
"dependencies": { "dependencies": {
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"cookie": "^0.6.0", "cookie": "^0.7.0",
"axios": "1.7.5", "axios": "1.7.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"node-pty": "^1.0.0", "node-pty": "^1.0.0",

View File

@@ -5,34 +5,38 @@ ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION=2024.4.1 ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15 ARG POSTGRES_VERSION=15
RUN apt-get update
# Postgres version requirements
RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list # Use build arguments for caching
ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
RUN apt-get update # Install dependencies
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
apt-get install -y $BUILDTIME_DEPS && \
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
apt-get update && \
apt-get install -y $RUNTIME_DEPS && \
apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
# Coolify requirements
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/ COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin RUN mkdir -p /usr/local/bin
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ RUN --mount=type=cache,target=/root/.cache \
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \ echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi" ;fi"
RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ RUN --mount=type=cache,target=/root/.cache \
/bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \ echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi" ;fi"

View File

@@ -26,5 +26,12 @@
"input.code": "الرمز لمرة واحدة", "input.code": "الرمز لمرة واحدة",
"input.recovery_code": "رمز الاسترداد", "input.recovery_code": "رمز الاسترداد",
"button.save": "حفظ", "button.save": "حفظ",
"repository.url": "<span class='text-helper'>أمثلة</span><br>للمستودعات العامة، استخدم <span class='text-helper'>https://...</span>.<br>للمستودعات الخاصة، استخدم <span class='text-helper'>git@...</span>.<br><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://github.com/coollabsio/coolify-examples</span><br>سيتم تحديد الفرع <span class='text-helper'>nodejs-fastify</span> لـ <span class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify</span><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://gitea.com/sedlav/expressjs.git</span><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://gitlab.com/andrasbacsai/nodejs-example.git</span>." "repository.url": "<span class='text-helper'>أمثلة</span><br>للمستودعات العامة، استخدم <span class='text-helper'>https://...</span>.<br>للمستودعات الخاصة، استخدم <span class='text-helper'>git@...</span>.<br><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://github.com/coollabsio/coolify-examples</span><br>سيتم تحديد الفرع <span class='text-helper'>nodejs-fastify</span> لـ <span class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify</span><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://gitea.com/sedlav/expressjs.git</span><br>سيتم تحديد الفرع <span class='text-helper'>main</span> لـ <span class='text-helper'>https://gitlab.com/andrasbacsai/nodejs-example.git</span>.",
"service.stop": "سيتم إيقاف هذه الخدمة.",
"resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).",
"resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.",
"resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.",
"resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.",
"resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.",
"database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي."
} }

View File

@@ -33,5 +33,6 @@
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.", "resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.", "resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server.", "resource.delete_configurations": "Permanently delete all configuration files from the server.",
"database.delete_backups_locally": "All backups will be permanently deleted from local storage." "database.delete_backups_locally": "All backups will be permanently deleted from local storage.",
"warning.sslipdomain": "Your configuration is saved, but sslip domain with https is <span class='dark:text-red-500 text-red-500 font-bold'>NOT</span> recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail). <br><br>Use your own domain instead."
} }

37
lang/ro.json Normal file
View File

@@ -0,0 +1,37 @@
{
"auth.login": "Autentificare",
"auth.login.azure": "Autentificare prin Microsoft",
"auth.login.bitbucket": "Autentificare prin Bitbucket",
"auth.login.github": "Autentificare prin GitHub",
"auth.login.gitlab": "Autentificare prin Gitlab",
"auth.login.google": "Autentificare prin Google",
"auth.already_registered": "Sunteți deja înregistrat?",
"auth.confirm_password": "Confirmați parola",
"auth.forgot_password": "Ați uitat parola",
"auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei",
"auth.register_now": "Înregistrare",
"auth.logout": "Deconectare",
"auth.register": "Înregistrare",
"auth.registration_disabled": "Înregistrarea este dezactivată. Vă rugăm să contactați administratorul site-ului.",
"auth.reset_password": "Resetare parolă",
"auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.",
"auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.",
"auth.failed.password": "Parola furnizată este incorectă.",
"auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.",
"auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.",
"input.name": "Nume",
"input.email": "E-mail",
"input.password": "Parolă",
"input.password.again": "Repetați parola",
"input.code": "Cod de unică folosință",
"input.recovery_code": "Cod de recuperare",
"button.save": "Salvare",
"repository.url": "<span class='text-helper'>Exemple</span><br>Pentru depozite publice, utilizați <span class='text-helper'>https://...</span>.<br>Pentru depozite private, utilizați <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples va fi selectată ramura <span class='text-helper'>main</span><br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura <span class='text-helper'>nodejs-fastify</span>.<br>https://gitea.com/sedlav/expressjs.git va fi selectată ramura <span class='text-helper'>main</span>.<br>https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura <span class='text-helper'>main</span>.",
"service.stop": "Acest serviciu va fi oprit.",
"resource.docker_cleanup": "Executați curățarea Docker (eliminați imaginile neutilizate și memoria cache a constructorului).",
"resource.non_persistent": "Toate datele nepersistente vor fi șterse.",
"resource.delete_volumes": "Ștergeți definitiv toate volumele asociate cu această resursă.",
"resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.",
"resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.",
"database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală."
}

View File

@@ -98,6 +98,10 @@ paths:
is_static: is_static:
type: boolean type: boolean
description: 'The flag to indicate if the application is static.' description: 'The flag to indicate if the application is static.'
static_image:
type: string
enum: ['nginx:alpine']
description: 'The static image.'
install_command: install_command:
type: string type: string
description: 'The install command.' description: 'The install command.'
@@ -323,6 +327,10 @@ paths:
is_static: is_static:
type: boolean type: boolean
description: 'The flag to indicate if the application is static.' description: 'The flag to indicate if the application is static.'
static_image:
type: string
enum: ['nginx:alpine']
description: 'The static image.'
install_command: install_command:
type: string type: string
description: 'The install command.' description: 'The install command.'
@@ -548,6 +556,10 @@ paths:
is_static: is_static:
type: boolean type: boolean
description: 'The flag to indicate if the application is static.' description: 'The flag to indicate if the application is static.'
static_image:
type: string
enum: ['nginx:alpine']
description: 'The static image.'
install_command: install_command:
type: string type: string
description: 'The install command.' description: 'The install command.'
@@ -3093,7 +3105,7 @@ paths:
security: security:
- -
bearerAuth: [] bearerAuth: []
/healthcheck: /health:
get: get:
summary: Healthcheck summary: Healthcheck
description: 'Healthcheck endpoint.' description: 'Healthcheck endpoint.'
@@ -4959,7 +4971,7 @@ components:
type: boolean type: boolean
is_reachable: is_reachable:
type: boolean type: boolean
is_server_api_enabled: is_sentinel_enabled:
type: boolean type: boolean
is_swarm_manager: is_swarm_manager:
type: boolean type: boolean
@@ -4981,11 +4993,11 @@ components:
type: string type: string
logdrain_newrelic_license_key: logdrain_newrelic_license_key:
type: string type: string
metrics_history_days: sentinel_metrics_history_days:
type: integer type: integer
metrics_refresh_rate_seconds: sentinel_metrics_refresh_rate_seconds:
type: integer type: integer
metrics_token: sentinel_token:
type: string type: string
docker_cleanup_frequency: docker_cleanup_frequency:
type: string type: string

88
public/svgs/affine.svg Normal file
View File

@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 500 500" width="500" height="500" style="width:100%;height:100%;transform:translate3d(0,0,0);content-visibility:visible">
<defs>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="a"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="g"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="b"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="c"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="d"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="e"/>
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0z" transform="translate(250 250)" style="display:block" id="f"/>
<mask id="r" mask-type="alpha">
<use xlink:href="#a"/>
</mask>
<mask id="p" mask-type="alpha">
<use xlink:href="#b"/>
</mask>
<mask id="n" mask-type="alpha">
<use xlink:href="#c"/>
</mask>
<mask id="m" mask-type="alpha">
<use xlink:href="#d"/>
</mask>
<mask id="l" mask-type="alpha">
<use xlink:href="#e"/>
</mask>
<mask id="k" mask-type="alpha">
<use xlink:href="#f"/>
</mask>
<mask id="j" mask-type="alpha">
<use xlink:href="#g"/>
</mask>
<clipPath id="h">
<path d="M0 0h500v500H0z"/>
</clipPath>
<clipPath id="i">
<path d="M0 0h500v500H0z"/>
</clipPath>
<clipPath id="s">
<path d="M0 0h1920v1080H0z"/>
</clipPath>
<clipPath id="q">
<path d="M0 0h1920v1080H0z"/>
</clipPath>
<clipPath id="o">
<path d="M0 0h1920v1080H0z"/>
</clipPath>
</defs>
<g clip-path="url(#h)">
<g clip-path="url(#i)" transform="translate(0 24)" style="display:block">
<path d="M-6.816-205.027c-20.92 36.208-170.344 294.875-187.382 324.715-1.947 4.684 1.644 10.592 6.749 10.684 1.368.132 11.815.066 13.815.079 97.651.04 259.272-.026 357.187 0 1.526 0 3.79.013 3.895-.079.618.053 1.105-.198 1.658-.224 4.25-1.276 6.815-6.276 5.091-10.447-.408-.592-.237-.67-1.342-2.486C143.266 31.247 71.403-93.101 21.696-179.24c-3.052-5.052-11.868-20.735-14.881-25.774-2.96-5.144-10.684-5.21-13.644 0zm-20.511-11.842c9.105-16.065 30.997-20.709 45.904-9.762 3.566 2.645 6.513 6.039 8.763 9.762l4.96 8.592 9.921 17.183L200.973 83.875l9.921 17.183c1.671 2.987 3.21 5.25 5.236 9.644 1.605 4.105 2.435 8.539 2.29 12.973-.382 13.381-9.974 25.656-22.881 29.248-4.184 1.184-9.144 1.237-11.986 1.184-98.112-.118-259.378.092-357.187 0-6.723-.237-14.512.763-21.906-1.197-17.788-4.96-27.683-25.183-20.578-42.234 17.486-31.524 167.371-290.153 188.791-327.545" transform="translate(250 250)" style="display:block"/>
<g mask="url(#j)" style="display:block">
<path d="m202.134 244.025 41.827 72.456c2.684 4.658 9.407 4.658 12.091 0l41.826-72.456c2.684-4.658-.671-10.473-6.039-10.473h-83.652c-5.368 0-8.736 5.815-6.039 10.473zm44.182 47.642-21.525-37.274c-1.645-2.842.408-6.394 3.697-6.394h43.05c3.276 0 5.328 3.552 3.697 6.394l-21.525 37.274c-1.645 2.842-5.736 2.842-7.381 0z"/>
</g>
<g mask="url(#k)" style="display:block">
<path d="M230.369 47.341c-22.906 53.22-42.589 115.018-23.274 172.146 2.802 7.868 6.288 15.42 10.604 22.591l-12.486 7.302c-4.684-7.881-8.618-16.486-11.657-25.222-17.749-52.128-4.526-108.427 14.486-158.029 3.197-8.184 6.605-16.262 10.157-24.248l12.157 5.447z"/>
</g>
<g mask="url(#l)" style="display:block">
<path d="M448.039 356.347c-34.642-46.457-78.31-94.402-137.451-106.23-8.21-1.5-16.499-2.263-24.867-2.105l-.079-14.46c9.17-.118 18.578.776 27.669 2.513 54.01 10.697 96.165 50.286 129.61 91.56 5.486 6.855 10.776 13.855 15.92 20.92z"/>
</g>
<g mask="url(#m)" style="display:block">
<path d="M71.59 390.345c57.549-6.776 120.914-20.617 160.727-65.93 5.408-6.355 10.21-13.158 14.262-20.486l12.565 7.158c-4.487 7.999-9.973 15.709-16.012 22.709-36.274 41.432-91.626 58.141-144.096 66.469a586 586 0 0 1-26.077 3.329l-1.355-13.249z"/>
</g>
<g mask="url(#n)" style="display:block">
<g clip-path="url(#o)" transform="translate(-710 -290)">
<path d="m26.6-48.803-69.413 93.109-5.881-7.184 62.386-93.517z" transform="translate(990.142 584.502)" style="display:block"/>
<path d="M-69.58 28.086 57.952-72.23l11.212 14.81-133.09 92.966z" transform="translate(988.235 608.382)" style="display:block"/>
<path d="m-101.789 16.832 210.317-98.614 8.268 18.054-214.415 89.649z" transform="translate(981.847 640.913)" style="display:block"/>
<path d="m-165.267 36.82 324.014-66.518 4.06 20.75-326.033 56.216z" transform="translate(981.847 640.913)" style="display:block"/>
</g>
</g>
<g mask="url(#p)" style="display:block">
<g clip-path="url(#q)" transform="rotate(120 689.283 205.728)">
<path d="m26.6-48.803-69.413 93.109-5.881-7.184 62.386-93.517z" transform="translate(990.142 584.502)" style="display:block"/>
<path d="M-69.58 28.086 57.952-72.23l11.212 14.81-133.09 92.966z" transform="translate(988.235 608.382)" style="display:block"/>
<path d="m-101.789 16.832 210.317-98.614 8.268 18.054-214.415 89.649z" transform="translate(981.847 640.913)" style="display:block"/>
<path d="m-165.267 36.82 324.014-66.518 4.06 20.75-326.033 56.216z" transform="translate(981.847 640.913)" style="display:block"/>
</g>
</g>
<g mask="url(#r)" style="display:block">
<g clip-path="url(#s)" transform="rotate(-120 520.37 615.193)">
<path d="m26.6-48.803-69.413 93.109-5.881-7.184 62.386-93.517z" transform="translate(990.142 584.502)" style="display:block"/>
<path d="M-69.58 28.086 57.952-72.23l11.212 14.81-133.09 92.966z" transform="translate(988.235 608.382)" style="display:block"/>
<path d="m-101.789 16.832 210.317-98.614 8.268 18.054-214.415 89.649z" transform="translate(981.847 640.913)" style="display:block"/>
<path d="m-165.267 36.82 324.014-66.518 4.06 20.75-326.033 56.216z" transform="translate(981.847 640.913)" style="display:block"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

9
public/svgs/calcom.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="101" height="22" viewBox="0 0 101 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0582 20.817C4.32115 20.817 0 16.2763 0 10.6704C0 5.04589 4.1005 0.467773 10.0582 0.467773C13.2209 0.467773 15.409 1.43945 17.1191 3.66311L14.3609 5.96151C13.2025 4.72822 11.805 4.11158 10.0582 4.11158C6.17833 4.11158 4.04533 7.08268 4.04533 10.6704C4.04533 14.2582 6.38059 17.1732 10.0582 17.1732C11.7866 17.1732 13.2577 16.5566 14.4161 15.3233L17.1375 17.7151C15.501 19.8453 13.2577 20.817 10.0582 20.817Z" fill="#fafafa"/>
<path d="M29.0161 5.88601H32.7304V20.4612H29.0161V18.331C28.2438 19.8446 26.9566 20.8536 24.4927 20.8536C20.5577 20.8536 17.4133 17.4341 17.4133 13.2297C17.4133 9.02528 20.5577 5.60571 24.4927 5.60571C26.9383 5.60571 28.2438 6.61477 29.0161 8.12835V5.88601ZM29.1264 13.2297C29.1264 10.95 27.5634 9.06266 25.0995 9.06266C22.7274 9.06266 21.1828 10.9686 21.1828 13.2297C21.1828 15.4346 22.7274 17.3967 25.0995 17.3967C27.5451 17.3967 29.1264 15.4907 29.1264 13.2297Z" fill="#fafafa"/>
<path d="M35.3599 0H39.0742V20.4427H35.3599V0Z" fill="#fafafa"/>
<path d="M40.7291 18.5182C40.7291 17.3223 41.6853 16.3132 42.9908 16.3132C44.2964 16.3132 45.2158 17.3223 45.2158 18.5182C45.2158 19.7515 44.278 20.7605 42.9908 20.7605C41.7037 20.7605 40.7291 19.7515 40.7291 18.5182Z" fill="#fafafa"/>
<path d="M59.4296 18.1068C58.0505 19.7885 55.9543 20.8536 53.4719 20.8536C49.0404 20.8536 45.7858 17.4341 45.7858 13.2297C45.7858 9.02528 49.0404 5.60571 53.4719 5.60571C55.8623 5.60571 57.9402 6.61477 59.3193 8.20309L56.4508 10.6136C55.7336 9.71667 54.7958 9.04397 53.4719 9.04397C51.0999 9.04397 49.5553 10.95 49.5553 13.211C49.5553 15.472 51.0999 17.378 53.4719 17.378C54.9062 17.378 55.8991 16.6306 56.6346 15.6215L59.4296 18.1068Z" fill="#fafafa"/>
<path d="M59.7422 13.2297C59.7422 9.02528 62.9968 5.60571 67.4283 5.60571C71.8598 5.60571 75.1144 9.02528 75.1144 13.2297C75.1144 17.4341 71.8598 20.8536 67.4283 20.8536C62.9968 20.8349 59.7422 17.4341 59.7422 13.2297ZM71.3449 13.2297C71.3449 10.95 69.8003 9.06266 67.4283 9.06266C65.0563 9.04397 63.5117 10.95 63.5117 13.2297C63.5117 15.4907 65.0563 17.3967 67.4283 17.3967C69.8003 17.3967 71.3449 15.4907 71.3449 13.2297Z" fill="#fafafa"/>
<path d="M100.232 11.5482V20.4428H96.518V12.4638C96.518 9.94119 95.3412 8.85739 93.576 8.85739C91.921 8.85739 90.7442 9.67958 90.7442 12.4638V20.4428H87.0299V12.4638C87.0299 9.94119 85.8346 8.85739 84.0878 8.85739C82.4329 8.85739 80.9802 9.67958 80.9802 12.4638V20.4428H77.2659V5.8676H80.9802V7.88571C81.7525 6.31607 83.15 5.53125 85.3014 5.53125C87.3425 5.53125 89.0525 6.5403 89.9903 8.24074C90.9281 6.50293 92.3072 5.53125 94.8079 5.53125C97.8603 5.54994 100.232 7.86702 100.232 11.5482Z" fill="#fafafa"/>
<script xmlns=""/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,7 @@
<svg width="47" height="30" viewBox="0 0 47 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.7759 14.1873C38.5379 14.1873 38.7759 14.1873 38.7759 14.1873C38.7759 6.60302 31.8594 0 24.0793 0C18.3479 0 13.236 3.37708 11.0359 8.16627C10.9594 8.16474 11.0363 8.16474 10.9594 8.16474C4.91834 8.16474 0 13.4481 0 19.3371C0 23.5291 1.79123 26.7912 5.40029 28.5334C5.97588 27.949 7.40249 27.9389 8.13884 27.5428C6.84539 27.1249 5.79299 26.2659 4.66867 25.4489C0.803302 22.6401 1.38287 15.7952 3.91798 13.4851C7.97916 9.78429 12.4979 10.7237 12.4979 10.7237L13.059 9.50237C13.9945 7.46591 15.8766 1.56091 24.4742 1.66552C33.5675 1.77619 36.0433 11.1473 36.0467 14.2713C36.0464 14.284 36.0463 14.2969 36.0463 14.3097L36.0415 16.517L38.2972 16.326C38.4759 16.3108 38.6533 16.3031 38.8244 16.3031C42.127 16.3031 45.4997 18.8079 45.4997 22.0274C45.4997 25.2468 42.127 27.9805 38.8244 27.9805H20.1518C20.3643 28.0866 22.5177 30 26.0633 30H38.8244C43.2765 30 46.8855 26.4818 46.8855 22.1418C46.8855 17.8019 43.228 14.1873 38.7759 14.1873Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.8004 9.8463C31.95 9.53546 29.7648 9.74492 28.2297 10.3292C27.4766 10.6158 26.8793 10.9921 26.674 11.4423C26.1107 12.6779 29.5611 14.8228 30.9519 14.4284C31.6146 14.2404 32.5663 12.8654 32.9903 11.6624C33.2918 10.8073 33.3271 10.0389 32.8004 9.8463Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.0842 10.8637C24.6901 10.8148 23.8755 10.7075 23.6155 10.4203C23.0338 9.77811 22.1014 9.60573 21.4237 9.75105C20.7054 9.90502 20.3232 10.7199 20.8163 11.7331C21.0382 12.189 20.9819 12.7384 20.7558 13.148C21.462 12.5069 22.1221 11.9844 22.6119 11.7331C23.5772 11.2382 24.4333 10.9815 25.0842 10.8637Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1314 20.2602C32.1314 20.2602 31.4272 22.5942 30.9343 23.6582C30.4998 24.5958 30.3622 24.5209 30.0482 24.6307C30.1662 24.34 30.2513 23.9586 30.4061 23.3149C30.5923 22.5406 30.4061 22.3368 30.4061 22.3368C30.162 23.4955 29.7824 24.3235 29.46 24.8668C29.0416 25.0032 28.5273 25.0913 28.1527 24.9452C27.6246 24.7393 27.5349 22.3195 27.5349 22.1823C29.8332 22.3368 31.0575 21.4272 32.1314 20.2602ZM20.8335 17.7094C20.8821 17.934 20.9363 18.1447 20.9929 18.349C20.9577 18.3105 20.9391 18.2896 20.9391 18.2896C20.9391 18.2896 20.9842 18.3913 21.0538 18.5659C21.1504 18.8876 21.2571 19.1817 21.3709 19.4538C21.7489 20.6371 22.1519 22.4536 21.5511 23.5925C21.1018 24.4441 15.3943 28.2867 25.1501 29.9574L33.9076 27.9808C30.0498 27.4718 31.5994 24.4504 32.5187 22.8344C33.622 20.8947 33.9986 19.6596 34.2362 17.8778C34.3792 16.8067 34.274 16.0052 34.2439 15.8123C34.2392 15.7831 34.2362 15.7669 34.2362 15.7669C34.2362 15.7669 34.232 15.8268 34.2207 15.9294L34.2362 15.6639C34.2362 15.6639 33.5397 18.3926 32.1666 19.3708C31.8471 19.5983 31.6426 19.7498 31.5115 19.8517C31.6499 19.3431 32.1686 17.1988 31.4272 16.0244C31.4272 16.0244 31.12 20.9578 28.4217 21.2197C25.7233 21.4816 21.9208 18.7903 20.8335 17.7094Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0693 17.5462C12.9609 18.1371 12.9143 18.6691 12.9312 19.1378C12.4068 19.9482 8.62053 28.7229 3.91797 24.7801C4.7226 25.4678 5.40027 28.5338 5.40027 28.5338C9.82341 31.0535 12.8862 21.1715 13.2359 20.5234C13.5211 21.1173 14.0027 22.3232 16.6758 22.3769C16.6758 22.3769 13.5399 20.8782 14.9397 16.901C16.3395 12.9238 18.4087 9.54824 21.2501 7.98749C24.0914 6.42674 26.9623 6.53321 29.603 7.63149C29.603 7.63149 25.6177 4.05998 21.0383 6.75964C18.4072 8.31077 16.6555 10.0319 15.4497 11.7959C14.4386 11.9719 13.2092 11.8129 12.7108 11.8129C12.0807 11.8129 11.1087 12.2295 11.548 13.7373C11.9783 15.2138 13.1659 16.707 13.0693 17.5462Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

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