diff --git a/.env.dusk.ci b/.env.dusk.ci new file mode 100644 index 000000000..9660de7b4 --- /dev/null +++ b/.env.dusk.ci @@ -0,0 +1,15 @@ +APP_ENV=production +APP_NAME="Coolify Staging" +APP_ID=development +APP_KEY= +APP_URL=http://localhost +APP_PORT=8000 +SSH_MUX_ENABLED=true + +# PostgreSQL Database Configuration +DB_DATABASE=coolify +DB_USERNAME=coolify +DB_PASSWORD=password +DB_HOST=localhost +DB_PORT=5432 + diff --git a/.env.windows-docker-desktop.example b/.env.windows-docker-desktop.example index 02a5a4174..b067b4c5c 100644 --- a/.env.windows-docker-desktop.example +++ b/.env.windows-docker-desktop.example @@ -4,6 +4,7 @@ APP_ID=coolify-windows-docker-desktop APP_NAME=Coolify APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80= +DB_USERNAME=coolify DB_PASSWORD=coolify REDIS_PASSWORD=coolify diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 000000000..b06c9e97c --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,65 @@ +name: Dusk +on: + push: + branches: [ "not-existing" ] +jobs: + dusk: + runs-on: ubuntu-latest + + services: + redis: + image: redis + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up PostgreSQL + run: | + sudo systemctl start postgresql + sudo -u postgres psql -c "CREATE DATABASE coolify;" + sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';" + sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';" + sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';" + sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Copy .env + run: cp .env.dusk.ci .env + - name: Install Dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Generate key + run: php artisan key:generate + - name: Install Chrome binaries + run: php artisan dusk:chrome-driver --detect + - name: Start Chrome Driver + run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 & + - name: Build assets + run: npm install && npm run build + - name: Run Laravel Server + run: php artisan serve --no-reload & + - name: Execute tests + run: php artisan dusk + - name: Upload Screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tests/Browser/screenshots + - name: Upload Console Logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: console + path: tests/Browser/console diff --git a/.github/workflows/lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml similarity index 100% rename from .github/workflows/lock-closed-issues-discussions-and-prs.yml rename to .github/workflows/chore-lock-closed-issues-discussions-and-prs.yml diff --git a/.github/workflows/manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml similarity index 100% rename from .github/workflows/manage-stale-issues-and-prs.yml rename to .github/workflows/chore-manage-stale-issues-and-prs.yml diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml similarity index 100% rename from .github/workflows/remove-labels-and-assignees-on-close.yml rename to .github/workflows/chore-remove-labels-and-assignees-on-close.yml diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index 4add8516e..4354294b1 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -1,4 +1,4 @@ -name: Coolify Helper Image Development (v4) +name: Coolify Helper Image Development on: push: @@ -8,7 +8,8 @@ on: - docker/coolify-helper/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-helper" jobs: @@ -19,25 +20,36 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next labels: | coolify.managed=true aarch64: @@ -47,27 +59,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -75,25 +99,42 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index a9e8a5dd0..6d852a2b3 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -1,4 +1,4 @@ -name: Coolify Helper Image (v4) +name: Coolify Helper Image on: push: @@ -8,7 +8,8 @@ on: - docker/coolify-helper/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-helper" jobs: @@ -19,25 +20,36 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} labels: | coolify.managed=true aarch64: @@ -47,25 +59,36 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 labels: | coolify.managed=true merge-manifest: @@ -75,25 +98,43 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml new file mode 100644 index 000000000..771687d4b --- /dev/null +++ b/.github/workflows/coolify-production-build.yml @@ -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 }} diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index 33e048627..7e937d17a 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -1,17 +1,18 @@ -name: Coolify Realtime Development (v4) +name: Coolify Realtime Development on: push: branches: [ "next" ] paths: - - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml - docker/coolify-realtime/Dockerfile - docker/coolify-realtime/terminal-server.js - docker/coolify-realtime/package.json - docker/coolify-realtime/soketi-entrypoint.sh env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-realtime" jobs: @@ -22,27 +23,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next labels: | coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -50,27 +63,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,26 +103,44 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 if: always() with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 30910ae0b..97bfd52eb 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -1,4 +1,4 @@ -name: Coolify Realtime (v4) +name: Coolify Realtime on: push: @@ -11,7 +11,8 @@ on: - docker/coolify-realtime/soketi-entrypoint.sh env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-realtime" jobs: @@ -22,27 +23,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} labels: | coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -50,27 +63,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,25 +103,43 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml new file mode 100644 index 000000000..dd5e6ebd6 --- /dev/null +++ b/.github/workflows/coolify-staging-build.yml @@ -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 }} diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 5fdc32991..95a228114 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -1,14 +1,15 @@ -name: Coolify Testing Host (v4-non-prod) +name: Coolify Testing Host on: push: - branches: [ "main", "next" ] + branches: [ "next" ] paths: - .github/workflows/coolify-testing-host.yml - docker/testing-host/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-testing-host" jobs: @@ -19,21 +20,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + labels: | + coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +55,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + labels: | + coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,21 +90,36 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml deleted file mode 100644 index 268b885ac..000000000 --- a/.github/workflows/development-build.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Development Build (v4) - -on: - push: - branches-ignore: ["main", "v3"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml deleted file mode 100644 index c78c865bf..000000000 --- a/.github/workflows/production-build.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Production Build (v4) - -on: - push: - branches: ["main"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - - templates/service-templates.json - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - aarch64: - runs-on: [self-hosted, arm64] - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php index 69365f921..d38f9c28b 100644 --- a/app/Actions/Application/GenerateConfig.php +++ b/app/Actions/Application/GenerateConfig.php @@ -12,6 +12,7 @@ class GenerateConfig public function handle(Application $application, bool $is_json = false) { ray()->clearAll(); + return $application->generateConfig(is_json: $is_json); } } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index eeddab924..3705ce938 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -21,8 +21,6 @@ class StartRedis { $this->database = $database; - $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; - $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; @@ -37,6 +35,8 @@ class StartRedis $environment_variables = $this->generate_environment_variables(); $this->add_custom_redis(); + $startCommand = $this->buildStartCommand(); + $docker_compose = [ 'services' => [ $container_name => [ @@ -105,7 +105,6 @@ class StartRedis 'target' => '/usr/local/etc/redis/redis.conf', 'read_only' => true, ]; - $docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes"; } // Add custom docker run options @@ -160,12 +159,26 @@ class StartRedis private function generate_environment_variables() { $environment_variables = collect(); - foreach ($this->database->runtime_environment_variables as $env) { - $environment_variables->push("$env->key=$env->real_value"); - } - if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { - $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); + foreach ($this->database->runtime_environment_variables as $env) { + if ($env->is_shared) { + $environment_variables->push("$env->key=$env->real_value"); + + if ($env->key === 'REDIS_PASSWORD') { + $this->database->update(['redis_password' => $env->real_value]); + } + + if ($env->key === 'REDIS_USERNAME') { + $this->database->update(['redis_username' => $env->real_value]); + } + } else { + if ($env->key === 'REDIS_PASSWORD') { + $env->update(['value' => $this->database->redis_password]); + } elseif ($env->key === 'REDIS_USERNAME') { + $env->update(['value' => $this->database->redis_username]); + } + $environment_variables->push("$env->key=$env->real_value"); + } } add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); @@ -173,6 +186,27 @@ class StartRedis return $environment_variables->all(); } + private function buildStartCommand(): string + { + $hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf); + $redisConfPath = '/usr/local/etc/redis/redis.conf'; + + if ($hasRedisConf) { + $confContent = $this->database->redis_conf; + $hasRequirePass = str_contains($confContent, 'requirepass'); + + if ($hasRequirePass) { + $command = "redis-server $redisConfPath"; + } else { + $command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}"; + } + } else { + $command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; + } + + return $command; + } + private function add_custom_redis() { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index ed563eaae..1cabc6418 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -3,8 +3,6 @@ namespace App\Actions\Docker; use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Proxy\CheckProxy; -use App\Actions\Proxy\StartProxy; use App\Actions\Shared\ComplexStatusCheck; use App\Models\ApplicationPreview; use App\Models\Server; @@ -49,323 +47,8 @@ class GetContainersStatus $this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) { return ! $skip_these_applications->pluck('id')->contains($value->id); }); - $this->old_way(); - // if ($this->server->isSwarm()) { - // $this->old_way(); - // } else { - // if (!$this->server->is_metrics_enabled) { - // $this->old_way(); - // return; - // } - // $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false); - // $sentinel_found = json_decode($sentinel_found, true); - // $status = data_get($sentinel_found, '0.State.Status', 'exited'); - // if ($status === 'running') { - // ray('Checking with Sentinel'); - // $this->sentinel(); - // } else { - // ray('Checking the Old way'); - // $this->old_way(); - // } - // } - } - - // private function sentinel() - // { - // try { - // $this->containers = $this->server->getContainersWithSentinel(); - // if ($this->containers->count() === 0) { - // return; - // } - // $databases = $this->server->databases(); - // $services = $this->server->services()->get(); - // $previews = $this->server->previews(); - // $foundApplications = []; - // $foundApplicationPreviews = []; - // $foundDatabases = []; - // $foundServices = []; - - // foreach ($this->containers as $container) { - // $labels = Arr::undot(data_get($container, 'labels')); - // $containerStatus = data_get($container, 'state'); - // $containerHealth = data_get($container, 'health_status', 'unhealthy'); - // $containerStatus = "$containerStatus ($containerHealth)"; - // $applicationId = data_get($labels, 'coolify.applicationId'); - // if ($applicationId) { - // $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - // if ($pullRequestId) { - // if (str($applicationId)->contains('-')) { - // $applicationId = str($applicationId)->before('-'); - // } - // $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - // if ($preview) { - // $foundApplicationPreviews[] = $preview->id; - // $statusFromDb = $preview->status; - // if ($statusFromDb !== $containerStatus) { - // $preview->update(['status' => $containerStatus]); - // } - // } else { - // //Notify user that this container should not be there. - // } - // } else { - // $application = $this->applications->where('id', $applicationId)->first(); - // if ($application) { - // $foundApplications[] = $application->id; - // $statusFromDb = $application->status; - // if ($statusFromDb !== $containerStatus) { - // $application->update(['status' => $containerStatus]); - // } - // } else { - // //Notify user that this container should not be there. - // } - // } - // } else { - // $uuid = data_get($labels, 'com.docker.compose.service'); - // $type = data_get($labels, 'coolify.type'); - // if ($uuid) { - // if ($type === 'service') { - // $database_id = data_get($labels, 'coolify.service.subId'); - // if ($database_id) { - // $service_db = ServiceDatabase::where('id', $database_id)->first(); - // if ($service_db) { - // $uuid = $service_db->service->uuid; - // $isPublic = data_get($service_db, 'is_public'); - // if ($isPublic) { - // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - // } else { - // return data_get($value, 'name') === "$uuid-proxy"; - // } - // })->first(); - // if (! $foundTcpProxy) { - // StartDatabaseProxy::run($service_db); - // // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - // } - // } - // } - // } - // } else { - // $database = $databases->where('uuid', $uuid)->first(); - // if ($database) { - // $isPublic = data_get($database, 'is_public'); - // $foundDatabases[] = $database->id; - // $statusFromDb = $database->status; - // if ($statusFromDb !== $containerStatus) { - // $database->update(['status' => $containerStatus]); - // } - // if ($isPublic) { - // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - // } else { - // return data_get($value, 'name') === "$uuid-proxy"; - // } - // })->first(); - // if (! $foundTcpProxy) { - // StartDatabaseProxy::run($database); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - // } - // } - // } else { - // // Notify user that this container should not be there. - // } - // } - // } - // if (data_get($container, 'name') === 'coolify-db') { - // $foundDatabases[] = 0; - // } - // } - // $serviceLabelId = data_get($labels, 'coolify.serviceId'); - // if ($serviceLabelId) { - // $subType = data_get($labels, 'coolify.service.subType'); - // $subId = data_get($labels, 'coolify.service.subId'); - // $service = $services->where('id', $serviceLabelId)->first(); - // if (! $service) { - // continue; - // } - // if ($subType === 'application') { - // $service = $service->applications()->where('id', $subId)->first(); - // } else { - // $service = $service->databases()->where('id', $subId)->first(); - // } - // if ($service) { - // $foundServices[] = "$service->id-$service->name"; - // $statusFromDb = $service->status; - // if ($statusFromDb !== $containerStatus) { - // // ray('Updating status: ' . $containerStatus); - // $service->update(['status' => $containerStatus]); - // } - // } - // } - // } - // $exitedServices = collect([]); - // foreach ($services as $service) { - // $apps = $service->applications()->get(); - // $dbs = $service->databases()->get(); - // foreach ($apps as $app) { - // if (in_array("$app->id-$app->name", $foundServices)) { - // continue; - // } else { - // $exitedServices->push($app); - // } - // } - // foreach ($dbs as $db) { - // if (in_array("$db->id-$db->name", $foundServices)) { - // continue; - // } else { - // $exitedServices->push($db); - // } - // } - // } - // $exitedServices = $exitedServices->unique('id'); - // foreach ($exitedServices as $exitedService) { - // if (str($exitedService->status)->startsWith('exited')) { - // continue; - // } - // $name = data_get($exitedService, 'name'); - // $fqdn = data_get($exitedService, 'fqdn'); - // if ($name) { - // if ($fqdn) { - // $containerName = "$name, available at $fqdn"; - // } else { - // $containerName = $name; - // } - // } else { - // if ($fqdn) { - // $containerName = $fqdn; - // } else { - // $containerName = null; - // } - // } - // $projectUuid = data_get($service, 'environment.project.uuid'); - // $serviceUuid = data_get($service, 'uuid'); - // $environmentName = data_get($service, 'environment.name'); - - // if ($projectUuid && $serviceUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - // } else { - // $url = null; - // } - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // $exitedService->update(['status' => 'exited']); - // } - - // $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - // foreach ($notRunningApplications as $applicationId) { - // $application = $this->applications->where('id', $applicationId)->first(); - // if (str($application->status)->startsWith('exited')) { - // continue; - // } - // $application->update(['status' => 'exited']); - - // $name = data_get($application, 'name'); - // $fqdn = data_get($application, 'fqdn'); - - // $containerName = $name ? "$name ($fqdn)" : $fqdn; - - // $projectUuid = data_get($application, 'environment.project.uuid'); - // $applicationUuid = data_get($application, 'uuid'); - // $environment = data_get($application, 'environment.name'); - - // if ($projectUuid && $applicationUuid && $environment) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - // } else { - // $url = null; - // } - - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - // $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); - // foreach ($notRunningApplicationPreviews as $previewId) { - // $preview = $previews->where('id', $previewId)->first(); - // if (str($preview->status)->startsWith('exited')) { - // continue; - // } - // $preview->update(['status' => 'exited']); - - // $name = data_get($preview, 'name'); - // $fqdn = data_get($preview, 'fqdn'); - - // $containerName = $name ? "$name ($fqdn)" : $fqdn; - - // $projectUuid = data_get($preview, 'application.environment.project.uuid'); - // $environmentName = data_get($preview, 'application.environment.name'); - // $applicationUuid = data_get($preview, 'application.uuid'); - - // if ($projectUuid && $applicationUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - // } else { - // $url = null; - // } - - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - // $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); - // foreach ($notRunningDatabases as $database) { - // $database = $databases->where('id', $database)->first(); - // if (str($database->status)->startsWith('exited')) { - // continue; - // } - // $database->update(['status' => 'exited']); - - // $name = data_get($database, 'name'); - // $fqdn = data_get($database, 'fqdn'); - - // $containerName = $name; - - // $projectUuid = data_get($database, 'environment.project.uuid'); - // $environmentName = data_get($database, 'environment.name'); - // $databaseUuid = data_get($database, 'uuid'); - - // if ($projectUuid && $databaseUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - // } else { - // $url = null; - // } - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - - // // Check if proxy is running - // $this->server->proxyType(); - // $foundProxyContainer = $this->containers->filter(function ($value, $key) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - // } else { - // return data_get($value, 'name') === 'coolify-proxy'; - // } - // })->first(); - // if (! $foundProxyContainer) { - // try { - // $shouldStart = CheckProxy::run($this->server); - // if ($shouldStart) { - // StartProxy::run($this->server, false); - // $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - // } - // } catch (\Throwable $e) { - // ray($e); - // } - // } else { - // $this->server->proxy->status = data_get($foundProxyContainer, 'state'); - // $this->server->save(); - // $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - // instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - // } - // } catch (\Exception $e) { - // // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); - // ray($e->getMessage()); - - // return handleError($e); - // } - // } - - private function old_way() - { if ($this->containers === null) { - ['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); + ['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); } if (is_null($this->containers)) { @@ -650,32 +333,5 @@ class GetContainersStatus } // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); } - - if (! $this->server->proxySet() || $this->server->proxy->force_stop) { - return; - } - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } } diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php new file mode 100644 index 000000000..15c892e75 --- /dev/null +++ b/app/Actions/Server/DeleteServer.php @@ -0,0 +1,17 @@ +forceDelete(); + } +} diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index f671f2d2a..2e1df8185 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -17,7 +17,7 @@ class InstallDocker throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type); - $dockerVersion = '24.0'; + $dockerVersion = '26.0'; $config = base64_encode('{ "log-driver": "json-file", "log-opts": { diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php index 9b6741211..43376efe5 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/InstallLogDrain.php @@ -209,6 +209,8 @@ Files: ]; $command = array_merge($command, $add_envs_command, $restart_command); + StopLogDrain::run($server); + return instant_remote_process($command, $server); } catch (\Throwable $e) { return handleError($e); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index b79bc8f67..aef7282e3 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -9,18 +9,55 @@ class StartSentinel { use AsAction; - public function handle(Server $server, $version = 'latest', bool $restart = false) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) { + if ($server->isSwarm() || $server->isBuildServer()) { + return; + } if ($restart) { StopSentinel::run($server); } - $metrics_history = $server->settings->metrics_history_days; - $refresh_rate = $server->settings->metrics_refresh_rate_seconds; - $token = $server->settings->metrics_token; + $version = $latestVersion ?? get_latest_sentinel_version(); + $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days'); + $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $token = data_get($server, 'settings.sentinel_token'); + $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $mountDir = '/data/coolify/sentinel'; + $image = "ghcr.io/coollabsio/sentinel:$version"; + if (! $endpoint) { + throw new \Exception('You should set FQDN in Instance Settings.'); + } + $environments = [ + 'TOKEN' => $token, + 'PUSH_ENDPOINT' => $endpoint, + 'PUSH_INTERVAL_SECONDS' => $pushInterval, + 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory, + ]; + $labels = [ + 'coolify.managed' => 'true', + ]; + if (isDev()) { + // data_set($environments, 'DEBUG', 'true'); + // $image = 'sentinel'; + $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; + } + $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; + $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels)); + $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image"; + instant_remote_process([ - "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", - 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', - 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, true); + 'docker rm -f coolify-sentinel || true', + "mkdir -p $mountDir", + $dockerCommand, + "chown -R 9999:root $mountDir", + "chmod -R 700 $mountDir", + ], $server); + + $server->settings->is_sentinel_enabled = true; + $server->settings->save(); + $server->sentinelHeartbeat(); } } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php index 21ffca3bd..aecb96c87 100644 --- a/app/Actions/Server/StopSentinel.php +++ b/app/Actions/Server/StopSentinel.php @@ -12,5 +12,6 @@ class StopSentinel public function handle(Server $server) { instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + $server->sentinelHeartbeat(isReset: true); } } diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 36722564c..ded729114 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -15,7 +15,6 @@ use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\StatusChanged; use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupSuccess; -use App\Notifications\Database\DailyBackup; use App\Notifications\Test; use Exception; use Illuminate\Console\Command; @@ -121,23 +120,6 @@ class Emails extends Command $this->mail = (new Test)->toMail(); $this->sendEmail(); break; - case 'database-backup-statuses-daily': - $scheduled_backups = ScheduledDatabaseBackup::all(); - $databases = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - if ($last_days_backups->isEmpty()) { - continue; - } - $failed = $last_days_backups->where('status', 'failed'); - $database = $scheduled_backup->database; - $databases->put($database->name, [ - 'failed_count' => $failed->count(), - ]); - } - $this->mail = (new DailyBackup($databases))->toMail(); - $this->sendEmail(); - break; case 'application-deployment-success-daily': $applications = Application::all(); foreach ($applications as $application) { diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1430fcdd1..1e55aa57f 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,17 +2,16 @@ namespace App\Console; +use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; +use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; -use App\Jobs\PullHelperImageJob; -use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; -use App\Jobs\ServerStorageCheckJob; use App\Jobs\UpdateCoolifyJob; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; @@ -20,14 +19,15 @@ use App\Models\Server; use App\Models\Team; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Illuminate\Support\Carbon; class Kernel extends ConsoleKernel { - private $all_servers; + private $allServers; protected function schedule(Schedule $schedule): void { - $this->all_servers = Server::all(); + $this->allServers = Server::all(); $settings = instanceSettings(); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -37,56 +37,51 @@ class Kernel extends ConsoleKernel $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkScheduledBackups($schedule); + $this->checkResources($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('uploads:clear')->everyTwoMinutes(); $schedule->command('telescope:prune')->daily(); - $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer(); + $schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - $this->schedule_updates($schedule); + $this->scheduleUpdates($schedule); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->pull_images($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkScheduledBackups($schedule); + $this->checkResources($schedule); + $this->pullImages($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('cleanup:database --yes')->daily(); $schedule->command('uploads:clear')->everyTwoMinutes(); } } - private function pull_images($schedule) + private function pullImages($schedule): void { $settings = instanceSettings(); - $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { $schedule->job(function () use ($server) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - PullSentinelImageJob::dispatch($server); - } + CheckAndStartSentinelJob::dispatch($server); })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); } } - $schedule->job(new PullHelperImageJob) + $schedule->job(new CheckHelperImageJob) ->cron($settings->update_check_frequency) ->timezone($settings->instance_timezone) ->onOneServer(); } - private function schedule_updates($schedule) + private function scheduleUpdates($schedule): void { $settings = instanceSettings(); @@ -105,19 +100,21 @@ class Kernel extends ConsoleKernel } } - private function check_resources($schedule) + private function checkResources($schedule): void { if (isCloud()) { - $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $own = Team::find(0)->servers; $servers = $servers->merge($own); } else { - $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->where('ip', '!=', '1.2.3.4'); } foreach ($servers as $server) { - $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); - // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer(); + $lastSentinelUpdate = $server->sentinel_updated_at; $serverTimezone = $server->settings->server_timezone; + if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + } if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); } else { @@ -126,7 +123,7 @@ class Kernel extends ConsoleKernel } } - private function check_scheduled_backups($schedule) + private function checkScheduledBackups($schedule): void { $scheduled_backups = ScheduledDatabaseBackup::all(); if ($scheduled_backups->isEmpty()) { @@ -159,7 +156,7 @@ class Kernel extends ConsoleKernel } } - private function check_scheduled_tasks($schedule) + private function checkScheduledTasks($schedule): void { $scheduled_tasks = ScheduledTask::all(); if ($scheduled_tasks->isEmpty()) { diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 2a1f846d3..46dd8120e 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1579,11 +1579,16 @@ class ApplicationsController extends Controller $request->offsetUnset('docker_compose_domains'); } $instantDeploy = $request->instant_deploy; + $isStatic = $request->is_static; + $useBuildServer = $request->use_build_server; - $use_build_server = $request->use_build_server; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } - if (isset($use_build_server)) { - $application->settings->is_build_server_enabled = $use_build_server; + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; $application->settings->save(); } diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 2414b7a42..062cc04e7 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -160,7 +160,7 @@ class OtherController extends Controller #[OA\Get( summary: 'Healthcheck', description: 'Healthcheck endpoint.', - path: '/healthcheck', + path: '/health', operationId: 'healthcheck', responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index a49515579..540069f85 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Api; +use App\Actions\Server\DeleteServer; use App\Actions\Server\ValidateServer; use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; @@ -23,7 +24,7 @@ class ServersController extends Controller return serializeApiResponse($settings); } $settings = $settings->makeHidden([ - 'metrics_token', + 'sentinel_token', ]); return serializeApiResponse($settings); @@ -726,6 +727,7 @@ class ServersController extends Controller return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); } $server->delete(); + DeleteServer::dispatch($server); return response()->json(['message' => 'Server deleted.']); } diff --git a/app/Jobs/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php new file mode 100644 index 000000000..8fbeee663 --- /dev/null +++ b/app/Jobs/CheckAndStartSentinelJob.php @@ -0,0 +1,56 @@ +server, false); + $sentinelFoundJson = json_decode($sentinelFound, true); + $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); + if ($sentinelStatus !== 'running') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + // If sentinel is running, check if it needs an update + $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($runningVersion)) { + $runningVersion = '0.0.0'; + } + if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest'); + + return; + } else { + if (version_compare($runningVersion, $latestVersion, '<')) { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + } + } catch (\Throwable $e) { + throw $e; + } + } +} diff --git a/app/Jobs/CheckHelperImageJob.php b/app/Jobs/CheckHelperImageJob.php new file mode 100644 index 000000000..d62ad601f --- /dev/null +++ b/app/Jobs/CheckHelperImageJob.php @@ -0,0 +1,40 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $settings = instanceSettings(); + $latest_version = data_get($versions, 'coolify.helper.version'); + $current_version = $settings->helper_version; + if (version_compare($latest_version, $current_version, '>')) { + $settings->update(['helper_version' => $latest_version]); + } + } + + } catch (\Throwable $e) { + send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage()); + throw $e; + } + } +} diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 769739d5e..41f4daa4b 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -504,8 +504,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $network = $this->database->destination->network; } - $this->ensureHelperImageAvailable(); - $fullImageName = $this->getFullImageName(); if (isDev()) { @@ -538,35 +536,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } } - private function ensureHelperImageAvailable(): void - { - $fullImageName = $this->getFullImageName(); - - $imageExists = $this->checkImageExists($fullImageName); - - if (! $imageExists) { - $this->pullHelperImage($fullImageName); - } - } - - private function checkImageExists(string $fullImageName): bool - { - $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false); - - return trim($result) === 'exists'; - } - - private function pullHelperImage(string $fullImageName): void - { - try { - instant_remote_process(["docker pull {$fullImageName}"], $this->server); - } catch (\Exception $e) { - $errorMessage = 'Failed to pull helper image: '.$e->getMessage(); - $this->add_to_backup_output($errorMessage); - throw new \RuntimeException($errorMessage); - } - } - private function getFullImageName(): string { $settings = instanceSettings(); diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 4b208fc31..0cc2be84d 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue { @@ -17,28 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 1000; - public function __construct() {} + public function __construct(public Server $server) {} public function handle(): void { try { - $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - $settings = instanceSettings(); - $latest_version = data_get($versions, 'coolify.helper.version'); - $current_version = $settings->helper_version; - if (version_compare($latest_version, $current_version, '>')) { - // New version available - // $helperImage = config('coolify.helper_image'); - // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); - $settings->update(['helper_version' => $latest_version]); - } - } - + $helperImage = config('coolify.helper_image'); + $latest_version = instanceSettings()->helper_version; + instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); } catch (\Throwable $e) { - send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php deleted file mode 100644 index 32f84e6d5..000000000 --- a/app/Jobs/PullSentinelImageJob.php +++ /dev/null @@ -1,47 +0,0 @@ -server, false); - if (empty($local_version)) { - $local_version = '0.0.0'; - } - if (version_compare($local_version, $version, '<')) { - StartSentinel::run($this->server, $version, true); - - return; - } - ray('Sentinel image is up to date'); - } catch (\Throwable $e) { - // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php new file mode 100644 index 000000000..62f059129 --- /dev/null +++ b/app/Jobs/PushServerUpdateJob.php @@ -0,0 +1,404 @@ +containers = collect(); + $this->foundApplicationIds = collect(); + $this->foundDatabaseUuids = collect(); + $this->foundServiceApplicationIds = collect(); + $this->foundApplicationPreviewsIds = collect(); + $this->foundServiceDatabaseIds = collect(); + $this->allApplicationIds = collect(); + $this->allDatabaseUuids = collect(); + $this->allTcpProxyUuids = collect(); + $this->allServiceApplicationIds = collect(); + $this->allServiceDatabaseIds = collect(); + } + + public function handle() + { + // TODO: Swarm is not supported yet + try { + if (! $this->data) { + throw new \Exception('No data provided'); + } + $data = collect($this->data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + if ($this->containers->isEmpty()) { + return; + } + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->previews = $this->server->previews(); + $this->services = $this->server->services()->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { + return $application->additional_servers->count() === 0; + })->pluck('id'); + $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { + return $application->additional_servers->count() > 0; + }); + $this->allApplicationPreviewsIds = $this->previews->pluck('id'); + $this->allDatabaseUuids = $this->databases->pluck('uuid'); + $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); + $this->services->each(function ($service) { + $service->applications()->pluck('id')->each(function ($applicationId) { + $this->allServiceApplicationIds->push($applicationId); + }); + $service->databases()->pluck('id')->each(function ($databaseId) { + $this->allServiceDatabaseIds->push($databaseId); + }); + }); + + ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]); + + foreach ($this->containers as $container) { + $containerStatus = data_get($container, 'state', 'exited'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = collect(data_get($container, 'labels')); + $coolify_managed = $labels->has('coolify.managed'); + if ($coolify_managed) { + $name = data_get($container, 'name'); + if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) { + $this->foundLogDrainContainer = true; + } + if ($labels->has('coolify.applicationId')) { + $applicationId = $labels->get('coolify.applicationId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0'); + try { + if ($pullRequestId === '0') { + if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationIds->push($applicationId); + } + $this->updateApplicationStatus($applicationId, $containerStatus); + } else { + if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationPreviewsIds->push($applicationId); + } + $this->updateApplicationPreviewStatus($applicationId, $containerStatus); + } + } catch (\Exception $e) { + ray()->error($e); + } + } elseif ($labels->has('coolify.serviceId')) { + $serviceId = $labels->get('coolify.serviceId'); + $subType = $labels->get('coolify.service.subType'); + $subId = $labels->get('coolify.service.subId'); + if ($subType === 'application' && $this->isRunning($containerStatus)) { + $this->foundServiceApplicationIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } elseif ($subType === 'database' && $this->isRunning($containerStatus)) { + $this->foundServiceDatabaseIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } + + } else { + $uuid = $labels->get('com.docker.compose.service'); + $type = $labels->get('coolify.type'); + if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) { + $this->foundProxy = true; + } elseif ($type === 'service' && $this->isRunning($containerStatus)) { + ray("Service: $uuid, $containerStatus"); + } else { + if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->foundDatabaseUuids->push($uuid); + if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); + } else { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false); + } + } + } + } + } + } + + $this->updateProxyStatus(); + + $this->updateNotFoundApplicationStatus(); + $this->updateNotFoundApplicationPreviewStatus(); + $this->updateNotFoundDatabaseStatus(); + $this->updateNotFoundServiceStatus(); + + $this->updateAdditionalServersStatus(); + + $this->checkLogDrainContainer(); + + } catch (\Exception $e) { + throw $e; + } + + } + + private function updateApplicationStatus(string $applicationId, string $containerStatus) + { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]); + } + + private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus) + { + $application = $this->previews->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]); + } + + private function updateNotFoundApplicationStatus() + { + $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); + if ($notFoundApplicationIds->isNotEmpty()) { + ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]); + $notFoundApplicationIds->each(function ($applicationId) { + ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']); + $application = Application::find($applicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']); + } + }); + } + } + + private function updateNotFoundApplicationPreviewStatus() + { + $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); + if ($notFoundApplicationPreviewsIds->isNotEmpty()) { + ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]); + $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) { + ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']); + $applicationPreview = ApplicationPreview::find($applicationPreviewId); + if ($applicationPreview) { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']); + } + }); + } + } + + private function updateProxyStatus() + { + // If proxy is not found, start it + if ($this->server->isProxyShouldRun()) { + if ($this->foundProxy === false) { + try { + if (CheckProxy::run($this->server)) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + + } + + private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false) + { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if (! $database) { + return; + } + $database->status = $containerStatus; + $database->save(); + ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]); + if ($this->isRunning($containerStatus) && $tcpProxy) { + $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { + return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; + })->first(); + if (! $tcpProxyContainerFound) { + ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]); + StartDatabaseProxy::dispatch($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } else { + ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]); + } + } + } + + private function updateNotFoundDatabaseStatus() + { + $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); + if ($notFoundDatabaseUuids->isNotEmpty()) { + ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]); + $notFoundDatabaseUuids->each(function ($databaseUuid) { + ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']); + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if ($database) { + $database->status = 'exited'; + $database->save(); + ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']); + ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]); + if ($database->is_public) { + ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]); + StopDatabaseProxy::dispatch($database); + } + } + }); + } + } + + private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) + { + $service = $this->services->where('id', $serviceId)->first(); + if (! $service) { + return; + } + if ($subType === 'application') { + $application = $service->applications()->where('id', $subId)->first(); + $application->status = $containerStatus; + $application->save(); + ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } elseif ($subType === 'database') { + $database = $service->databases()->where('id', $subId)->first(); + $database->status = $containerStatus; + $database->save(); + ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } else { + ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } + } + + private function updateNotFoundServiceStatus() + { + $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); + $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + if ($notFoundServiceApplicationIds->isNotEmpty()) { + ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]); + $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { + ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']); + $application = ServiceApplication::find($serviceApplicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']); + } + }); + } + if ($notFoundServiceDatabaseIds->isNotEmpty()) { + ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]); + $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { + ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']); + $database = ServiceDatabase::find($serviceDatabaseId); + if ($database) { + $database->status = 'exited'; + $database->save(); + ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']); + } + }); + } + } + + private function updateAdditionalServersStatus() + { + $this->allApplicationsWithAdditionalServers->each(function ($application) { + ray('Updating additional servers status for application', ['application_id' => $application->id]); + ComplexStatusCheck::run($application); + }); + } + + private function isRunning(string $containerStatus) + { + return str($containerStatus)->contains('running'); + } + + private function checkLogDrainContainer() + { + if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { + InstallLogDrain::dispatch($this->server); + } + } +} diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index f38cf823c..5b406f50f 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -29,7 +30,7 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue public int $maxExceptions = 5; public function __construct( - public string $text, + public DiscordMessage $message, public string $webhookUrl ) {} @@ -38,9 +39,6 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue */ public function handle(): void { - $payload = [ - 'content' => $this->text, - ]; - Http::post($this->webhookUrl, $payload); + Http::post($this->webhookUrl, $this->message->toPayload()); } } diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 39d4aa0c0..5ac98e954 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -2,14 +2,11 @@ namespace App\Jobs; -use App\Actions\Database\StartDatabaseProxy; use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Actions\Server\InstallLogDrain; -use App\Models\ApplicationPreview; use App\Models\Server; -use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -17,7 +14,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Arr; class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue { @@ -47,31 +43,57 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public function handle() { try { + if ($this->server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + $this->applications = $this->server->applications(); $this->databases = $this->server->databases(); $this->services = $this->server->services()->get(); $this->previews = $this->server->previews(); - $up = $this->serverStatus(); - if (! $up) { - ray('Server is not reachable.'); - - return 'Server is not reachable.'; - } - if (! $this->server->isFunctional()) { - ray('Server is not ready.'); - - return 'Server is not ready.'; - } if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); if (is_null($this->containers)) { return 'No containers found.'; } + ServerStorageCheckJob::dispatch($this->server); GetContainersStatus::run($this->server, $this->containers, $containerReplicates); + + if ($this->server->isSentinelEnabled()) { + CheckAndStartSentinelJob::dispatch($this->server); + } + if ($this->server->isLogDrainEnabled()) { $this->checkLogDrainContainer(); } + + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $this->server->proxyType(); + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + ray($e); + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } } } catch (\Throwable $e) { @@ -82,39 +104,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue } - private function serverStatus() - { - ['uptime' => $uptime] = $this->server->validateConnection(false); - if ($uptime) { - if ($this->server->unreachable_notification_sent === true) { - $this->server->update(['unreachable_notification_sent' => false]); - } - } else { - // $this->server->team?->notify(new Unreachable($this->server)); - foreach ($this->applications as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - - return false; - } - - return true; - - } - private function checkLogDrainContainer() { $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { @@ -129,289 +118,4 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue InstallLogDrain::dispatch($this->server); } } - - private function containerStatus() - { - - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; - - foreach ($this->containers as $container) { - if ($this->server->isSwarm()) { - $labels = data_get($container, 'Spec.Labels'); - $uuid = data_get($labels, 'coolify.name'); - } else { - $labels = data_get($container, 'Config.Labels'); - } - $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; - $labels = Arr::undot(format_docker_labels_to_json($labels)); - $applicationId = data_get($labels, 'coolify.applicationId'); - if ($applicationId) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - if ($pullRequestId) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $foundApplicationPreviews[] = $preview->id; - $statusFromDb = $preview->status; - if ($statusFromDb !== $containerStatus) { - $preview->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } else { - $application = $this->applications->where('id', $applicationId)->first(); - if ($application) { - $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } - } else { - $uuid = data_get($labels, 'com.docker.compose.service'); - $type = data_get($labels, 'coolify.type'); - - if ($uuid) { - if ($type === 'service') { - $database_id = data_get($labels, 'coolify.service.subId'); - if ($database_id) { - $service_db = ServiceDatabase::where('id', $database_id)->first(); - if ($service_db) { - $uuid = data_get($service_db, 'service.uuid'); - if ($uuid) { - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - } - } - } - } - } - } else { - $database = $this->databases->where('uuid', $uuid)->first(); - if ($database) { - $isPublic = data_get($database, 'is_public'); - $foundDatabases[] = $database->id; - $statusFromDb = $database->status; - if ($statusFromDb !== $containerStatus) { - $database->update(['status' => $containerStatus]); - } - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - } - } - } else { - // Notify user that this container should not be there. - } - } - } - if (data_get($container, 'Name') === '/coolify-db') { - $foundDatabases[] = 0; - } - } - $serviceLabelId = data_get($labels, 'coolify.serviceId'); - if ($serviceLabelId) { - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = $this->services->where('id', $serviceLabelId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); - } else { - $service = $service->databases()->where('id', $subId)->first(); - } - if ($service) { - $foundServices[] = "$service->id-$service->name"; - $statusFromDb = $service->status; - if ($statusFromDb !== $containerStatus) { - // ray('Updating status: ' . $containerStatus); - $service->update(['status' => $containerStatus]); - } - } - } - } - $exitedServices = collect([]); - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - if (in_array("$app->id-$app->name", $foundServices)) { - continue; - } else { - $exitedServices->push($app); - } - } - foreach ($dbs as $db) { - if (in_array("$db->id-$db->name", $foundServices)) { - continue; - } else { - $exitedServices->push($db); - } - } - } - $exitedServices = $exitedServices->unique('id'); - foreach ($exitedServices as $exitedService) { - if (str($exitedService->status)->startsWith('exited')) { - continue; - } - $name = data_get($exitedService, 'name'); - $fqdn = data_get($exitedService, 'fqdn'); - if ($name) { - if ($fqdn) { - $containerName = "$name, available at $fqdn"; - } else { - $containerName = $name; - } - } else { - if ($fqdn) { - $containerName = $fqdn; - } else { - $containerName = null; - } - } - $projectUuid = data_get($service, 'environment.project.uuid'); - $serviceUuid = data_get($service, 'uuid'); - $environmentName = data_get($service, 'environment.name'); - - if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - $exitedService->update(['status' => 'exited']); - } - - $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $this->applications->where('id', $applicationId)->first(); - if (str($application->status)->startsWith('exited')) { - continue; - } - $application->update(['status' => 'exited']); - - $name = data_get($application, 'name'); - $fqdn = data_get($application, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); - - if ($projectUuid && $applicationUuid && $environment) { - $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $this->previews->where('id', $previewId)->first(); - if (str($preview->status)->startsWith('exited')) { - continue; - } - $preview->update(['status' => 'exited']); - - $name = data_get($preview, 'name'); - $fqdn = data_get($preview, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($preview, 'application.environment.project.uuid'); - $environmentName = data_get($preview, 'application.environment.name'); - $applicationUuid = data_get($preview, 'application.uuid'); - - if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $this->databases->where('id', $database)->first(); - if (str($database->status)->startsWith('exited')) { - continue; - } - $database->update(['status' => 'exited']); - - $name = data_get($database, 'name'); - $fqdn = data_get($database, 'fqdn'); - - $containerName = $name; - - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); - - if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } - } } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php deleted file mode 100644 index fcc33c859..000000000 --- a/app/Jobs/ServerStatusJob.php +++ /dev/null @@ -1,60 +0,0 @@ -server->isServerReady($this->tries)) { - throw new \RuntimeException('Server is not ready.'); - } - try { - if ($this->server->isFunctional()) { - $this->remove_unnecessary_coolify_yaml(); - if ($this->server->isSentinelEnabled()) { - $this->server->checkSentinel(); - } - } - } catch (\Throwable $e) { - // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - - return handleError($e); - } - - } - - private function remove_unnecessary_coolify_yaml() - { - // This will remote the coolify.yaml file from the server as it is not needed on cloud servers - if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $this->server, false); - } - } -} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 376cb8532..c646f77eb 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -3,12 +3,14 @@ namespace App\Jobs; use App\Models\Server; +use App\Notifications\Server\HighDiskUsage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\RateLimiter; class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue { @@ -18,40 +20,47 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 60; - public $containers; - - public $applications; - - public $databases; - - public $services; - - public $previews; - public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Server $server) {} + public function __construct(public Server $server, public ?int $percentage = null) {} public function handle() { try { if (! $this->server->isFunctional()) { - ray('Server is not ready.'); - return 'Server is not ready.'; } - $team = $this->server->team; - $percentage = $this->server->storageCheck(); - if ($percentage > 1) { - ray('Server storage is at '.$percentage.'%'); + $team = data_get($this->server, 'team'); + $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold'); + + if (is_null($this->percentage)) { + $this->percentage = $this->server->storageCheck(); + loggy('Server storage check percentage: '.$this->percentage); + } + if (! $this->percentage) { + return 'No percentage could be retrieved.'; + } + if ($this->percentage > $serverDiskUsageNotificationThreshold) { + $executed = RateLimiter::attempt( + 'high-disk-usage:'.$this->server->id, + $maxAttempts = 0, + function () use ($team, $serverDiskUsageNotificationThreshold) { + $team->notify(new HighDiskUsage($this->server, $this->percentage, $serverDiskUsageNotificationThreshold)); + }, + $decaySeconds = 3600, + ); + + if (! $executed) { + return 'Too many messages sent!'; + } + } else { + RateLimiter::hit('high-disk-usage:'.$this->server->id, 600); } } catch (\Throwable $e) { - ray($e->getMessage()); - return handleError($e); } diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 26b31e515..16cd9152e 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -14,6 +14,18 @@ class Index extends Component public $search = ''; + public function mount() + { + if (! isCloud()) { + return redirect()->route('dashboard'); + } + + if (auth()->user()->id !== 0) { + return redirect()->route('dashboard'); + } + $this->getSubscribers(); + } + public function submitSearch() { if ($this->search !== '') { @@ -38,17 +50,6 @@ class Index extends Component } } - public function mount() - { - if (! isCloud()) { - return redirect()->route('dashboard'); - } - if (auth()->user()->id !== 0) { - return redirect()->route('dashboard'); - } - $this->getSubscribers(); - } - public function getSubscribers() { $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index b7dd8656d..c9c3092b3 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -85,26 +85,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->remoteServerDescription = 'Created by Coolify'; $this->remoteServerHost = 'coolify-testing-host'; } - // if ($this->currentState === 'create-project') { - // $this->getProjects(); - // } - // if ($this->currentState === 'create-resource') { - // $this->selectExistingServer(); - // $this->selectExistingProject(); - // } - // if ($this->currentState === 'private-key') { - // $this->setServerType('remote'); - // } - // if ($this->currentState === 'create-server') { - // $this->selectExistingPrivateKey(); - // } - // if ($this->currentState === 'validate-server') { - // $this->selectExistingServer(); - // } - // if ($this->currentState === 'select-existing-server') { - // $this->selectExistingServer(); - // } - } public function explanation() diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 5650e82ba..37583a944 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -66,7 +66,7 @@ class Show extends Component return ! $alreadyAddedNetworks->contains('network', $network['Name']); }); if ($this->networks->count() === 0) { - $this->dispatch('success', 'No new networks found.'); + $this->dispatch('success', 'No new destinations found on this server.'); return; } diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index a732ef1c9..73ce5bcc0 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -24,6 +24,9 @@ class ForcePasswordReset extends Component public function mount() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } $this->email = auth()->user()->email; } @@ -34,6 +37,10 @@ class ForcePasswordReset extends Component public function submit() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } + try { $this->rateLimit(10); $this->validate(); diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index 988add7c8..10ba0c86a 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -2,6 +2,7 @@ namespace App\Livewire; +use App\Models\InstanceSettings; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -18,10 +19,12 @@ class NavbarDeleteTeam extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $currentTeam = currentTeam(); diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 65c202b7d..49666dabd 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -18,6 +18,7 @@ class Discord extends Component 'team.discord_notifications_status_changes' => 'nullable|boolean', 'team.discord_notifications_database_backups' => 'nullable|boolean', 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', + 'team.discord_notifications_server_disk_usage' => 'nullable|boolean', ]; protected $validationAttributes = [ diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 53673292e..08415731d 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -4,6 +4,7 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; +use Illuminate\Support\Facades\RateLimiter; use Livewire\Component; class Email extends Component @@ -30,6 +31,7 @@ class Email extends Component 'team.smtp_notifications_status_changes' => 'nullable|boolean', 'team.smtp_notifications_database_backups' => 'nullable|boolean', 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', + 'team.smtp_notifications_server_disk_usage' => 'nullable|boolean', 'team.use_instance_email_settings' => 'boolean', 'team.resend_enabled' => 'nullable|boolean', 'team.resend_api_key' => 'nullable', @@ -74,8 +76,23 @@ class Email extends Component public function sendTestNotification() { - $this->team?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test Email sent.'); + try { + $executed = RateLimiter::attempt( + 'test-email:'.$this->team->id, + $perMinute = 0, + function () { + $this->team?->notify(new Test($this->emails)); + $this->dispatch('success', 'Test Email sent.'); + }, + $decaySeconds = 10, + ); + + if (! $executed) { + throw new \Exception('Too many messages sent!'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function instantSaveInstance() diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index e163a25e0..a035df71c 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -24,6 +24,7 @@ class Telegram extends Component 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', + 'team.telegram_notifications_server_disk_usage' => 'nullable|boolean', ]; protected $validationAttributes = [ diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 75c8feea6..f1575a01f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -6,6 +6,7 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; use Illuminate\Support\Collection; use Livewire\Component; +use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class General extends Component @@ -183,9 +184,7 @@ class General extends Component $storage->save(); }); } - } - } public function loadComposeFile($isInit = false) @@ -242,24 +241,6 @@ class General extends Component } } - public function updatedApplicationFqdn() - { - try { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } catch (\Throwable $e) { - $originalFqdn = $this->application->getOriginal('fqdn'); - $this->application->fqdn = $originalFqdn; - return handleError($e, $this); - } - $this->resetDefaultLabels(); - } - public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -293,10 +274,10 @@ class General extends Component } } - public function resetDefaultLabels() + public function resetDefaultLabels($manualReset = false) { try { - if ($this->application->settings->is_container_label_readonly_enabled) { + if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { return; } $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); @@ -349,15 +330,24 @@ class General extends Component public function submit($showToaster = true) { try { - if ($this->application->isDirty('redirect')) { - $this->set_redirect(); - } $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } + $this->resetDefaultLabels(); + + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->checkFqdns(); @@ -418,13 +408,19 @@ class General extends Component } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); - $showToaster && $this->dispatch('success', 'Application settings updated!'); + $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + return handleError($e, $this); } finally { $this->dispatch('configurationChanged'); } } + public function downloadConfig() { $config = GenerateConfig::run($this->application, true); @@ -434,7 +430,7 @@ class General extends Component echo $config; }, $fileName, [ 'Content-Type' => 'application/json', - 'Content-Disposition' => 'attachment; filename=' . $fileName, + 'Content-Disposition' => 'attachment; filename='.$fileName, ]); } } diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index e4f100fcf..9a0b9b851 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -31,10 +31,14 @@ class Form extends Component public function generate_real_url() { if (data_get($this->application, 'fqdn')) { - $firstFqdn = str($this->application->fqdn)->before(','); - $url = Url::fromString($firstFqdn); - $host = $url->getHost(); - $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + try { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } catch (\Exception $e) { + $this->dispatch('error', 'Invalid FQDN.'); + } } } diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 7e2e4a12b..016218b78 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -58,10 +59,12 @@ class BackupEdit extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index c8c33a022..d7ac18097 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,10 +2,10 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; -use Livewire\Attributes\On; use Livewire\Component; class BackupExecutions extends Component @@ -28,7 +28,6 @@ class BackupExecutions extends Component return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - 'deleteBackup', ]; } @@ -41,13 +40,14 @@ class BackupExecutions extends Component } } - #[On('deleteBackup')] public function deleteBackup($executionId, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $execution = $this->backup->executions()->where('id', $executionId)->first(); diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 72fd95de8..25a96b292 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -11,12 +11,21 @@ use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; + protected $listeners = [ + 'envsUpdated' => 'refresh', + 'refresh', + ]; public Server $server; public StandaloneRedis $database; + public string $redis_username; + + public string $redis_password; + + public string $redis_version; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -25,33 +34,33 @@ class General extends Component 'database.name' => 'required', 'database.description' => 'nullable', 'database.redis_conf' => 'nullable', - 'database.redis_password' => 'required', 'database.image' => 'required', 'database.ports_mappings' => 'nullable', 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', 'database.custom_docker_run_options' => 'nullable', + 'redis_username' => 'required', + 'redis_password' => 'required', ]; protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', 'database.redis_conf' => 'Redis Configuration', - 'database.redis_password' => 'Redis Password', 'database.image' => 'Image', 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', 'database.custom_docker_run_options' => 'Custom Docker Options', + 'redis_username' => 'Redis Username', + 'redis_password' => 'Redis Password', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - + $this->refreshView(); } public function instantSaveAdvanced() @@ -75,13 +84,24 @@ class General extends Component { try { $this->validate(); - if ($this->database->redis_conf === '') { - $this->database->redis_conf = null; + + if (version_compare($this->redis_version, '6.0', '>=')) { + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_USERNAME'], + ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] + ); } + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_PASSWORD'], + ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] + ); + $this->database->save(); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); + } finally { + $this->dispatch('refreshEnvs'); } } @@ -119,10 +139,25 @@ class General extends Component public function refresh(): void { $this->database->refresh(); + $this->refreshView(); + } + + private function refreshView() + { + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + $this->redis_version = $this->database->getRedisVersion(); + $this->redis_username = $this->database->redis_username; + $this->redis_password = $this->database->redis_password; } public function render() { return view('livewire.project.database.redis.general'); } + + public function isSharedVariable($name) + { + return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists(); + } } diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index e01741770..6d8c3aff7 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -7,18 +7,22 @@ use Livewire\Component; class DeleteEnvironment extends Component { - public array $parameters; - public int $environment_id; public bool $disabled = false; public string $environmentName = ''; + public array $parameters; + public function mount() { - $this->parameters = get_route_parameters(); - $this->environmentName = Environment::findOrFail($this->environment_id)->name; + try { + $this->environmentName = Environment::findOrFail($this->environment_id)->name; + $this->parameters = get_route_parameters(); + } catch (\Exception $e) { + return handleError($e, $this); + } } public function delete() @@ -30,7 +34,7 @@ class DeleteEnvironment extends Component if ($environment->isEmpty()) { $environment->delete(); - return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); + return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]); } return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0e4f15a5c..f8eb838be 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -18,7 +18,11 @@ class Index extends Component public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) { + $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]); + + return $project; + }); $this->servers = Server::ownedByCurrentTeam()->count(); } diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 971d4700b..a6601a898 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -317,6 +317,7 @@ class PublicGitRepository extends Component // $application->setConfig($config); // } } + return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index 71ce2c356..283496887 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -32,8 +32,11 @@ class Index extends Component public $services = []; + public array $parameters; + public function mount() { + $this->parameters = get_route_parameters(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (! $project) { return redirect()->route('dashboard'); @@ -44,7 +47,6 @@ class Index extends Component } $this->project = $project; $this->environment = $environment; - $this->applications = $this->environment->applications->load(['tags']); $this->applications = $this->applications->map(function ($application) { if (data_get($application, 'environment.project.uuid')) { diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index cdec580bb..e89aeda85 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Models\ServiceApplication; use Livewire\Component; +use Spatie\Url\Url; class EditDomain extends Component { @@ -21,24 +22,21 @@ class EditDomain extends Component $this->application = ServiceApplication::find($this->applicationId); } - public function updatedApplicationFqdn() + public function submit() { try { $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } catch(\Throwable $e) { - return handleError($e, $this); - } - } - - public function submit() - { - try { + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -46,14 +44,18 @@ class EditDomain extends Component if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->application->service->parse(); $this->dispatch('refresh'); $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 215019112..4d070bc0c 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Service; use App\Models\Application; +use App\Models\InstanceSettings; use App\Models\LocalFileVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -87,10 +88,12 @@ class FileStorage extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 70b3b5db6..7db6d9834 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -39,6 +39,7 @@ class Navbar extends Component return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', + 'envsUpdated' => '$refresh', ]; } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 56b506043..8324ee645 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,10 +2,12 @@ namespace App\Livewire\Project\Service; +use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Component; +use Spatie\Url\Url; class ServiceApplicationView extends Component { @@ -29,17 +31,6 @@ class ServiceApplicationView extends Component 'application.is_stripprefix_enabled' => 'nullable|boolean', ]; - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } - public function instantSave() { $this->submit(); @@ -59,10 +50,12 @@ class ServiceApplicationView extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { @@ -83,6 +76,18 @@ class ServiceApplicationView extends Component public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -90,12 +95,16 @@ class ServiceApplicationView extends Component if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->dispatch('generateDockerCompose'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index c05260899..2a21532ce 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared; use App\Jobs\DeleteResourceJob; +use App\Models\InstanceSettings; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -91,7 +92,7 @@ class Danger extends Component public function delete($password) { - if (isProduction()) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 7fb5c45db..c305e817c 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -5,7 +5,7 @@ namespace App\Livewire\Project\Shared; use App\Actions\Application\StopApplicationOneServer; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ContainerStatusJob; +use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use Illuminate\Support\Facades\Auth; @@ -119,10 +119,12 @@ class Destination extends Component public function removeServer(int $network_id, int $server_id, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 463ceecad..53c4374df 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -37,6 +37,7 @@ class Show extends Component 'env.is_literal' => 'required|boolean', 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', + 'env.is_required' => 'required|boolean', ]; protected $validationAttributes = [ @@ -46,6 +47,7 @@ class Show extends Component 'env.is_multiline' => 'Multiline', 'env.is_literal' => 'Literal', 'env.is_shown_once' => 'Shown Once', + 'env.is_required' => 'Required', ]; public function refresh() @@ -109,15 +111,21 @@ class Show extends Component } else { $this->validate(); } - // if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { - // $type = str($this->env->value)->after('{{')->before('.')->value; - // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - // return; - // } - // } + if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) { + $oldValue = $this->env->getOriginal('value'); + $this->env->value = $oldValue; + $this->dispatch('error', 'Required environment variable cannot be empty.'); + + return; + } + $this->serialize(); + + if ($this->isSharedVariable) { + unset($this->env->is_required); + } + $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); $this->dispatch('envsUpdated'); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 90419caed..574dfa969 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -52,6 +52,7 @@ class ExecuteContainerCommand extends Component $this->servers = $this->servers->push($server); } } + $this->loadContainers(); } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); @@ -62,12 +63,18 @@ class ExecuteContainerCommand extends Component if ($this->resource->destination->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->destination->server); } + $this->loadContainers(); } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } + $this->loadContainers(); + } elseif (data_get($this->parameters, 'server_uuid')) { + $this->type = 'server'; + $this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail(); + $this->server = $this->resource; } } @@ -130,6 +137,28 @@ class ExecuteContainerCommand extends Component if ($this->containers->count() > 0) { $this->container = $this->containers->first(); } + if ($this->containers->count() === 1) { + $this->selected_container = data_get($this->containers->first(), 'container.Names'); + } + } + + #[On('connectToServer')] + public function connectToServer() + { + try { + if ($this->server->isForceDisabled()) { + throw new \RuntimeException('Server is disabled.'); + } + $this->dispatch( + 'send-terminal-command', + false, + data_get($this->server, 'name'), + data_get($this->server, 'uuid') + ); + + } catch (\Throwable $e) { + return handleError($e, $this); + } } #[On('connectToContainer')] diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 0e140b8c1..f3d9da07d 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -39,7 +39,7 @@ class GetLogs extends Component public ?bool $showTimeStamps = true; - public int $numberOfLines = 100; + public ?int $numberOfLines = 100; public function mount() { @@ -98,7 +98,7 @@ class GetLogs extends Component if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { return; } - if ($this->numberOfLines <= 0) { + if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) { $this->numberOfLines = 1000; } if ($this->container) { diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php index d9d7dd3ef..fdc35fc0f 100644 --- a/app/Livewire/Project/Shared/Metrics.php +++ b/app/Livewire/Project/Shared/Metrics.php @@ -31,13 +31,8 @@ class Metrics extends Component public function loadData() { try { - $metrics = $this->resource->getMetrics($this->interval); - $cpuMetrics = collect($metrics)->map(function ($metric) { - return [$metric[0], $metric[1]]; - }); - $memoryMetrics = collect($metrics)->map(function ($metric) { - return [$metric[0], $metric[2]]; - }); + $cpuMetrics = $this->resource->getCpuMetrics($this->interval); + $memoryMetrics = $this->resource->getMemoryMetrics($this->interval); $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index e4b5c9b89..54b1be3af 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Shared\Storages; +use App\Models\InstanceSettings; use App\Models\LocalPersistentVolume; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -40,10 +41,12 @@ class Show extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $this->storage->delete(); diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php index dea842651..3859b387a 100644 --- a/app/Livewire/Project/Shared/UploadConfig.php +++ b/app/Livewire/Project/Shared/UploadConfig.php @@ -8,8 +8,11 @@ use Livewire\Component; class UploadConfig extends Component { public $config; + public $applicationId; - public function mount() { + + public function mount() + { if (isDev()) { $this->config = '{ "build_pack": "nixpacks", @@ -22,6 +25,7 @@ class UploadConfig extends Component }'; } } + public function uploadConfig() { try { @@ -30,10 +34,12 @@ class UploadConfig extends Component $this->dispatch('success', 'Application settings updated'); } catch (\Exception $e) { $this->dispatch('error', $e->getMessage()); + return; } } + public function render() { return view('livewire.project.shared.upload-config'); diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php new file mode 100644 index 000000000..4d2d777d4 --- /dev/null +++ b/app/Livewire/Server/Advanced.php @@ -0,0 +1,79 @@ + 'required|integer|min:1', + 'server.settings.dynamic_timeout' => 'required|integer|min:1', + 'server.settings.force_docker_cleanup' => 'required|boolean', + 'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string', + 'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100', + 'server.settings.server_disk_usage_notification_threshold' => 'required|integer|min:50|max:100', + 'server.settings.delete_unused_volumes' => 'boolean', + 'server.settings.delete_unused_networks' => 'boolean', + ]; + + protected $validationAttributes = [ + + 'server.settings.concurrent_builds' => 'Concurrent Builds', + 'server.settings.dynamic_timeout' => 'Dynamic Timeout', + 'server.settings.force_docker_cleanup' => 'Force Docker Cleanup', + 'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency', + 'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold', + 'server.settings.server_disk_usage_notification_threshold' => 'Server Disk Usage Notification Threshold', + 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes', + 'server.settings.delete_unused_networks' => 'Delete Unused Networks', + ]; + + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->save(); + $this->dispatch('success', 'Server updated.'); + $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + $this->server->settings->refresh(); + + return handleError($e, $this); + } + } + + public function manualCleanup() + { + try { + DockerCleanupJob::dispatch($this->server, true); + $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + $frequency = $this->server->settings->docker_cleanup_frequency; + if (empty($frequency) || ! validate_cron_expression($frequency)) { + $this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; + throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); + } + $this->server->settings->save(); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.advanced'); + } +} diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php index 0921c7fa4..20f8dd9ed 100644 --- a/app/Livewire/Server/Charts.php +++ b/app/Livewire/Server/Charts.php @@ -34,12 +34,6 @@ class Charts extends Component try { $cpuMetrics = $this->server->getCpuMetrics($this->interval); $memoryMetrics = $this->server->getMemoryMetrics($this->interval); - $cpuMetrics = collect($cpuMetrics)->map(function ($metric) { - return [$metric[0], $metric[1]]; - }); - $memoryMetrics = collect($memoryMetrics)->map(function ($metric) { - return [$metric[0], $metric[1]]; - }); $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php new file mode 100644 index 000000000..82bc789db --- /dev/null +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -0,0 +1,44 @@ + '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'); + } +} diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index ed2345b2a..cc2b335e1 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -2,6 +2,8 @@ namespace App\Livewire\Server; +use App\Actions\Server\DeleteServer; +use App\Models\InstanceSettings; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -15,10 +17,12 @@ class Delete extends Component public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { $this->authorize('delete', $this->server); @@ -28,6 +32,7 @@ class Delete extends Component return; } $this->server->delete(); + DeleteServer::dispatch($this->server); return redirect()->route('server.index'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 30c96eaee..16201b652 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -4,8 +4,6 @@ namespace App\Livewire\Server; use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; -use App\Jobs\DockerCleanupJob; -use App\Jobs\PullSentinelImageJob; use App\Models\Server; use Livewire\Component; @@ -46,25 +44,19 @@ class Form extends Component 'server.ip' => 'required', 'server.user' => 'required', 'server.port' => 'required', - 'server.settings.is_cloudflare_tunnel' => 'required|boolean', + 'wildcard_domain' => 'nullable|url', 'server.settings.is_reachable' => 'required', 'server.settings.is_swarm_manager' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_build_server' => 'required|boolean', - 'server.settings.concurrent_builds' => 'required|integer|min:1', - 'server.settings.dynamic_timeout' => 'required|integer|min:1', 'server.settings.is_metrics_enabled' => 'required|boolean', - 'server.settings.metrics_token' => 'required', - 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', - 'server.settings.metrics_history_days' => 'required|integer|min:1', - 'wildcard_domain' => 'nullable|url', - 'server.settings.is_server_api_enabled' => 'required|boolean', + 'server.settings.sentinel_token' => 'required', + 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1', + 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1', + 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10', + 'server.settings.sentinel_custom_url' => 'nullable|url', + 'server.settings.is_sentinel_enabled' => 'required|boolean', 'server.settings.server_timezone' => 'required|string|timezone', - 'server.settings.force_docker_cleanup' => 'required|boolean', - 'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string', - 'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100', - 'server.settings.delete_unused_volumes' => 'boolean', - 'server.settings.delete_unused_networks' => 'boolean', ]; protected $validationAttributes = [ @@ -73,21 +65,18 @@ class Form extends Component 'server.ip' => 'IP address/Domain', 'server.user' => 'User', 'server.port' => 'Port', - 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_reachable' => 'Is reachable', 'server.settings.is_swarm_manager' => 'Swarm Manager', 'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_build_server' => 'Build Server', - 'server.settings.concurrent_builds' => 'Concurrent Builds', - 'server.settings.dynamic_timeout' => 'Dynamic Timeout', 'server.settings.is_metrics_enabled' => 'Metrics', - 'server.settings.metrics_token' => 'Metrics Token', - 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', - 'server.settings.metrics_history_days' => 'Metrics History', - 'server.settings.is_server_api_enabled' => 'Server API', + 'server.settings.sentinel_token' => 'Metrics Token', + 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval', + 'server.settings.sentinel_metrics_history_days' => 'Metrics History', + 'server.settings.sentinel_push_interval_seconds' => 'Push Interval', + 'server.settings.is_sentinel_enabled' => 'Server API', + 'server.settings.sentinel_custom_url' => 'Coolify URL', 'server.settings.server_timezone' => 'Server Timezone', - 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes', - 'server.settings.delete_unused_networks' => 'Delete Unused Networks', ]; public function mount(Server $server) @@ -97,6 +86,25 @@ class Form extends Component $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes; $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks; + + } + + public function checkSyncStatus() + { + $this->server->refresh(); + $this->server->settings->refresh(); + } + + public function regenerateSentinelToken() + { + try { + $this->server->settings->generateSentinelToken(); + $this->server->settings->refresh(); + // $this->restartSentinel(notification: false); + $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function updated($field) @@ -129,21 +137,35 @@ class Form extends Component $this->dispatch('proxyStatusUpdated'); } - public function checkPortForServerApi() + public function updatedServerSettingsIsSentinelEnabled($value) { - try { - if ($this->server->settings->is_server_api_enabled === true) { - $this->server->checkServerApi(); - $this->dispatch('success', 'Server API is reachable.'); + $this->validate([ + 'server.settings.sentinel_custom_url' => 'required|url', + ]); + if ($value === false) { + StopSentinel::dispatch($this->server); + $this->server->settings->is_metrics_enabled = false; + $this->server->settings->save(); + $this->server->sentinelHeartbeat(isReset: true); + } else { + try { + StartSentinel::run($this->server); + } catch (\Throwable $e) { + return handleError($e, $this); } - } catch (\Throwable $e) { - return handleError($e, $this); } } + public function updatedServerSettingsIsMetricsEnabled() + { + $this->restartSentinel(); + } + public function instantSave() { try { + + $this->validate(); refresh_server_connection($this->server->privateKey); $this->validateServer(false); @@ -151,33 +173,39 @@ class Form extends Component $this->server->save(); $this->dispatch('success', 'Server updated.'); $this->dispatch('refreshServerShow'); - if ($this->server->isSentinelEnabled()) { - PullSentinelImageJob::dispatchSync($this->server); - ray('Sentinel is enabled'); - if ($this->server->settings->isDirty('is_metrics_enabled')) { - $this->dispatch('reloadWindow'); - } - if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) { - ray('Starting sentinel'); - } - } else { - ray('Sentinel is not enabled'); - StopSentinel::dispatch($this->server); - } - $this->server->settings->save(); - // $this->checkPortForServerApi(); - } catch (\Throwable $e) { + $this->server->settings->refresh(); + return handleError($e, $this); + } finally { } } - public function restartSentinel() + public function saveSentinel() { try { - $version = get_latest_sentinel_version(); - StartSentinel::run($this->server, $version, true); - $this->dispatch('success', 'Sentinel restarted.'); + $this->validate(); + $this->server->settings->save(); + $this->dispatch('success', 'Sentinel updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->checkSyncStatus(); + } + } + + public function restartSentinel($notification = true) + { + try { + $this->validate(); + $this->validate([ + 'server.settings.sentinel_custom_url' => 'required|url', + ]); + $this->server->settings->save(); + $this->server->restartSentinel(async: false); + if ($notification) { + $this->dispatch('success', 'Sentinel restarted.'); + } } catch (\Throwable $e) { return handleError($e, $this); } @@ -238,7 +266,6 @@ class Form extends Component $newTimezone = $this->server->settings->server_timezone; if ($currentTimezone !== $newTimezone || $currentTimezone === '') { $this->server->settings->server_timezone = $newTimezone; - $this->server->settings->save(); } $this->server->settings->save(); $this->server->save(); @@ -248,29 +275,4 @@ class Form extends Component return handleError($e, $this); } } - - public function updatedServerSettingsServerTimezone($value) - { - $this->server->settings->server_timezone = $value; - $this->server->settings->save(); - $this->dispatch('success', 'Server timezone updated.'); - } - - public function manualCleanup() - { - try { - DockerCleanupJob::dispatch($this->server, true); - $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function manualCloudflareConfig() - { - $this->server->settings->is_cloudflare_tunnel = true; - $this->server->settings->save(); - $this->server->refresh(); - $this->dispatch('success', 'Cloudflare Tunnels enabled.'); - } } diff --git a/app/Livewire/Server/Proxy/Modal.php b/app/Livewire/Server/Proxy/Modal.php deleted file mode 100644 index 5679944d0..000000000 --- a/app/Livewire/Server/Proxy/Modal.php +++ /dev/null @@ -1,16 +0,0 @@ -dispatch('proxyStatusUpdated'); - } -} diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php index d70e44e55..5ecb56a69 100644 --- a/app/Livewire/Server/Proxy/Show.php +++ b/app/Livewire/Server/Proxy/Show.php @@ -22,10 +22,7 @@ class Show extends Component { $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 800344ac3..f549b43cb 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -15,7 +15,9 @@ class Resources extends Component public $parameters = []; - public Collection $unmanagedContainers; + public Collection $containers; + + public $activeTab = 'managed'; public function getListeners() { @@ -50,14 +52,29 @@ class Resources extends Component public function refreshStatus() { $this->server->refresh(); - $this->loadUnmanagedContainers(); + if ($this->activeTab === 'managed') { + $this->loadManagedContainers(); + } else { + $this->loadUnmanagedContainers(); + } $this->dispatch('success', 'Resource statuses refreshed.'); } + public function loadManagedContainers() + { + try { + $this->activeTab = 'managed'; + $this->containers = $this->server->refresh()->definedResources(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function loadUnmanagedContainers() { + $this->activeTab = 'unmanaged'; try { - $this->unmanagedContainers = $this->server->loadUnmanagedContainers(); + $this->containers = $this->server->loadUnmanagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -65,13 +82,14 @@ class Resources extends Component public function mount() { - $this->unmanagedContainers = collect(); + $this->containers = collect(); $this->parameters = get_route_parameters(); try { $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); if (is_null($this->server)) { return redirect()->route('server.index'); } + $this->loadManagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index a5e94a19a..85c5f95f8 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -10,20 +10,17 @@ class Show extends Component { use AuthorizesRequests; - public ?Server $server = null; + public Server $server; - public $parameters = []; + public array $parameters; protected $listeners = ['refreshServerShow']; public function mount() { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php index 92869c44b..cc77fd611 100644 --- a/app/Livewire/Server/ShowPrivateKey.php +++ b/app/Livewire/Server/ShowPrivateKey.php @@ -14,15 +14,36 @@ class ShowPrivateKey extends Component public $parameters; + public function mount() + { + $this->parameters = get_route_parameters(); + } + public function setPrivateKey($privateKeyId) { + $ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId); + if (is_null($ownedPrivateKey)) { + $this->dispatch('error', 'You are not allowed to use this private key.'); + + return; + } + + $originalPrivateKeyId = $this->server->getOriginal('private_key_id'); try { - $privateKey = PrivateKey::findOrFail($privateKeyId); - $this->server->update(['private_key_id' => $privateKey->id]); - $this->server->refresh(); - $this->dispatch('success', 'Private key updated successfully.'); + $this->server->update(['private_key_id' => $privateKeyId]); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Private key updated successfully.'); + } else { + throw new \Exception('Server is not reachable.

Check this documentation for further help.

Error: '.$error); + } } catch (\Exception $e) { + $this->server->update(['private_key_id' => $originalPrivateKeyId]); + $this->server->validateConnection(); $this->dispatch('error', 'Failed to update private key: '.$e->getMessage()); + } finally { + $this->dispatch('refreshServerShow'); + $this->server->refresh(); } } @@ -33,18 +54,15 @@ class ShowPrivateKey extends Component if ($uptime) { $this->dispatch('success', 'Server is reachable.'); } else { - ray($error); $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); return; } } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->dispatch('refreshServerShow'); + $this->server->refresh(); } } - - public function mount() - { - $this->parameters = get_route_parameters(); - } } diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 754f0929b..0a6c5bae6 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -5,62 +5,95 @@ namespace App\Livewire\Settings; use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; use App\Models\Server; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Rule; use Livewire\Component; class Index extends Component { public InstanceSettings $settings; - public bool $do_not_track; - - public bool $is_auto_update_enabled; - - public bool $is_registration_enabled; - - public bool $is_dns_validation_enabled; - - public bool $is_api_enabled; - - public string $auto_update_frequency; - - public string $update_check_frequency; - - protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; - protected Server $server; - protected $rules = [ - 'settings.fqdn' => 'nullable', - 'settings.resale_license' => 'nullable', - 'settings.public_port_min' => 'required', - 'settings.public_port_max' => 'required', - 'settings.custom_dns_servers' => 'nullable', - 'settings.instance_name' => 'nullable', - 'settings.allowed_ips' => 'nullable', - 'settings.is_auto_update_enabled' => 'boolean', - 'auto_update_frequency' => 'string', - 'update_check_frequency' => 'string', - 'settings.instance_timezone' => 'required|string|timezone', - ]; - - protected $validationAttributes = [ - 'settings.fqdn' => 'FQDN', - 'settings.resale_license' => 'Resale License', - 'settings.public_port_min' => 'Public port min', - 'settings.public_port_max' => 'Public port max', - 'settings.custom_dns_servers' => 'Custom DNS servers', - 'settings.allowed_ips' => 'Allowed IPs', - 'settings.is_auto_update_enabled' => 'Auto Update Enabled', - 'auto_update_frequency' => 'Auto Update Frequency', - 'update_check_frequency' => 'Update Check Frequency', - ]; - + #[Locked] public $timezones; + #[Rule('boolean')] + public bool $is_auto_update_enabled; + + #[Rule('nullable|string|max:255')] + public ?string $fqdn = null; + + #[Rule('nullable|string|max:255')] + public ?string $resale_license = null; + + #[Rule('required|integer|min:1025|max:65535')] + public int $public_port_min; + + #[Rule('required|integer|min:1025|max:65535')] + public int $public_port_max; + + #[Rule('nullable|string')] + public ?string $custom_dns_servers = null; + + #[Rule('nullable|string|max:255')] + public ?string $instance_name = null; + + #[Rule('nullable|string')] + public ?string $allowed_ips = null; + + #[Rule('nullable|string')] + public ?string $public_ipv4 = null; + + #[Rule('nullable|string')] + public ?string $public_ipv6 = null; + + #[Rule('string')] + public string $auto_update_frequency; + + #[Rule('string')] + public string $update_check_frequency; + + #[Rule('required|string|timezone')] + public string $instance_timezone; + + #[Rule('boolean')] + public bool $do_not_track; + + #[Rule('boolean')] + public bool $is_registration_enabled; + + #[Rule('boolean')] + public bool $is_dns_validation_enabled; + + #[Rule('boolean')] + public bool $is_api_enabled; + + #[Rule('boolean')] + public bool $disable_two_step_confirmation; + + public function render() + { + return view('livewire.settings.index'); + } + public function mount() { - if (isInstanceAdmin()) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } else { $this->settings = instanceSettings(); + $this->fqdn = $this->settings->fqdn; + $this->resale_license = $this->settings->resale_license; + $this->public_port_min = $this->settings->public_port_min; + $this->public_port_max = $this->settings->public_port_max; + $this->custom_dns_servers = $this->settings->custom_dns_servers; + $this->instance_name = $this->settings->instance_name; + $this->allowed_ips = $this->settings->allowed_ips; + $this->public_ipv4 = $this->settings->public_ipv4; + $this->public_ipv6 = $this->settings->public_ipv6; $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; @@ -69,13 +102,22 @@ class Index extends Component $this->auto_update_frequency = $this->settings->auto_update_frequency; $this->update_check_frequency = $this->settings->update_check_frequency; $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); - } else { - return redirect()->route('dashboard'); + $this->instance_timezone = $this->settings->instance_timezone; + $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; } } - public function instantSave() + public function instantSave($isSave = true) { + $this->settings->fqdn = $this->fqdn; + $this->settings->resale_license = $this->resale_license; + $this->settings->public_port_min = $this->public_port_min; + $this->settings->public_port_max = $this->public_port_max; + $this->settings->custom_dns_servers = $this->custom_dns_servers; + $this->settings->instance_name = $this->instance_name; + $this->settings->allowed_ips = $this->allowed_ips; + $this->settings->public_ipv4 = $this->public_ipv4; + $this->settings->public_ipv6 = $this->public_ipv6; $this->settings->do_not_track = $this->do_not_track; $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; @@ -83,8 +125,12 @@ class Index extends Component $this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->auto_update_frequency = $this->auto_update_frequency; $this->settings->update_check_frequency = $this->update_check_frequency; - $this->settings->save(); - $this->dispatch('success', 'Settings updated!'); + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; + $this->settings->instance_timezone = $this->instance_timezone; + if ($isSave) { + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } } public function submit() @@ -141,13 +187,8 @@ class Index extends Component $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); - $this->settings->do_not_track = $this->do_not_track; - $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; - $this->settings->is_registration_enabled = $this->is_registration_enabled; - $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - $this->settings->is_api_enabled = $this->is_api_enabled; - $this->settings->auto_update_frequency = $this->auto_update_frequency; - $this->settings->update_check_frequency = $this->update_check_frequency; + $this->instantSave(isSave: false); + $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); if (! $error_show) { @@ -170,15 +211,16 @@ class Index extends Component } } - public function updatedSettingsInstanceTimezone($value) + public function toggleTwoStepConfirmation($password) { - $this->settings->instance_timezone = $value; - $this->settings->save(); - $this->dispatch('success', 'Instance timezone updated.'); - } + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - public function render() - { - return view('livewire.settings.index'); + return; + } + + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true; + $this->settings->save(); + $this->dispatch('success', 'Two step confirmation has been disabled.'); } } diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index ca0c9c1ae..8cb7aad8f 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -28,6 +28,9 @@ class License extends Component if (! isCloud()) { abort(404); } + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->instance_id = config('app.id'); $this->settings = instanceSettings(); } diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 9240aa96d..47423a3f9 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -2,50 +2,59 @@ namespace App\Livewire; -use App\Jobs\DatabaseBackupJob; use App\Models\InstanceSettings; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Rule; use Livewire\Component; class SettingsBackup extends Component { public InstanceSettings $settings; - public $s3s; - public ?StandalonePostgresql $database = null; public ScheduledDatabaseBackup|null|array $backup = []; + #[Locked] + public $s3s; + + #[Locked] public $executions = []; - protected $rules = [ - 'database.uuid' => 'required', - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.postgres_user' => 'required', - 'database.postgres_password' => 'required', + #[Rule(['required'])] + public string $uuid; - ]; + #[Rule(['required'])] + public string $name; - protected $validationAttributes = [ - 'database.uuid' => 'uuid', - 'database.name' => 'name', - 'database.description' => 'description', - 'database.postgres_user' => 'postgres user', - 'database.postgres_password' => 'postgres password', - ]; + #[Rule(['nullable'])] + public ?string $description = null; + + #[Rule(['required'])] + public string $postgres_user; + + #[Rule(['required'])] + public string $postgres_password; public function mount() { - if (isInstanceAdmin()) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } else { $settings = instanceSettings(); $this->database = StandalonePostgresql::whereName('coolify-db')->first(); $s3s = S3Storage::whereTeamId(0)->get() ?? []; if ($this->database) { + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + if ($this->database->status !== 'running') { $this->database->status = 'running'; $this->database->save(); @@ -55,13 +64,10 @@ class SettingsBackup extends Component } $this->settings = $settings; $this->s3s = $s3s; - - } else { - return redirect()->route('dashboard'); } } - public function add_coolify_database() + public function addCoolifyDatabase() { try { $server = Server::findOrFail(0); @@ -98,16 +104,14 @@ class SettingsBackup extends Component } } - public function backup_now() - { - dispatch(new DatabaseBackupJob( - backup: $this->backup - )); - $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); - } - public function submit() { + $this->database->update([ + 'name' => $this->name, + 'description' => $this->description, + 'postgres_user' => $this->postgres_user, + 'postgres_password' => $this->postgres_password, + ]); $this->dispatch('success', 'Backup updated.'); } } diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 4515df9a7..2b515bf68 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -3,7 +3,6 @@ namespace App\Livewire; use App\Models\InstanceSettings; -use App\Notifications\TransactionalEmails\Test; use Livewire\Component; class SettingsEmail extends Component @@ -124,10 +123,4 @@ class SettingsEmail extends Component return handleError($e, $this); } } - - public function sendTestNotification() - { - $this->settings?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test email sent.'); - } } diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index c3884589f..17b3b89a3 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -24,6 +24,9 @@ class SettingsOauth extends Component public function mount() { + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) { $carry[$setting->provider] = $setting; diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 193b650ff..7df6f968f 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -93,52 +93,55 @@ class Change extends Component // } public function mount() { - $github_app_uuid = request()->github_app_uuid; - $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first(); - if (! $this->github_app) { - return redirect()->route('source.all'); - } - $this->applications = $this->github_app->applications; - $settings = instanceSettings(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + try { + $github_app_uuid = request()->github_app_uuid; + $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); - $this->name = str($this->github_app->name)->kebab(); - $this->fqdn = $settings->fqdn; + $this->applications = $this->github_app->applications; + $settings = instanceSettings(); + $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); - if ($settings->public_ipv4) { - $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); - } - if ($settings->public_ipv6) { - $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); - } - if ($this->github_app->installation_id && session('from')) { - $source_id = data_get(session('from'), 'source_id'); - if (! $source_id || $this->github_app->id !== $source_id) { - session()->forget('from'); - } else { - $parameters = data_get(session('from'), 'parameters'); - $back = data_get(session('from'), 'back'); - $environment_name = data_get($parameters, 'environment_name'); - $project_uuid = data_get($parameters, 'project_uuid'); - $type = data_get($parameters, 'type'); - $destination = data_get($parameters, 'destination'); - session()->forget('from'); + $this->name = str($this->github_app->name)->kebab(); + $this->fqdn = $settings->fqdn; - return redirect()->route($back, [ - 'environment_name' => $environment_name, - 'project_uuid' => $project_uuid, - 'type' => $type, - 'destination' => $destination, - ]); + if ($settings->public_ipv4) { + $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); } + if ($settings->public_ipv6) { + $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); + } + if ($this->github_app->installation_id && session('from')) { + $source_id = data_get(session('from'), 'source_id'); + if (! $source_id || $this->github_app->id !== $source_id) { + session()->forget('from'); + } else { + $parameters = data_get(session('from'), 'parameters'); + $back = data_get(session('from'), 'back'); + $environment_name = data_get($parameters, 'environment_name'); + $project_uuid = data_get($parameters, 'project_uuid'); + $type = data_get($parameters, 'type'); + $destination = data_get($parameters, 'destination'); + session()->forget('from'); + + return redirect()->route($back, [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'type' => $type, + 'destination' => $destination, + ]); + } + } + $this->parameters = get_route_parameters(); + if (isCloud() && ! isDev()) { + $this->webhook_endpoint = config('app.url'); + } else { + $this->webhook_endpoint = $this->ipv4; + $this->is_system_wide = $this->github_app->is_system_wide; + } + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->parameters = get_route_parameters(); - if (isCloud() && ! isDev()) { - $this->webhook_endpoint = config('app.url'); - } else { - $this->webhook_endpoint = $this->ipv4; - $this->is_system_wide = $this->github_app->is_system_wide; - } + } public function submit() diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index f85e8646e..103c5c9fb 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -23,7 +23,7 @@ class Create extends Component public function mount() { - $this->name = generate_random_name(); + $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long } public function createGitHubApp() diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 270aa176a..e4afa5b60 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -7,19 +7,19 @@ use Livewire\Component; class Deployments extends Component { - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - public $resource_ids = []; + public $resourceIds = []; public function render() { return view('livewire.tags.deployments'); } - public function get_deployments() + public function getDeployments() { try { - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([ 'id', 'application_id', 'application_name', @@ -29,7 +29,7 @@ class Deployments extends Component 'server_id', 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); - $this->dispatch('deployments', $this->deployments_per_tag_per_server); + $this->dispatch('deployments', $this->deploymentsPerTagPerServer); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php index a01d00a70..642b2bded 100644 --- a/app/Livewire/Tags/Index.php +++ b/app/Livewire/Tags/Index.php @@ -5,9 +5,11 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\DeployController; use App\Models\Tag; use Illuminate\Support\Collection; +use Livewire\Attributes\Title; use Livewire\Attributes\Url; use Livewire\Component; +#[Title('Tags | Coolify')] class Index extends Component { #[Url()] @@ -21,33 +23,47 @@ class Index extends Component public $webhook = null; - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - protected $listeners = ['deployments' => 'update_deployments']; + protected $listeners = ['deployments' => 'updateDeployments']; - public function update_deployments($deployments) + public function render() { - $this->deployments_per_tag_per_server = $deployments; + return view('livewire.tags.index'); } - public function tag_updated() + public function mount() + { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if ($this->tag) { + $this->tagUpdated(); + } + } + + public function updateDeployments($deployments) + { + $this->deploymentsPerTagPerServer = $deployments; + } + + public function tagUpdated() { if ($this->tag == '') { return; } - $tag = $this->tags->where('name', $this->tag)->first(); + $sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8'); + $tag = $this->tags->where('name', $sanitizedTag)->first(); if (! $tag) { - $this->dispatch('error', "Tag ({$this->tag}) not found."); + $this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.'); $this->tag = ''; return; } - $this->webhook = generatTagDeployWebhook($tag->name); + $this->webhook = generateTagDeployWebhook($tag->name); $this->applications = $tag->applications()->get(); $this->services = $tag->services()->get(); } - public function redeploy_all() + public function redeployAll() { try { $this->applications->each(function ($resource) { @@ -63,17 +79,4 @@ class Index extends Component return handleError($e, $this); } } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tag_updated(); - } - } - - public function render() - { - return view('livewire.tags.index'); - } } diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 668101edb..0dffcce57 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,8 +5,10 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Livewire\Attributes\Title; use Livewire\Component; +#[Title('Tags | Coolify')] class Show extends Component { public $tags; @@ -28,7 +30,7 @@ class Show extends Component if (! $tag) { return redirect()->route('tags.index'); } - $this->webhook = generatTagDeployWebhook($tag->name); + $this->webhook = generateTagDeployWebhook($tag->name); $this->applications = $tag->applications()->get(); $this->services = $tag->services()->get(); $this->tag = $tag; diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 3026cb297..700c30f0a 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -2,6 +2,7 @@ namespace App\Livewire\Team; +use App\Models\InstanceSettings; use App\Models\Team; use App\Models\User; use Illuminate\Support\Facades\Auth; @@ -77,10 +78,12 @@ class AdminView extends Component public function delete($id, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 6a32a1d16..043c4e2e8 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -13,17 +13,18 @@ class Invitations extends Component public function deleteInvitation(int $invitation_id) { - $initiation_found = TeamInvitation::find($invitation_id); - if (! $initiation_found) { + try { + $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); + $initiation_found->delete(); + $this->refreshInvitations(); + $this->dispatch('success', 'Invitation revoked.'); + } catch (\Exception $e) { return $this->dispatch('error', 'Invitation not found.'); } - $initiation_found->delete(); - $this->refreshInvitations(); - $this->dispatch('success', 'Invitation revoked.'); } public function refreshInvitations() { - $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); + $this->invitations = TeamInvitation::ownedByCurrentTeam()->get(); } } diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 6c9e405fc..25f8a1ff5 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -41,6 +41,9 @@ class InviteLink extends Component { try { $this->validate(); + if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + throw new \Exception('Admins cannot invite owners.'); + } $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 680cb901b..4d5cec805 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -12,29 +12,57 @@ class Member extends Component public function makeAdmin() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']); - $this->dispatch('reloadWindow'); + try { + if (! auth()->user()->isAdmin()) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeOwner() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); - $this->dispatch('reloadWindow'); + try { + if (! auth()->user()->isOwner()) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeReadonly() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); - $this->dispatch('reloadWindow'); + try { + if (! auth()->user()->isAdmin()) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function remove() { - $this->member->teams()->detach(currentTeam()); - Cache::forget("team:{$this->member->id}"); - Cache::remember('team:'.$this->member->id, 3600, function () { - return $this->member->teams()->first(); - }); - $this->dispatch('reloadWindow'); + try { + if (! auth()->user()->isAdmin()) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->detach(currentTeam()); + Cache::forget("team:{$this->member->id}"); + Cache::remember('team:'.$this->member->id, 3600, function () { + return $this->member->teams()->first(); + }); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } } diff --git a/app/Models/Application.php b/app/Models/Application.php index b20f65201..3fae7153b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1400,13 +1400,13 @@ class Application extends BaseModel return []; } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; if ($server->isMetricsEnabled()) { $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); if (str($metrics)->contains('error')) { $error = json_decode($metrics, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -1415,14 +1415,33 @@ class Application extends BaseModel } throw new \Exception($error); } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); + return $parsedCollection->toArray(); + } + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; }); return $parsedCollection->toArray(); @@ -1459,7 +1478,7 @@ class Application extends BaseModel return $config; } - + public function setConfig($config) { $validator = Validator::make(['config' => $config], [ diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 9f8e4b342..f77d73db8 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -44,7 +44,7 @@ class EnvironmentVariable extends Model 'version' => 'string', ]; - protected $appends = ['real_value', 'is_shared']; + protected $appends = ['real_value', 'is_shared', 'is_really_required']; protected static function booted() { @@ -74,6 +74,9 @@ class EnvironmentVariable extends Model 'version' => config('version'), ]); }); + static::saving(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->updateIsShared(); + }); } public function service() @@ -130,6 +133,13 @@ class EnvironmentVariable extends Model ); } + protected function isReallyRequired(): Attribute + { + return Attribute::make( + get: fn () => $this->is_required && str($this->real_value)->isEmpty(), + ); + } + protected function isShared(): Attribute { return Attribute::make( @@ -210,4 +220,11 @@ class EnvironmentVariable extends Model set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value, ); } + + protected function updateIsShared(): void + { + $type = str($this->value)->after('{{')->before('.')->value; + $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}'); + $this->is_shared = $isShared; + } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 66ecdd967..b9564e02c 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -31,6 +31,11 @@ class GithubApp extends BaseModel }); } + public static function ownedByCurrentTeam() + { + return GithubApp::whereTeamId(currentTeam()->id); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index a789a7e65..2112a4a66 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -9,6 +9,11 @@ class GitlabApp extends BaseModel 'app_secret', ]; + public static function ownedByCurrentTeam() + { + return GitlabApp::whereTeamId(currentTeam()->id); + } + public function applications() { return $this->morphMany(Application::class, 'source'); diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index bb3d1478b..339daed2a 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Jobs\PullHelperImageJob; use App\Notifications\Channels\SendsEmail; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -21,8 +22,23 @@ class InstanceSettings extends Model implements SendsEmail 'is_auto_update_enabled' => 'boolean', 'auto_update_frequency' => 'string', 'update_check_frequency' => 'string', + 'sentinel_token' => 'encrypted', ]; + protected static function booted(): void + { + static::updated(function ($settings) { + if ($settings->isDirty('helper_version')) { + Server::chunkById(100, function ($servers) { + foreach ($servers as $server) { + PullHelperImageJob::dispatch($server); + } + }); + } + }); + + } + public function fqdn(): Attribute { return Attribute::make( @@ -86,16 +102,16 @@ class InstanceSettings extends Model implements SendsEmail return "[{$instanceName}]"; } - public function helperVersion(): Attribute - { - return Attribute::make( - get: function ($value) { - if (isDev()) { - return 'latest'; - } + // public function helperVersion(): Attribute + // { + // return Attribute::make( + // get: function ($value) { + // if (isDev()) { + // return 'latest'; + // } - return $value; - } - ); - } + // return $value; + // } + // ); + // } } diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 3921e32e4..473fc7b4b 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -51,7 +51,6 @@ class ScheduledDatabaseBackup extends BaseModel } } - return null; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 8864deef1..2f023a248 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,10 +3,15 @@ namespace App\Models; use App\Actions\Server\InstallDocker; +use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; -use App\Jobs\PullSentinelImageJob; +use App\Jobs\CheckAndStartSentinelJob; +use App\Notifications\Server\Reachable; +use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; @@ -43,7 +48,7 @@ use Symfony\Component\Yaml\Yaml; class Server extends BaseModel { - use SchemalessAttributesTrait; + use SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -58,6 +63,7 @@ class Server extends BaseModel $payload['ip'] = str($server->ip)->trim(); } $server->forceFill($payload); + }); static::created(function ($server) { ServerSetting::create([ @@ -95,7 +101,8 @@ class Server extends BaseModel } } }); - static::deleting(function ($server) { + + static::forceDeleting(function ($server) { $server->destinations()->each(function ($destination) { $destination->delete(); }); @@ -103,12 +110,15 @@ class Server extends BaseModel }); } - public $casts = [ + protected $casts = [ 'proxy' => SchemalessAttributes::class, 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', 'delete_unused_volumes' => 'boolean', 'delete_unused_networks' => 'boolean', + 'unreachable_notification_sent' => 'boolean', + 'is_build_server' => 'boolean', + 'force_disabled' => 'boolean', ]; protected $schemalessAttributes = [ @@ -127,6 +137,11 @@ class Server extends BaseModel protected $guarded = []; + public function type() + { + return 'server'; + } + public static function isReachable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); @@ -209,10 +224,13 @@ respond 404 1 => 'https', ], 'service' => 'noop', - 'rule' => 'HostRegexp(`{catchall:.*}`)', + 'rule' => 'HostRegexp(`.+`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], 'priority' => 1, 'middlewares' => [ - 0 => 'redirect-regexp@file', + 0 => 'redirect-regexp', ], ], ], @@ -507,24 +525,48 @@ $schema://$host { public function forceEnableServer() { - $this->settings->update([ - 'force_disabled' => false, - ]); + $this->settings->force_disabled = false; + $this->settings->save(); } public function forceDisableServer() { - $this->settings->update([ - 'force_disabled' => true, - ]); + $this->settings->force_disabled = true; + $this->settings->save(); $sshKeyFileLocation = "id.root@{$this->uuid}"; Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function sentinelHeartbeat(bool $isReset = false) + { + $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now(); + $this->save(); + } + + /** + * Get the wait time for Sentinel to push before performing an SSH check. + * + * @return int The wait time in seconds. + */ + public function waitBeforeDoingSshCheck(): int + { + $wait = $this->settings->sentinel_push_interval_seconds * 3; + if ($wait < 120) { + $wait = 120; + } + + return $wait; + } + + public function isSentinelLive() + { + return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck())); + } + public function isSentinelEnabled() { - return $this->isMetricsEnabled() || $this->isServerApiEnabled(); + return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer(); } public function isMetricsEnabled() @@ -534,49 +576,19 @@ $schema://$host { public function isServerApiEnabled() { - return $this->settings->is_server_api_enabled; - } - - public function checkServerApi() - { - if ($this->isServerApiEnabled()) { - $server_ip = $this->ip; - if (isDev()) { - if ($this->id === 0) { - $server_ip = 'localhost'; - } - } - $command = "curl -s http://{$server_ip}:12172/api/health"; - $process = Process::timeout(5)->run($command); - if ($process->failed()) { - ray($process->exitCode(), $process->output(), $process->errorOutput()); - throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); - } - - } + return $this->settings->is_sentinel_enabled; } public function checkSentinel() { - // ray("Checking sentinel on server: {$this->name}"); - if ($this->isSentinelEnabled()) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - // ray('Sentinel is not running, starting it...'); - PullSentinelImageJob::dispatch($this); - } else { - // ray('Sentinel is running'); - } - } + CheckAndStartSentinelJob::dispatch($this); } public function getCpuMetrics(int $mins = 5) { if ($this->isMetricsEnabled()) { $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); if (str($cpu)->contains('error')) { $error = json_decode($cpu, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -585,17 +597,12 @@ $schema://$host { } throw new \Exception($error); } - $cpu = str($cpu)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($cpu)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 0); - - return [(int) $time, (float) $cpu_usage_percent]; - }); + $cpu = json_decode($cpu, true); + $parsedCollection = collect($cpu)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; }); - return $parsedCollection->toArray(); + return $parsedCollection; } } @@ -603,7 +610,7 @@ $schema://$host { { if ($this->isMetricsEnabled()) { $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); if (str($memory)->contains('error')) { $error = json_decode($memory, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -612,89 +619,19 @@ $schema://$host { } throw new \Exception($error); } - $memory = str($memory)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($memory)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $used, $free, $usedPercent] = explode(',', trim($line)); - $usedPercent = number_format($usedPercent, 0); - - return [(int) $time, (float) $usedPercent]; - }); + $memory = json_decode($memory, true); + $parsedCollection = collect($memory)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['usedPercent']]; }); return $parsedCollection->toArray(); } } - public function isServerReady(int $tries = 3) - { - if ($this->skipServer()) { - return false; - } - $serverUptimeCheckNumber = $this->unreachable_count; - if ($this->unreachable_count < $tries) { - $serverUptimeCheckNumber = $this->unreachable_count + 1; - } - if ($this->unreachable_count > $tries) { - $serverUptimeCheckNumber = $tries; - } - - $serverUptimeCheckNumberMax = $tries; - - // ray('server: ' . $this->name); - // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber); - // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax); - - ['uptime' => $uptime] = $this->validateConnection(); - if ($uptime) { - if ($this->unreachable_notification_sent === true) { - $this->update(['unreachable_notification_sent' => false]); - } - - return true; - } else { - if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { - // Reached max number of retries - if ($this->unreachable_notification_sent === false) { - ray('Server unreachable, sending notification...'); - // $this->team?->notify(new Unreachable($this)); - $this->update(['unreachable_notification_sent' => true]); - } - if ($this->settings->is_reachable === true) { - $this->settings()->update([ - 'is_reachable' => false, - ]); - } - - foreach ($this->applications() as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases() as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services()->get() as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - } else { - $this->update([ - 'unreachable_count' => $this->unreachable_count + 1, - ]); - } - - return false; - } - } - public function getDiskUsage(): ?string { - return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); + return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false); + // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); } public function definedResources() @@ -974,7 +911,8 @@ $schema://$host { public function isProxyShouldRun() { - if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { + // TODO: Do we need "|| $this->proxy->force_stop" here? + if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) { return false; } @@ -1038,39 +976,111 @@ $schema://$host { return data_get($this, 'settings.is_swarm_worker'); } + public function serverStatus(): bool + { + if ($this->status() === false) { + return false; + } + if ($this->isFunctional() === false) { + return false; + } + + return true; + } + + public function status(): bool + { + if ($this->skipServer()) { + return false; + } + ['uptime' => $uptime] = $this->validateConnection(false); + if ($uptime === false) { + foreach ($this->applications() as $application) { + $application->status = 'exited'; + $application->save(); + } + foreach ($this->databases() as $database) { + $database->status = 'exited'; + $database->save(); + } + foreach ($this->services() as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->status = 'exited'; + $app->save(); + } + foreach ($dbs as $db) { + $db->status = 'exited'; + $db->save(); + } + } + + return false; + } + + return true; + } + + public function isReachableChanged() + { + $this->refresh(); + $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; + $isReachable = (bool) $this->settings->is_reachable; + loggy('Server setting is_reachable changed to '.$isReachable.' for server '.$this->id.'. Unreachable notification sent: '.$unreachableNotificationSent); + // If the server is reachable, send the reachable notification if it was sent before + if ($isReachable === true) { + if ($unreachableNotificationSent === true) { + $this->sendReachableNotification(); + } + } else { + // If the server is unreachable, send the unreachable notification if it was not sent before + if ($unreachableNotificationSent === false) { + $this->sendUnreachableNotification(); + } + } + } + + public function sendReachableNotification() + { + $this->unreachable_notification_sent = false; + $this->save(); + $this->refresh(); + $this->team->notify(new Reachable($this)); + } + + public function sendUnreachableNotification() + { + $this->unreachable_notification_sent = true; + $this->save(); + $this->refresh(); + $this->team->notify(new Unreachable($this)); + } + public function validateConnection($isManualCheck = true) { config()->set('constants.ssh.mux_enabled', ! $isManualCheck); - // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false')); - $server = Server::find($this->id); - if (! $server) { - return ['uptime' => false, 'error' => 'Server not found.']; - } - if ($server->skipServer()) { + if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { // Make sure the private key is stored - if ($server->privateKey) { - $server->privateKey->storeInFileSystem(); + if ($this->privateKey) { + $this->privateKey->storeInFileSystem(); } - instant_remote_process(['ls /'], $server); - $server->settings()->update([ - 'is_reachable' => true, - ]); - $server->update([ - 'unreachable_count' => 0, - ]); - if (data_get($server, 'unreachable_notification_sent') === true) { - $server->update(['unreachable_notification_sent' => false]); + instant_remote_process(['ls /'], $this); + if ($this->settings->is_reachable === false) { + $this->settings->is_reachable = true; + $this->settings->save(); } return ['uptime' => true, 'error' => null]; } catch (\Throwable $e) { - $server->settings()->update([ - 'is_reachable' => false, - ]); + if ($this->settings->is_reachable === true) { + $this->settings->is_reachable = false; + $this->settings->save(); + } return ['uptime' => false, 'error' => $e->getMessage()]; } @@ -1225,4 +1235,22 @@ $schema://$host { { return str($this->ip)->contains(':'); } + + public function restartSentinel(bool $async = true): void + { + try { + if ($async) { + StartSentinel::dispatch($this, true); + } else { + StartSentinel::run($this, true); + } + } catch (\Throwable $e) { + loggy('Error restarting Sentinel: '.$e->getMessage()); + } + } + + public function url() + { + return base_url().'/server/'.$this->uuid; + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index c44a393b4..7a8e7b8ed 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -24,7 +24,7 @@ use OpenApi\Attributes as OA; 'is_logdrain_newrelic_enabled' => ['type' => 'boolean'], 'is_metrics_enabled' => ['type' => 'boolean'], 'is_reachable' => ['type' => 'boolean'], - 'is_server_api_enabled' => ['type' => 'boolean'], + 'is_sentinel_enabled' => ['type' => 'boolean'], 'is_swarm_manager' => ['type' => 'boolean'], 'is_swarm_worker' => ['type' => 'boolean'], 'is_usable' => ['type' => 'boolean'], @@ -35,9 +35,9 @@ use OpenApi\Attributes as OA; 'logdrain_highlight_project_id' => ['type' => 'string'], 'logdrain_newrelic_base_uri' => ['type' => 'string'], 'logdrain_newrelic_license_key' => ['type' => 'string'], - 'metrics_history_days' => ['type' => 'integer'], - 'metrics_refresh_rate_seconds' => ['type' => 'integer'], - 'metrics_token' => ['type' => 'string'], + 'sentinel_metrics_history_days' => ['type' => 'integer'], + 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'], + 'sentinel_token' => ['type' => 'string'], 'docker_cleanup_frequency' => ['type' => 'string'], 'docker_cleanup_threshold' => ['type' => 'integer'], 'server_id' => ['type' => 'integer'], @@ -53,8 +53,78 @@ class ServerSetting extends Model protected $casts = [ 'force_docker_cleanup' => 'boolean', 'docker_cleanup_threshold' => 'integer', + 'sentinel_token' => 'encrypted', + 'is_reachable' => 'boolean', + 'is_usable' => 'boolean', ]; + protected static function booted() + { + static::creating(function ($setting) { + try { + if (str($setting->sentinel_token)->isEmpty()) { + $setting->generateSentinelToken(save: false); + } + if (str($setting->sentinel_custom_url)->isEmpty()) { + $setting->generateSentinelUrl(save: false); + } + } catch (\Throwable $e) { + loggy('Error creating server setting: '.$e->getMessage()); + } + }); + static::updated(function ($settings) { + if ( + $settings->isDirty('sentinel_token') || + $settings->isDirty('sentinel_custom_url') || + $settings->isDirty('sentinel_metrics_refresh_rate_seconds') || + $settings->isDirty('sentinel_metrics_history_days') || + $settings->isDirty('sentinel_push_interval_seconds') + ) { + $settings->server->restartSentinel(); + } + if ($settings->isDirty('is_reachable')) { + $settings->server->isReachableChanged(); + } + }); + } + + public function generateSentinelToken(bool $save = true) + { + $data = [ + 'server_uuid' => $this->server->uuid, + ]; + $token = json_encode($data); + $encrypted = encrypt($token); + $this->sentinel_token = $encrypted; + if ($save) { + $this->save(); + } + + return $token; + } + + public function generateSentinelUrl(bool $save = true) + { + $domain = null; + $settings = InstanceSettings::get(); + if ($this->server->isLocalhost()) { + $domain = 'http://host.docker.internal:8000'; + } elseif ($settings->fqdn) { + $domain = $settings->fqdn; + } elseif ($settings->public_ipv4) { + $domain = 'http://'.$settings->public_ipv4.':8000'; + } elseif ($settings->public_ipv6) { + $domain = 'http://'.$settings->public_ipv6.':8000'; + } + $this->sentinel_custom_url = $domain; + loggy('Sentinel URL: '.$domain); + if ($save) { + $this->save(); + } + + return $domain; + } + public function server() { return $this->belongsTo(Server::class); diff --git a/app/Models/Service.php b/app/Models/Service.php index 5f0fea4aa..0af1adf22 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -297,7 +297,7 @@ class Service extends BaseModel 'key' => 'CP_DISABLE_HTTPS', 'value' => data_get($disable_https, 'value'), 'rules' => 'required', - 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS", + 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS', ], ]); } @@ -319,7 +319,7 @@ class Service extends BaseModel if ($password) { $data = $data->merge([ 'Password' => [ - 'key' => 'LABEL_STUDIO_PASSWORD', + 'key' => data_get($password, 'key'), 'value' => data_get($password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -359,7 +359,7 @@ class Service extends BaseModel if ($email) { $data = $data->merge([ 'Admin Email' => [ - 'key' => 'LANGFUSE_INIT_USER_EMAIL', + 'key' => data_get($email, 'key'), 'value' => data_get($email, 'value'), 'rules' => 'required|email', ], @@ -370,7 +370,7 @@ class Service extends BaseModel if ($password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'LANGFUSE_INIT_USER_PASSWORD', + 'key' => data_get($password, 'key'), 'value' => data_get($password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -384,7 +384,7 @@ class Service extends BaseModel $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first(); $data = $data->merge([ 'Email' => [ - 'key' => 'IN_USER_EMAIL', + 'key' => data_get($email, 'key'), 'value' => data_get($email, 'value'), 'rules' => 'required|email', ], @@ -392,7 +392,7 @@ class Service extends BaseModel $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first(); $data = $data->merge([ 'Password' => [ - 'key' => 'IN_PASSWORD', + 'key' => data_get($password, 'key'), 'value' => data_get($password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -487,7 +487,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_TOLGEE', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -534,7 +534,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_UNLEASH', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -557,7 +557,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'GF_SECURITY_ADMIN_PASSWORD', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -919,7 +919,7 @@ class Service extends BaseModel if ($admin_user) { $data = $data->merge([ 'User' => [ - 'key' => 'SERVICE_USER_ADMIN', + 'key' => data_get($admin_user, 'key'), 'value' => data_get($admin_user, 'value', 'admin'), 'readonly' => true, 'rules' => 'required', @@ -929,7 +929,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Password' => [ - 'key' => 'SERVICE_PASSWORD_ADMIN', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -939,7 +939,7 @@ class Service extends BaseModel if ($admin_email) { $data = $data->merge([ 'Email' => [ - 'key' => 'ADMIN_EMAIL', + 'key' => data_get($admin_email, 'key'), 'value' => data_get($admin_email, 'value'), 'rules' => 'required|email', ], @@ -997,8 +997,8 @@ class Service extends BaseModel break; case $image->contains('mysql'): $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER']; - $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT']; + $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT']; $dbNameVariables = ['MYSQL_DATABASE']; $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); @@ -1232,7 +1232,6 @@ class Service extends BaseModel public function environment_variables(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } @@ -1316,4 +1315,20 @@ class Service extends BaseModel return $networks; } + + protected function isDeployable(): Attribute + { + return Attribute::make( + get: function () { + $envs = $this->environment_variables()->where('is_required', true)->get(); + foreach ($envs as $env) { + if ($env->is_really_required) { + return false; + } + } + + return true; + } + ); + } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index e4341b1b9..c9d3ea031 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -266,33 +266,48 @@ class StandaloneClickhouse extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 94ab2d745..2bde51080 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -266,33 +266,48 @@ class StandaloneDragonfly extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 335c8931c..fbee9789a 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -266,33 +266,48 @@ class StandaloneKeydb extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index c6c08dee5..00f635faf 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -266,33 +266,48 @@ class StandaloneMariadb extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 99893b1d1..c225011c5 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -286,33 +286,48 @@ class StandaloneMongodb extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index f2a5b5c14..52725ffc7 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -267,33 +267,48 @@ class StandaloneMysql extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 1b18a5ca7..ccab7d658 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -268,37 +268,52 @@ class StandalonePostgresql extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) - { - $server = $this->destination->server; - $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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 = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); - } - } - public function isBackupSolutionAvailable() { return true; } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index a5868e243..5b6993b9a 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -210,7 +210,12 @@ class StandaloneRedis extends BaseModel protected function internalDbUrl(): 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( get: function () { 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; @@ -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() { return $this->belongsTo(Environment::class); @@ -262,37 +277,81 @@ class StandaloneRedis extends BaseModel return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - 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); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() { return false; } + + public function redisPassword(): Attribute + { + return new Attribute( + get: function () { + $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first(); + if (! $password) { + return null; + } + + return $password->value; + }, + + ); + } + + public function redisUsername(): Attribute + { + return new Attribute( + get: function () { + $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first(); + if (! $username) { + return null; + } + + return $username->value; + } + ); + } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 3f8e97bc5..c5e46f165 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -34,6 +34,7 @@ use OpenApi\Attributes as OA; 'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'], 'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'], 'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'], + 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'], 'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'], 'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'], 'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'], @@ -41,6 +42,7 @@ use OpenApi\Attributes as OA; 'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'], 'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'], 'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'], + 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'], 'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'], 'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'], 'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'], @@ -56,6 +58,7 @@ use OpenApi\Attributes as OA; 'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'], 'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'], 'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'], + 'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'], 'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'], 'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'], @@ -164,8 +167,12 @@ class Team extends Model implements SendsDiscord, SendsEmail if (currentTeam()->id === 0 && isDev()) { return 9999999; } + $team = Team::find(currentTeam()->id); + if (! $team) { + return 0; + } - return Team::find(currentTeam()->id)->limits['serverLimit']; + return data_get($team, 'limits.serverLimit', 0); } public function limits(): Attribute diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index c202710e2..0f298a829 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -20,6 +20,11 @@ class TeamInvitation extends Model return $this->belongsTo(Team::class); } + public static function ownedByCurrentTeam() + { + return TeamInvitation::whereTeamId(currentTeam()->id); + } + public function isValid() { $createdAt = $this->created_at; diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 1809da368..242980e00 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -4,6 +4,7 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -72,14 +73,42 @@ class DeploymentFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); + if ($this->fqdn) { + $message->addField('Domain', $this->fqdn, true); + } } else { - $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + if ($this->fqdn) { + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; + } + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: $description, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 5085065c2..a2674ad76 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -4,6 +4,7 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -78,24 +79,39 @@ class DeploymentSuccess extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.' + $message = new DiscordMessage( + title: ':white_check_mark: Preview deployment successful', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::successColor(), + ); -'; if ($this->preview->fqdn) { - $message .= '[Open Application]('.$this->preview->fqdn.') | '; + $message->addField('Application', '[Link]('.$this->preview->fqdn.')'); } - $message .= '[Deployment logs]('.$this->deployment_url.')'; - } else { - $message = 'Coolify: New version successfully deployed of '.$this->application_name.' -'; + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); + } else { if ($this->fqdn) { - $message .= '[Open Application]('.$this->fqdn.') | '; + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; } - $message .= '[Deployment logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':white_check_mark: New version successfully deployed', + description: $description, + color: DiscordMessage::successColor(), + ); + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 53ed8a589..a080fcabe 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -3,6 +3,7 @@ namespace App\Notifications\Application; use App\Models\Application; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -55,12 +56,14 @@ class StatusChanged extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: '.$this->resource_name.' has been stopped. - -'; - $message .= '[Open Application in Coolify]('.$this->resource_url.')'; + $message = new DiscordMessage( + title: ':cross_mark: Application stopped', + description: '[Open Application in Coolify]('.$this->resource_url.')', + color: DiscordMessage::errorColor(), + isCritical: true, + ); return $message; } diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index f1706f138..3a33d8902 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -12,7 +12,7 @@ class DiscordChannel */ public function send(SendsDiscord $notifiable, Notification $notification): void { - $message = $notification->toDiscord($notifiable); + $message = $notification->toDiscord(); $webhookUrl = $notifiable->routeNotificationForDiscord(); if (! $webhookUrl) { return; diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 23f6de264..182a1f5fc 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -34,9 +35,17 @@ class ContainerRestarted extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + $message = new DiscordMessage( + title: ':warning: Resource restarted', + description: "{$this->name} has been restarted automatically on {$this->server->name}.", + color: DiscordMessage::infoColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index bcf5e67a5..33a55c65a 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -34,9 +35,17 @@ class ContainerStopped extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}"; + $message = new DiscordMessage( + title: ':cross_mark: Resource stopped', + description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 77024c05b..8e2733339 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -45,9 +46,19 @@ class BackupFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Database backup failed', + description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Frequency', $this->frequency, true); + $message->addField('Output', $this->output); + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index f8dc6eb56..4bd6b97b8 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -44,9 +45,17 @@ class BackupSuccess extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; + $message = new DiscordMessage( + title: ':white_check_mark: Database backup successful', + description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.", + color: DiscordMessage::successColor(), + ); + + $message->addField('Frequency', $this->frequency, true); + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php deleted file mode 100644 index a51ac6283..000000000 --- a/app/Notifications/Database/DailyBackup.php +++ /dev/null @@ -1,50 +0,0 @@ -subject('Coolify: Daily backup statuses'); - $mail->view('emails.daily-backup', [ - 'databases' => $this->databases, - ]); - - return $mail; - } - - public function toDiscord(): string - { - return 'Coolify: Daily backup statuses'; - } - - public function toTelegram(): array - { - $message = 'Coolify: Daily backup statuses'; - - return [ - 'message' => $message, - ]; - } -} diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php new file mode 100644 index 000000000..856753dca --- /dev/null +++ b/app/Notifications/Dto/DiscordMessage.php @@ -0,0 +1,83 @@ +fields[] = [ + 'name' => $name, + 'value' => $value, + 'inline' => $inline, + ]; + + return $this; + } + + public function toPayload(): array + { + $footerText = 'Coolify v'.config('version'); + if (isCloud()) { + $footerText = 'Coolify Cloud'; + } + $payload = [ + 'embeds' => [ + [ + 'title' => $this->title, + 'description' => $this->description, + 'color' => $this->color, + 'fields' => $this->addTimestampToFields($this->fields), + 'footer' => [ + 'text' => $footerText, + ], + ], + ], + ]; + if ($this->isCritical) { + $payload['content'] = '@here'; + } + + return $payload; + } + + private function addTimestampToFields(array $fields): array + { + $fields[] = [ + 'name' => 'Time', + 'value' => 'timestamp.':R>', + 'inline' => true, + ]; + + return $fields; + } +} diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 1d4d648c8..48e7d8340 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -4,6 +4,7 @@ namespace App\Notifications\Internal; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -32,9 +33,13 @@ class GeneralNotification extends Notification implements ShouldQueue return $channels; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return $this->message; + return new DiscordMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: DiscordMessage::infoColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 479cc1aa1..2cc33f2ba 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -46,9 +47,19 @@ class TaskFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Scheduled task failed', + description: "Scheduled task ({$this->task->name}) failed.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Scheduled task', '[Link]('.$this->url.')'); + } + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 682ed7a1a..7ea1b84c2 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -49,11 +50,13 @@ class DockerCleanup extends Notification implements ShouldQueue // return $mail; // } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server cleanup job done', + description: $this->message, + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 6377f2f15..a26c803ee 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -50,9 +51,15 @@ class ForceDisabled extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions)."; + $message = new DiscordMessage( + title: ':cross_mark: Server disabled', + description: "Server ({$this->server->name}) disabled because it is not paid!", + color: DiscordMessage::errorColor(), + ); + + $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)'); return $message; } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 83594d643..65b65a10c 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -50,11 +51,13 @@ class ForceEnabled extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) enabled again!"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server enabled', + description: "Server '{$this->server->name}' enabled again!", + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 34cb22091..e373abc03 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,9 +3,7 @@ namespace App\Notifications\Server; use App\Models\Server; -use App\Notifications\Channels\DiscordChannel; -use App\Notifications\Channels\EmailChannel; -use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,26 +15,11 @@ class HighDiskUsage extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {} + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - - if ($isDiscordEnabled) { - $channels[] = DiscordChannel::class; - } - if ($isEmailEnabled) { - $channels[] = EmailChannel::class; - } - if ($isTelegramEnabled) { - $channels[] = TelegramChannel::class; - } - - return $channels; + return setNotificationChannels($notifiable, 'server_disk_usage'); } public function toMail(): MailMessage @@ -46,15 +29,25 @@ class HighDiskUsage extends Notification implements ShouldQueue $mail->view('emails.high-disk-usage', [ 'name' => $this->server->name, 'disk_usage' => $this->disk_usage, - 'threshold' => $this->docker_cleanup_threshold, + 'threshold' => $this->server_disk_usage_notification_threshold, ]); return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup."; + $message = new DiscordMessage( + title: ':cross_mark: High disk usage detected', + description: "Server '{$this->server->name}' high disk usage detected!", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Disk usage', "{$this->disk_usage}%", true); + $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true); + $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true); + $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)'); return $message; } @@ -62,7 +55,7 @@ class HighDiskUsage extends Notification implements ShouldQueue public function toTelegram(): array { return [ - 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", + 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php similarity index 64% rename from app/Notifications/Server/Revived.php rename to app/Notifications/Server/Reachable.php index 3f2b3b696..9b54501d9 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Reachable.php @@ -2,35 +2,37 @@ namespace App\Notifications\Server; -use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; -class Revived extends Notification implements ShouldQueue +class Reachable extends Notification implements ShouldQueue { use Queueable; public $tries = 1; + protected bool $isRateLimited = false; + public function __construct(public Server $server) { - if ($this->server->unreachable_notification_sent === false) { - return; - } - GetContainersStatus::dispatch($server)->onQueue('high'); - // dispatch(new ContainerStatusJob($server)); + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-reachable:'.$this->server->id, + ); } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -45,20 +47,8 @@ class Revived extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-revived-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - if (! $executed) { - return []; - } - - return $executed; + return $channels; } public function toMail(): MailMessage @@ -72,11 +62,13 @@ class Revived extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"; - - return $message; + return new DiscordMessage( + title: ":white_check_mark: Server '{$this->server->name}' revived", + description: 'All automations & integrations are turned on again!', + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 2fb83559a..5bc568e82 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,11 +6,11 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; class Unreachable extends Notification implements ShouldQueue { @@ -18,10 +18,21 @@ class Unreachable extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) {} + protected bool $isRateLimited = false; + + public function __construct(public Server $server) + { + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-unreachable:'.$this->server->id, + ); + } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -36,23 +47,11 @@ class Unreachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-unreachable-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - if (! $executed) { - return []; - } - - return $executed; + return $channels; } - public function toMail(): MailMessage + public function toMail(): ?MailMessage { $mail = new MailMessage; $mail->subject("Coolify: Your server ({$this->server->name}) is unreachable."); @@ -63,14 +62,20 @@ class Unreachable extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): ?DiscordMessage { - $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."; + $message = new DiscordMessage( + title: ':cross_mark: Server unreachable', + description: "Your server '{$this->server->name}' is unreachable.", + color: DiscordMessage::errorColor(), + ); + + $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.'); return $message; } - public function toTelegram(): array + public function toTelegram(): ?array { return [ 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 3b46a9a24..4c069ffd6 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -2,10 +2,12 @@ namespace App\Notifications; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Queue\Middleware\RateLimited; class Test extends Notification implements ShouldQueue { @@ -20,6 +22,14 @@ class Test extends Notification implements ShouldQueue return setNotificationChannels($notifiable, 'test'); } + public function middleware(object $notifiable, string $channel) + { + return match ($channel) { + 'App\Notifications\Channels\EmailChannel' => [new RateLimited('email')], + default => [], + }; + } + public function toMail(): MailMessage { $mail = new MailMessage; @@ -29,11 +39,15 @@ class Test extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: This is a test Discord notification from Coolify.'; - $message .= "\n\n"; - $message .= '[Go to your dashboard]('.base_url().')'; + $message = new DiscordMessage( + title: ':white_check_mark: Test Success', + description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:', + color: DiscordMessage::successColor(), + ); + + $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true); return $message; } diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 950eb67b6..e12910f82 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -1,5 +1,6 @@ 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->destination_id = $destination->id; $database->destination_type = $destination->getMorphClass(); @@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth } $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; } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 5d1ad5390..496017217 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -164,6 +164,7 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', '8080:8080', ], 'healthcheck' => [ @@ -187,6 +188,7 @@ function generate_default_proxy_configuration(Server $server) '--entryPoints.http.http2.maxConcurrentStreams=50', '--entrypoints.https.http.encodequerysemicolons=true', '--entryPoints.https.http2.maxConcurrentStreams=50', + '--entrypoints.https.http3', '--providers.docker.exposedbydefault=false', '--providers.file.directory=/traefik/dynamic/', '--providers.file.watch=true', @@ -239,9 +241,11 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', ], 'labels' => [ 'coolify.managed=true', + 'coolify.proxy=true', ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index f8bd1ffbd..dbab6861d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -39,6 +39,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; @@ -126,7 +127,7 @@ function refreshSession(?Team $team = null): void } function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { - ray($error); + loggy($error); if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); @@ -142,6 +143,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n return 'Duplicate entry found. Please use a different name.'; } + if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { + abort(404); + } + if ($error instanceof Throwable) { $message = $error->getMessage(); } else { @@ -164,14 +169,11 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { try { - $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); + $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); $versions = $response->json(); - return data_get($versions, 'sentinel.version'); + return data_get($versions, 'coolify.sentinel.version'); } catch (\Throwable $e) { - //throw $e; - ray($e->getMessage()); - return '0.0.0'; } } @@ -643,7 +645,7 @@ function queryResourcesByUuid(string $uuid) return $resource; } -function generatTagDeployWebhook($tag_name) +function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); $api = Url::fromString($baseUrl).'/api/v1'; @@ -1338,13 +1340,6 @@ function isAnyDeploymentInprogress() exit(0); } -function generateSentinelToken() -{ - $token = Str::random(64); - - return $token; -} - function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; @@ -3569,6 +3564,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int ]); } else { if ($value->startsWith('$')) { + $isRequired = false; if ($value->contains(':-')) { $value = replaceVariables($value); $key = $value->before(':'); @@ -3583,11 +3579,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $key = $value->before(':'); $value = $value->after(':?'); + $isRequired = true; } elseif ($value->contains('?')) { $value = replaceVariables($value); $key = $value->before('?'); $value = $value->after('?'); + $isRequired = true; } if ($originalValue->value() === $value->value()) { // This means the variable does not have a default value, so it needs to be created in Coolify @@ -3598,6 +3596,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int ], [ 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); // Add the variable to the environment so it will be shown in the deployable compose file $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value; @@ -3611,6 +3610,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'value' => $value, 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); } @@ -3787,7 +3787,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int service_name: $serviceName, image: $image, predefinedPort: $predefinedPort - )); } } @@ -3896,6 +3895,8 @@ function isAssociativeArray($array) */ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|Application|Service $resource, Collection &$where_to_add, ?Collection $where_to_check = null) { + // Currently disabled + return; if ($resource instanceof Service) { $ip = $resource->server->ip; } else { @@ -3983,13 +3984,14 @@ function instanceSettings() 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(); - if (!$server) { + if (! $server) { return; } - $uuid = new Cuid2(); + $uuid = new Cuid2; $cloneCommand = "git clone --no-checkout -b $branch $repository ."; $workdir = rtrim($base_directory, '/'); $fileList = collect([".$workdir/coolify.json"]); @@ -4007,6 +4009,60 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire try { return instant_remote_process($commands, $server); } catch (\Exception $e) { - // 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; +} + +function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool +{ + if (isDev()) { + $decaySeconds = 120; + } + $rateLimited = false; + $executed = RateLimiter::attempt( + $limiterKey, + $maxAttempts = 0, + function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { + isDev() && loggy('Rate limit not reached for '.$limiterKey); + $rateLimited = false; + + if ($callbackOnSuccess) { + $callbackOnSuccess(); + } + }, + $decaySeconds, + ); + if (! $executed) { + isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + $rateLimited = true; + } + + return $rateLimited; +} diff --git a/composer.json b/composer.json index 03adf9823..b17c3bf4e 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "guzzlehttp/guzzle": "^7.5.0", "laravel/fortify": "^v1.16.0", "laravel/framework": "^v11", - "laravel/horizon": "^5.27.1", + "laravel/horizon": "^5.29.1", + "laravel/pail": "^1.1", "laravel/prompts": "^0.1.6", "laravel/sanctum": "^v4.0", "laravel/socialite": "^v5.14.0", diff --git a/composer.lock b/composer.lock index 420d87ec0..981e723d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42c28ab141b70fcabf75b51afa96c670", + "content-hash": "943975ec232403b96a40d215253492d8", "packages": [ { "name": "amphp/amp", @@ -317,16 +317,16 @@ }, { "name": "amphp/parallel", - "version": "v2.2.9", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238" + "reference": "9777db1460d1535bc2a843840684fb1205225b87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/73d293f1fc4df1bebc3c4fce1432e82dd7032238", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238", + "url": "https://api.github.com/repos/amphp/parallel/zipball/9777db1460d1535bc2a843840684fb1205225b87", + "reference": "9777db1460d1535bc2a843840684fb1205225b87", "shasum": "" }, "require": { @@ -389,7 +389,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.2.9" + "source": "https://github.com/amphp/parallel/tree/v2.3.0" }, "funding": [ { @@ -397,7 +397,7 @@ "type": "github" } ], - "time": "2024-03-24T18:27:44+00:00" + "time": "2024-09-14T19:16:14+00:00" }, { "name": "amphp/parser", @@ -921,16 +921,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.321.9", + "version": "3.324.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a" + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", - "reference": "5de5099cfe0e17cb3eb2fe51de0101c99bc9442a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b258712f0d986e00e1143d55246b6f9e344c7184", + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184", "shasum": "" }, "require": { @@ -1013,22 +1013,22 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.321.9" + "source": "https://github.com/aws/aws-sdk-php/tree/3.324.0" }, - "time": "2024-09-11T18:15:49+00:00" + "time": "2024-10-10T18:06:36+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41" + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", "shasum": "" }, "require": { @@ -1067,9 +1067,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" }, - "time": "2024-04-18T11:16:25+00:00" + "time": "2024-10-01T13:55:55+00:00" }, { "name": "brick/math", @@ -1518,16 +1518,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.1", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", - "reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -1543,7 +1543,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", @@ -1611,7 +1611,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.1" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -1627,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-09-01T13:49:23+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -1937,16 +1937,16 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.3", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" + "reference": "8c784d071debd117328803d86b2097615b457500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", "shasum": "" }, "require": { @@ -1959,10 +1959,14 @@ "require-dev": { "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.0", - "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { "Cron\\": "src/Cron/" @@ -1986,7 +1990,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" }, "funding": [ { @@ -1994,7 +1998,7 @@ "type": "github" } ], - "time": "2023-08-10T19:36:49+00:00" + "time": "2024-10-09T13:47:03+00:00" }, { "name": "egulias/email-validator", @@ -2789,16 +2793,16 @@ }, { "name": "laravel/fortify", - "version": "v1.24.1", + "version": "v1.24.2", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20" + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/8158ba0960bb5f4aae509d01d74a95e16e30de20", - "reference": "8158ba0960bb5f4aae509d01d74a95e16e30de20", + "url": "https://api.github.com/repos/laravel/fortify/zipball/42695c45087e5abb3e173725b4f1ef4956a7b47d", + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d", "shasum": "" }, "require": { @@ -2850,20 +2854,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-09-03T10:02:14+00:00" + "time": "2024-09-16T19:20:52+00:00" }, { "name": "laravel/framework", - "version": "v11.23.2", + "version": "v11.27.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3" + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", - "reference": "d38bf0fd3a8936e1cb9ca8eb8d7304a564f790f3", + "url": "https://api.github.com/repos/laravel/framework/zipball/a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", "shasum": "" }, "require": { @@ -2882,7 +2886,7 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.18", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", @@ -2968,7 +2972,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.4.0", + "orchestra/testbench-core": "^9.5", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", @@ -3027,6 +3031,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -3058,20 +3063,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-09-11T21:59:23+00:00" + "time": "2024-10-09T04:17:35+00:00" }, { "name": "laravel/horizon", - "version": "v5.28.1", + "version": "v5.29.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f" + "reference": "9f482f21c23ed01c2366d1157843165165579c23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", - "reference": "9d2c4eaeb11408384401f8a7d1b0ea4c76554f3f", + "url": "https://api.github.com/repos/laravel/horizon/zipball/9f482f21c23ed01c2366d1157843165165579c23", + "reference": "9f482f21c23ed01c2366d1157843165165579c23", "shasum": "" }, "require": { @@ -3135,9 +3140,86 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.28.1" + "source": "https://github.com/laravel/horizon/tree/v5.29.1" }, - "time": "2024-09-04T14:06:50+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", @@ -3199,16 +3281,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.0.2", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1" + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1", - "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab", + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab", "shasum": "" }, "require": { @@ -3259,20 +3341,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2024-04-10T19:39:58+00:00" + "time": "2024-09-27T14:55:41+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.3.4", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81" + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/61b87392d986dc49ad5ef64e75b1ff5fee24ef81", - "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", "shasum": "" }, "require": { @@ -3320,7 +3402,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2024-08-02T07:48:17+00:00" + "time": "2024-09-23T13:33:08+00:00" }, { "name": "laravel/socialite", @@ -3465,16 +3547,16 @@ }, { "name": "laravel/tinker", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe" + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5", + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "shasum": "" }, "require": { @@ -3525,9 +3607,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.9.0" + "source": "https://github.com/laravel/tinker/tree/v2.10.0" }, - "time": "2024-01-04T16:10:04+00:00" + "time": "2024-09-23T13:32:56+00:00" }, { "name": "laravel/ui", @@ -3594,38 +3676,38 @@ }, { "name": "lcobucci/jwt", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051", + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051", "shasum": "" }, "require": { "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.27.0", - "lcobucci/clock": "^3.0", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2.9", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.10.7", "phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.2.6" + "phpunit/phpunit": "^11.1" }, "suggest": { - "lcobucci/clock": ">= 3.0" + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -3651,7 +3733,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.0" }, "funding": [ { @@ -3663,7 +3745,7 @@ "type": "patreon" } ], - "time": "2024-04-11T23:07:54+00:00" + "time": "2024-10-08T22:06:45+00:00" }, { "name": "league/commonmark", @@ -3855,16 +3937,16 @@ }, { "name": "league/flysystem", - "version": "3.28.0", + "version": "3.29.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c" + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", "shasum": "" }, "require": { @@ -3932,22 +4014,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" }, - "time": "2024-05-22T10:09:12+00:00" + "time": "2024-10-08T08:58:34+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8" + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/22071ef1604bc776f5ff2468ac27a752514665c8", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", "shasum": "" }, "require": { @@ -3987,22 +4069,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-17T13:10:48+00:00" }, { "name": "league/flysystem-local", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40" + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", "shasum": "" }, "require": { @@ -4036,22 +4118,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-09T21:24:39+00:00" }, { "name": "league/flysystem-sftp-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41" + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/abedadd3c64d4f0e276d6ecc796ec8194d136b41", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252", + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252", "shasum": "" }, "require": { @@ -4085,22 +4167,22 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-14T19:35:54+00:00" }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", "shasum": "" }, "require": { @@ -4131,7 +4213,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -4143,7 +4225,7 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "league/oauth1-client", @@ -4955,24 +5037,24 @@ }, { "name": "nette/schema", - "version": "v1.3.0", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.3" + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.4", + "nette/tester": "^2.5.2", "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.8" }, @@ -5011,9 +5093,9 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.0" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2023-12-11T11:54:22+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", @@ -5103,16 +5185,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -5155,9 +5237,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-07-01T20:03:41+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nubs/random-name-generator", @@ -5897,16 +5979,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.41", + "version": "3.0.42", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb" + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/621c73f7dcb310b61de34d1da4c4204e8ace6ceb", - "reference": "621c73f7dcb310b61de34d1da4c4204e8ace6ceb", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", "shasum": "" }, "require": { @@ -5987,7 +6069,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.41" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" }, "funding": [ { @@ -6003,20 +6085,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T00:13:54+00:00" + "time": "2024-09-16T03:06:04+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.1", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e", - "reference": "51b95ec8670af41009e2b2b56873bad96682413e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -6048,22 +6130,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-09-07T20:13:05+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.3", + "version": "1.12.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009" + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0fcbf194ab63d8159bb70d9aa3e1350051632009", - "reference": "0fcbf194ab63d8159bb70d9aa3e1350051632009", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", "shasum": "" }, "require": { @@ -6108,7 +6190,7 @@ "type": "github" } ], - "time": "2024-09-09T08:10:35+00:00" + "time": "2024-10-06T15:03:59+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6814,16 +6896,16 @@ }, { "name": "purplepixie/phpdns", - "version": "2.1.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/purplepixie/phpdns.git", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad" + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/18cd3a43fadcfd16e2789e3c78a264945f6cbfad", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad", + "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", "shasum": "" }, "require": { @@ -6856,9 +6938,9 @@ "description": "PHP DNS Direct Query Module", "support": { "issues": "https://github.com/purplepixie/phpdns/issues", - "source": "https://github.com/purplepixie/phpdns/tree/2.1.1" + "source": "https://github.com/purplepixie/phpdns/tree/2.2.0" }, - "time": "2024-05-27T13:27:50+00:00" + "time": "2024-09-26T14:39:58+00:00" }, { "name": "pusher/pusher-php-server", @@ -7148,21 +7230,21 @@ }, { "name": "rector/rector", - "version": "1.2.5", + "version": "1.2.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339" + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/e98aa793ca3fcd17e893cfaf9103ac049775d339", - "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.12.2" + "phpstan/phpstan": "^1.12.5" }, "conflict": { "rector/rector-doctrine": "*", @@ -7195,7 +7277,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.5" + "source": "https://github.com/rectorphp/rector/tree/1.2.6" }, "funding": [ { @@ -7203,7 +7285,7 @@ "type": "github" } ], - "time": "2024-09-08T17:43:24+00:00" + "time": "2024-10-03T08:56:44+00:00" }, { "name": "resend/resend-laravel", @@ -7494,16 +7576,16 @@ }, { "name": "sentry/sentry-laravel", - "version": "4.8.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7" + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", - "reference": "2bbcb7e81097993cf64d5b296eaa6d396cddd5a7", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3", + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3", "shasum": "" }, "require": { @@ -7567,7 +7649,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.8.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0" }, "funding": [ { @@ -7579,7 +7661,7 @@ "type": "custom" } ], - "time": "2024-08-15T19:03:01+00:00" + "time": "2024-09-19T12:58:53+00:00" }, { "name": "socialiteproviders/manager", @@ -8580,16 +8662,16 @@ }, { "name": "symfony/console", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", - "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", "shasum": "" }, "require": { @@ -8653,7 +8735,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.4" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -8669,7 +8751,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/css-selector", @@ -9100,16 +9182,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a" + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", - "reference": "f602d5c17d1fa02f8019ace2687d9d136b7f4a1a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", "shasum": "" }, "require": { @@ -9157,7 +9239,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.1.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" }, "funding": [ { @@ -9173,20 +9255,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747" + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6efcbd1b3f444f631c386504fc83eeca25963747", - "reference": "6efcbd1b3f444f631c386504fc83eeca25963747", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b", + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b", "shasum": "" }, "require": { @@ -9271,7 +9353,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.1.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.5" }, "funding": [ { @@ -9287,20 +9369,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T17:02:28+00:00" + "time": "2024-09-21T06:09:21+00:00" }, { "name": "symfony/mailer", - "version": "v7.1.2", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee" + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/8fcff0af9043c8f8a8e229437cea363e282f9aee", - "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b", + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b", "shasum": "" }, "require": { @@ -9351,7 +9433,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.1.2" + "source": "https://github.com/symfony/mailer/tree/v7.1.5" }, "funding": [ { @@ -9367,20 +9449,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T08:00:31+00:00" + "time": "2024-09-08T12:32:26+00:00" }, { "name": "symfony/mime", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758" + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758", - "reference": "ccaa6c2503db867f472a587291e764d6a1e58758", + "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", + "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", "shasum": "" }, "require": { @@ -9435,7 +9517,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.4" + "source": "https://github.com/symfony/mime/tree/v7.1.5" }, "funding": [ { @@ -9451,7 +9533,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:28:19+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/options-resolver", @@ -10238,16 +10320,16 @@ }, { "name": "symfony/process", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", - "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -10279,7 +10361,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.3" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -10295,7 +10377,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:44:47+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -10608,16 +10690,16 @@ }, { "name": "symfony/string", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", - "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -10675,7 +10757,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -10691,20 +10773,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/translation", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1" + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/8d5e50c813ba2859a6dfc99a0765c550507934a1", - "reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1", + "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea", + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea", "shasum": "" }, "require": { @@ -10769,7 +10851,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.1.3" + "source": "https://github.com/symfony/translation/tree/v7.1.5" }, "funding": [ { @@ -10785,7 +10867,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-09-16T06:30:38+00:00" }, { "name": "symfony/translation-contracts", @@ -10867,16 +10949,16 @@ }, { "name": "symfony/uid", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf" + "reference": "8c7bb8acb933964055215d89f9a9871df0239317" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/82177535395109075cdb45a70533aa3d7a521cdf", - "reference": "82177535395109075cdb45a70533aa3d7a521cdf", + "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317", + "reference": "8c7bb8acb933964055215d89f9a9871df0239317", "shasum": "" }, "require": { @@ -10921,7 +11003,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.1.4" + "source": "https://github.com/symfony/uid/tree/v7.1.5" }, "funding": [ { @@ -10937,20 +11019,20 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:59:40+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa" + "reference": "e20e03889539fd4e4211e14d2179226c513c010d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a5fa7481b199090964d6fd5dab6294d5a870c7aa", - "reference": "a5fa7481b199090964d6fd5dab6294d5a870c7aa", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", + "reference": "e20e03889539fd4e4211e14d2179226c513c010d", "shasum": "" }, "require": { @@ -11004,7 +11086,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" }, "funding": [ { @@ -11020,20 +11102,20 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:12:47+00:00" + "time": "2024-09-16T10:07:02+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "be37e7f13195e05ab84ca5269365591edd240335" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/be37e7f13195e05ab84ca5269365591edd240335", - "reference": "be37e7f13195e05ab84ca5269365591edd240335", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -11076,7 +11158,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.11" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -11092,7 +11174,7 @@ "type": "tidelift" } ], - "time": "2024-08-12T09:55:28+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11742,16 +11824,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.10.6", + "version": "4.11.0", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6" + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e462ff5269ea0ec91070edd5d51dc7215bdea3b6", - "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", "shasum": "" }, "require": { @@ -11765,7 +11847,7 @@ "require-dev": { "composer/package-versions-deprecated": "^1.11", "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", + "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0", "phpstan/phpstan": "^1.6", "phpunit/phpunit": ">=8", "vimeo/psalm": "^4.23" @@ -11817,31 +11899,31 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.10.6" + "source": "https://github.com/zircote/swagger-php/tree/4.11.0" }, - "time": "2024-07-26T03:04:43+00:00" + "time": "2024-10-09T03:11:12+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.13.5", + "version": "v3.14.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07" + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07", - "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", "shasum": "" }, "require": { "illuminate/routing": "^9|^10|^11", "illuminate/session": "^9|^10|^11", "illuminate/support": "^9|^10|^11", - "maximebf/debugbar": "~1.22.0", + "maximebf/debugbar": "~1.23.0", "php": "^8.0", "symfony/finder": "^6|^7" }, @@ -11854,7 +11936,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.13-dev" + "dev-master": "3.14-dev" }, "laravel": { "providers": [ @@ -11893,7 +11975,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.3" }, "funding": [ { @@ -11905,7 +11987,7 @@ "type": "github" } ], - "time": "2024-04-12T11:20:37+00:00" + "time": "2024-10-02T09:17:49+00:00" }, { "name": "brianium/paratest", @@ -12127,26 +12209,26 @@ }, { "name": "filp/whoops", - "version": "2.15.4", + "version": "2.16.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", + "php": "^7.1 || ^8.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -12186,7 +12268,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.4" + "source": "https://github.com/filp/whoops/tree/2.16.0" }, "funding": [ { @@ -12194,7 +12276,7 @@ "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12249,16 +12331,16 @@ }, { "name": "laravel/dusk", - "version": "v8.2.5", + "version": "v8.2.8", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad" + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/e641800393ce4ad39f0a47133f51aae67ceb01ad", - "reference": "e641800393ce4ad39f0a47133f51aae67ceb01ad", + "url": "https://api.github.com/repos/laravel/dusk/zipball/5bff1e8dd87ec653a2202475377152e5d14fde40", + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40", "shasum": "" }, "require": { @@ -12315,22 +12397,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v8.2.5" + "source": "https://github.com/laravel/dusk/tree/v8.2.8" }, - "time": "2024-08-26T12:34:33+00:00" + "time": "2024-10-04T14:02:20+00:00" }, { "name": "laravel/pint", - "version": "v1.17.3", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", - "reference": "9d77be916e145864f10788bb94531d03e1f7b482", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -12383,20 +12465,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-03T15:00:28+00:00" + "time": "2024-09-24T17:22:50+00:00" }, { "name": "maximebf/debugbar", - "version": "v1.22.5", + "version": "v1.23.2", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989" + "reference": "689720d724c771ac4add859056744b7b3f2406da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989", - "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", + "reference": "689720d724c771ac4add859056744b7b3f2406da", "shasum": "" }, "require": { @@ -12418,7 +12500,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } }, "autoload": { @@ -12449,9 +12531,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" }, - "time": "2024-09-09T08:05:55+00:00" + "time": "2024-09-16T11:23:09+00:00" }, { "name": "mockery/mockery", @@ -14892,16 +14974,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.11", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65" + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4c92046bb788648ff1098cc66da69aa7eac8cb65", - "reference": "4c92046bb788648ff1098cc66da69aa7eac8cb65", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", "shasum": "" }, "require": { @@ -14965,7 +15047,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.11" + "source": "https://github.com/symfony/http-client/tree/v6.4.12" }, "funding": [ { @@ -14981,7 +15063,7 @@ "type": "tidelift" } ], - "time": "2024-08-26T06:30:21+00:00" + "time": "2024-09-20T08:21:33+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/debugbar.php b/config/debugbar.php index eae406ba7..daeea96b6 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -18,6 +18,7 @@ return [ 'except' => [ 'telescope*', 'horizon*', + 'api*', ], /* diff --git a/config/sentry.php b/config/sentry.php index b7e230e71..e8b6ab098 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.358', + 'release' => '4.0.0-beta.361', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/testing.php b/config/testing.php new file mode 100644 index 000000000..41b8eadf0 --- /dev/null +++ b/config/testing.php @@ -0,0 +1,6 @@ + env('DUSK_TEST_EMAIL', 'test@example.com'), + 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'), +]; diff --git a/config/version.php b/config/version.php index a89343eb9..0e83ff40e 100644 --- a/config/version.php +++ b/config/version.php @@ -1,4 +1,3 @@ boolean('is_metrics_enabled')->default(false); $table->integer('metrics_refresh_rate_seconds')->default(5); $table->integer('metrics_history_days')->default(30); - $table->string('metrics_token')->default(generateSentinelToken()); + $table->string('metrics_token')->nullable(); }); } diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php index f1b175a9c..8f9405b86 100644 --- a/database/migrations/2024_06_25_184323_update_db.php +++ b/database/migrations/2024_06_25_184323_update_db.php @@ -4,6 +4,7 @@ use App\Models\EnvironmentVariable; use App\Models\Server; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Visus\Cuid2\Cuid2; @@ -14,44 +15,45 @@ return new class extends Migration */ public function up(): void { - Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('docker_compose_pr_location'); - $table->dropColumn('docker_compose_pr'); - $table->dropColumn('docker_compose_pr_raw'); - }); - Schema::table('subscriptions', function (Blueprint $table) { - $table->dropColumn('lemon_subscription_id'); - $table->dropColumn('lemon_order_id'); - $table->dropColumn('lemon_product_id'); - $table->dropColumn('lemon_variant_id'); - $table->dropColumn('lemon_variant_name'); - $table->dropColumn('lemon_customer_id'); - $table->dropColumn('lemon_status'); - $table->dropColumn('lemon_renews_at'); - $table->dropColumn('lemon_update_payment_menthod_url'); - $table->dropColumn('lemon_trial_ends_at'); - $table->dropColumn('lemon_ends_at'); - }); - Schema::table('environment_variables', function (Blueprint $table) { - $table->string('uuid')->nullable()->after('id'); - }); + try { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('docker_compose_pr_location'); + $table->dropColumn('docker_compose_pr'); + $table->dropColumn('docker_compose_pr_raw'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('lemon_subscription_id'); + $table->dropColumn('lemon_order_id'); + $table->dropColumn('lemon_product_id'); + $table->dropColumn('lemon_variant_id'); + $table->dropColumn('lemon_variant_name'); + $table->dropColumn('lemon_customer_id'); + $table->dropColumn('lemon_status'); + $table->dropColumn('lemon_renews_at'); + $table->dropColumn('lemon_update_payment_menthod_url'); + $table->dropColumn('lemon_trial_ends_at'); + $table->dropColumn('lemon_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable()->after('id'); + }); - EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { - $environmentVariable->update([ - 'uuid' => (string) new Cuid2, - ]); - }); - Schema::table('environment_variables', function (Blueprint $table) { - $table->string('uuid')->nullable(false)->change(); - }); - Schema::table('server_settings', function (Blueprint $table) { - $table->integer('metrics_history_days')->default(7)->change(); - }); - Server::all()->each(function (Server $server) { - $server->settings->update([ - 'metrics_history_days' => 7, - ]); - }); + EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->update([ + 'uuid' => (string) new Cuid2, + ]); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable(false)->change(); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(7)->change(); + }); + + DB::table('server_settings')->update(['metrics_history_days' => 7]); + } catch (\Exception $e) { + loggy($e); + } } /** diff --git a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php index a33665bd0..ea3695b3f 100644 --- a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php +++ b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { 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); }); } diff --git a/database/migrations/2024_10_11_114331_add_required_env_variables.php b/database/migrations/2024_10_11_114331_add_required_env_variables.php new file mode 100644 index 000000000..4fde0c2bb --- /dev/null +++ b/database/migrations/2024_10_11_114331_add_required_env_variables.php @@ -0,0 +1,28 @@ +boolean('is_required')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_required'); + }); + } +}; diff --git a/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php new file mode 100644 index 000000000..d5c38501f --- /dev/null +++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php @@ -0,0 +1,54 @@ +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'); + }); + } +}; diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php new file mode 100644 index 000000000..eb878e2f6 --- /dev/null +++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php @@ -0,0 +1,22 @@ +boolean('is_shared')->default(false); + }); + } + + public function down() + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_shared'); + }); + } +} diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php new file mode 100644 index 000000000..fa01e8e85 --- /dev/null +++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php @@ -0,0 +1,41 @@ +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(); + } + } +} diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php new file mode 100644 index 000000000..7040daf44 --- /dev/null +++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php @@ -0,0 +1,22 @@ +boolean('disable_two_step_confirmation')->default(false); + }); + } + + public function down() + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('disable_two_step_confirmation'); + }); + } +}; diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php new file mode 100644 index 000000000..7a7f28e24 --- /dev/null +++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php new file mode 100644 index 000000000..76ccd1352 --- /dev/null +++ b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php @@ -0,0 +1,28 @@ +integer('server_disk_usage_notification_threshold')->default(80)->after('docker_cleanup_threshold'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('server_disk_usage_notification_threshold'); + }); + } +}; diff --git a/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php new file mode 100644 index 000000000..a2aa381b7 --- /dev/null +++ b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php @@ -0,0 +1,32 @@ +boolean('discord_notifications_server_disk_usage')->default(true)->after('discord_enabled'); + $table->boolean('smtp_notifications_server_disk_usage')->default(true)->after('smtp_enabled'); + $table->boolean('telegram_notifications_server_disk_usage')->default(true)->after('telegram_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('discord_notifications_server_disk_usage'); + $table->dropColumn('smtp_notifications_server_disk_usage'); + $table->dropColumn('telegram_notifications_server_disk_usage'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index be5083108..6e66c64f4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -26,6 +26,8 @@ class DatabaseSeeder extends Seeder S3StorageSeeder::class, StandalonePostgresqlSeeder::class, OauthSettingSeeder::class, + DisableTwoStepConfirmationSeeder::class, + SentinelSeeder::class, ]); } } diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php new file mode 100644 index 000000000..c43bf1b01 --- /dev/null +++ b/database/seeders/DisableTwoStepConfirmationSeeder.php @@ -0,0 +1,20 @@ +updateOrInsert( + [], + ['disable_two_step_confirmation' => true] + ); + } +} diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 206f04d6b..90b9d46ff 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -186,6 +186,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->call(OauthSettingSeeder::class); $this->call(PopulateSshKeysDirectorySeeder::class); + $this->call(SentinelSeeder::class); } } diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php new file mode 100644 index 000000000..117ba6782 --- /dev/null +++ b/database/seeders/SentinelSeeder.php @@ -0,0 +1,31 @@ +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"); + } + } + }); + } +} diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 90d4f77db..146e6b90a 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -4,7 +4,7 @@ "dependencies": { "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "axios": "1.7.5", "dotenv": "^16.4.5", "node-pty": "^1.0.0", diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 63832dc36..d2381f764 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -5,34 +5,38 @@ ARG TARGETPLATFORM ARG CLOUDFLARED_VERSION=2024.4.1 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 -RUN apt-get install postgresql-client-$POSTGRES_VERSION -y +# Install dependencies +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 docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf -RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc -RUN echo "alias a='php artisan'" >>/etc/bash.bashrc +RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \ + echo "alias a='php artisan'" >>/etc/bash.bashrc 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' && \ 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" -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' && \ 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" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index d0cebcbca..22a5b143a 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,4 +1,4 @@ -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base +FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base WORKDIR /var/www/html COPY composer.json composer.lock ./ diff --git a/lang/ar.json b/lang/ar.json index c5ec96c8d..4b9afbe99 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -26,5 +26,12 @@ "input.code": "الرمز لمرة واحدة", "input.recovery_code": "رمز الاسترداد", "button.save": "حفظ", - "repository.url": "أمثلة
للمستودعات العامة، استخدم https://....
للمستودعات الخاصة، استخدم git@....

سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples
سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify
سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git
سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git." + "repository.url": "أمثلة
للمستودعات العامة، استخدم https://....
للمستودعات الخاصة، استخدم git@....

سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples
سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify
سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git
سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git.", + "service.stop": "سيتم إيقاف هذه الخدمة.", + "resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).", + "resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.", + "resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.", + "resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.", + "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.", + "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي." } diff --git a/lang/en.json b/lang/en.json index fa69c7035..5ea474b02 100644 --- a/lang/en.json +++ b/lang/en.json @@ -33,5 +33,6 @@ "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_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 NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).

Use your own domain instead." } diff --git a/lang/fr.json b/lang/fr.json index cb089812e..dbd5a1bf7 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,30 +1,37 @@ { - "auth.login": "Connexion", - "auth.login.azure": "Connexion avec Microsoft", - "auth.login.bitbucket": "Connexion avec Bitbucket", - "auth.login.github": "Connexion avec GitHub", - "auth.login.gitlab": "Connexion avec Gitlab", - "auth.login.google": "Connexion avec Google", - "auth.already_registered": "Déjà enregistré ?", - "auth.confirm_password": "Confirmer le mot de passe", - "auth.forgot_password": "Mot de passe oublié", - "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", - "auth.register_now": "S'enregistrer", - "auth.logout": "Déconnexion", - "auth.register": "S'enregistrer", - "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administateur.", - "auth.reset_password": "Réinitialiser le mot de passe", - "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.", - "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", - "auth.failed.password": "Le mot de passe renseigné est incorrect.", - "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", - "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", - "input.name": "Nom", - "input.email": "Email", - "input.password": "Mot de passe", - "input.password.again": "Mot de passe identique", - "input.code": "Code à usage unique", - "input.recovery_code": "Code de récupération", - "button.save": "Sauvegarder", - "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée." + "auth.login": "Connexion", + "auth.login.azure": "Connexion avec Microsoft", + "auth.login.bitbucket": "Connexion avec Bitbucket", + "auth.login.github": "Connexion avec GitHub", + "auth.login.gitlab": "Connexion avec Gitlab", + "auth.login.google": "Connexion avec Google", + "auth.already_registered": "Déjà enregistré ?", + "auth.confirm_password": "Confirmer le mot de passe", + "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", + "auth.register_now": "S'enregistrer", + "auth.logout": "Déconnexion", + "auth.register": "S'enregistrer", + "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administrateur.", + "auth.reset_password": "Réinitialiser le mot de passe", + "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.", + "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", + "auth.failed.password": "Le mot de passe renseigné est incorrect.", + "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", + "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", + "input.name": "Nom", + "input.email": "Email", + "input.password": "Mot de passe", + "input.password.again": "Mot de passe identique", + "input.code": "Code à usage unique", + "input.recovery_code": "Code de récupération", + "button.save": "Sauvegarder", + "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée.", + "service.stop": "Ce service sera arrêté.", + "resource.docker_cleanup": "Exécuter le nettoyage Docker (supprimer les images inutilisées et le cache du builder).", + "resource.non_persistent": "Toutes les données non persistantes seront supprimées.", + "resource.delete_volumes": "Supprimer définitivement tous les volumes associés à cette ressource.", + "resource.delete_connected_networks": "Supprimer définitivement tous les réseaux non-prédéfinis associés à cette ressource.", + "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.", + "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local." } diff --git a/lang/ro.json b/lang/ro.json new file mode 100644 index 000000000..db1aa85db --- /dev/null +++ b/lang/ro.json @@ -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": "Exemple
Pentru depozite publice, utilizați https://....
Pentru depozite private, utilizați git@....

https://github.com/coollabsio/coolify-examples va fi selectată ramura main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura nodejs-fastify.
https://gitea.com/sedlav/expressjs.git va fi selectată ramura main.
https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura main.", + "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ă." +} diff --git a/openapi.yaml b/openapi.yaml index 91d5c1443..d2616e9c6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -98,6 +98,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -323,6 +327,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -548,6 +556,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -3093,7 +3105,7 @@ paths: security: - bearerAuth: [] - /healthcheck: + /health: get: summary: Healthcheck description: 'Healthcheck endpoint.' @@ -4959,7 +4971,7 @@ components: type: boolean is_reachable: type: boolean - is_server_api_enabled: + is_sentinel_enabled: type: boolean is_swarm_manager: type: boolean @@ -4981,11 +4993,11 @@ components: type: string logdrain_newrelic_license_key: type: string - metrics_history_days: + sentinel_metrics_history_days: type: integer - metrics_refresh_rate_seconds: + sentinel_metrics_refresh_rate_seconds: type: integer - metrics_token: + sentinel_token: type: string docker_cleanup_frequency: type: string diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg new file mode 100644 index 000000000..d8063e920 --- /dev/null +++ b/public/svgs/affine.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg new file mode 100644 index 000000000..446b16655 --- /dev/null +++ b/public/svgs/calcom.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg new file mode 100644 index 000000000..4a7634766 --- /dev/null +++ b/public/svgs/cloudbeaver.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/svgs/coder.svg b/public/svgs/coder.svg new file mode 100644 index 000000000..45b7f795c --- /dev/null +++ b/public/svgs/coder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png new file mode 100644 index 000000000..be121cfd0 Binary files /dev/null and b/public/svgs/cryptgeon.png differ diff --git a/public/svgs/dify.png b/public/svgs/dify.png new file mode 100644 index 000000000..326acf789 Binary files /dev/null and b/public/svgs/dify.png differ diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg new file mode 100644 index 000000000..a906f7f7e --- /dev/null +++ b/public/svgs/edgedb.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svgs/flowise.png b/public/svgs/flowise.png new file mode 100644 index 000000000..6b0be0d2a Binary files /dev/null and b/public/svgs/flowise.png differ diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png new file mode 100644 index 000000000..c6a04508f Binary files /dev/null and b/public/svgs/foundryvtt.png differ diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png new file mode 100644 index 000000000..d1a75118f Binary files /dev/null and b/public/svgs/freshrss.png differ diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg new file mode 100644 index 000000000..ff29ca654 --- /dev/null +++ b/public/svgs/heyform.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg new file mode 100644 index 000000000..9d844a772 --- /dev/null +++ b/public/svgs/immich.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg new file mode 100644 index 000000000..35b146972 --- /dev/null +++ b/public/svgs/kimai.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg new file mode 100644 index 000000000..103d47d60 --- /dev/null +++ b/public/svgs/libretranslate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg new file mode 100644 index 000000000..aa0b8e038 --- /dev/null +++ b/public/svgs/litequeen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/martin.png b/public/svgs/martin.png new file mode 100644 index 000000000..d1a99e148 Binary files /dev/null and b/public/svgs/martin.png differ diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg new file mode 100644 index 000000000..53799dd1c --- /dev/null +++ b/public/svgs/mindsdb.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png new file mode 100644 index 000000000..eb287a7cd Binary files /dev/null and b/public/svgs/mosquitto.png differ diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg new file mode 100644 index 000000000..9e5b5136f --- /dev/null +++ b/public/svgs/ntfy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png new file mode 100644 index 000000000..65885b71b Binary files /dev/null and b/public/svgs/osticket.png differ diff --git a/public/svgs/owncloud.svg b/public/svgs/owncloud.svg new file mode 100644 index 000000000..83631e3f5 --- /dev/null +++ b/public/svgs/owncloud.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png new file mode 100644 index 000000000..38db83de0 Binary files /dev/null and b/public/svgs/peppermint.png differ diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg new file mode 100644 index 000000000..69d8cf62a --- /dev/null +++ b/public/svgs/qbittorrent.svg @@ -0,0 +1,16 @@ + + + qbittorrent-new-light + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png new file mode 100644 index 000000000..c747aea05 Binary files /dev/null and b/public/svgs/traccar.png differ diff --git a/public/svgs/transmission.svg b/public/svgs/transmission.svg new file mode 100644 index 000000000..9a11f77f4 --- /dev/null +++ b/public/svgs/transmission.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/unsend.svg b/public/svgs/unsend.svg new file mode 100644 index 000000000..f5ff6fabc --- /dev/null +++ b/public/svgs/unsend.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg new file mode 100644 index 000000000..2b66b3087 --- /dev/null +++ b/public/svgs/vvveb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/wireguard.svg b/public/svgs/wireguard.svg new file mode 100644 index 000000000..81823b3eb --- /dev/null +++ b/public/svgs/wireguard.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/public/svgs/zep.png b/public/svgs/zep.png new file mode 100644 index 000000000..7d51b32dc Binary files /dev/null and b/public/svgs/zep.png differ diff --git a/public/svgs/zipline.png b/public/svgs/zipline.png new file mode 100644 index 000000000..2b8f6972d Binary files /dev/null and b/public/svgs/zipline.png differ diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 439fc4ad2..fed6ad77f 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -14,7 +14,10 @@ 'w-full' => $fullWidth, ])> @if (!$hideLabel) -