diff --git a/.dockerignore b/.dockerignore index d6abd1451..2eba3cb46 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,5 @@ yarn-error.log /_data .rnd /.ssh +.ignition.json +.env.dusk.local 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..7017b4897 --- /dev/null +++ b/.github/workflows/coolify-production-build.yml @@ -0,0 +1,139 @@ +name: Production Build (v4) + +on: + push: + branches: ["main"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/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 }} + + aarch64: + runs-on: [self-hosted, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml 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..6e4d4adc3 --- /dev/null +++ b/.github/workflows/coolify-staging-build.yml @@ -0,0 +1,125 @@ +name: Staging Build + +on: + push: + branches-ignore: ["main", "v3"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/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 }} + + aarch64: + runs-on: [self-hosted, arm64] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 5fdc32991..95a228114 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -1,14 +1,15 @@ -name: Coolify Testing Host (v4-non-prod) +name: Coolify Testing Host on: push: - branches: [ "main", "next" ] + branches: [ "next" ] paths: - .github/workflows/coolify-testing-host.yml - docker/testing-host/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-testing-host" jobs: @@ -19,21 +20,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + labels: | + coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +55,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + labels: | + coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,21 +90,36 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml deleted file mode 100644 index 268b885ac..000000000 --- a/.github/workflows/development-build.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Development Build (v4) - -on: - push: - branches-ignore: ["main", "v3"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml deleted file mode 100644 index c78c865bf..000000000 --- a/.github/workflows/production-build.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Production Build (v4) - -on: - push: - branches: ["main"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - - templates/service-templates.json - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - aarch64: - runs-on: [self-hosted, arm64] - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.gitignore b/.gitignore index ac8a1e090..1a021ab3e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ _ide_helper_models.php .rnd /.ssh scripts/load-test/* +.ignition.json +.env.dusk.local diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php index 69365f921..991146b48 100644 --- a/app/Actions/Application/GenerateConfig.php +++ b/app/Actions/Application/GenerateConfig.php @@ -11,7 +11,6 @@ class GenerateConfig public function handle(Application $application, bool $is_json = false) { - ray()->clearAll(); return $application->generateConfig(is_json: $is_json); } } diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 61005845b..cab7e45f0 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -17,7 +17,6 @@ class StopApplication if (! $server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping application: '.$application->name); if ($server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $server); @@ -36,8 +35,6 @@ class StopApplication CleanupDocker::dispatch($server, true); } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index da8c700fe..b13b10efd 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -32,8 +32,6 @@ class StopApplicationOneServer } } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index 686b60780..6676b7937 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -48,7 +48,6 @@ class PrepareCoolifyTask call_event_data: $this->remoteProcessArgs->call_event_data, ); if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { - ray('Dispatching a high priority job'); dispatch($job)->onQueue('high'); } else { dispatch($job); diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index c691f52c0..c75a30ae8 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -125,7 +125,6 @@ class RunRemoteProcess ])); } } catch (\Throwable $e) { - ray($e); } } diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php index 323c52ff9..73b2f9ac0 100644 --- a/app/Actions/Database/StartDatabase.php +++ b/app/Actions/Database/StartDatabase.php @@ -23,28 +23,28 @@ class StartDatabase return 'Server is not functional'; } switch ($database->getMorphClass()) { - case 'App\Models\StandalonePostgresql': + case \App\Models\StandalonePostgresql::class: $activity = StartPostgresql::run($database); break; - case 'App\Models\StandaloneRedis': + case \App\Models\StandaloneRedis::class: $activity = StartRedis::run($database); break; - case 'App\Models\StandaloneMongodb': + case \App\Models\StandaloneMongodb::class: $activity = StartMongodb::run($database); break; - case 'App\Models\StandaloneMysql': + case \App\Models\StandaloneMysql::class: $activity = StartMysql::run($database); break; - case 'App\Models\StandaloneMariadb': + case \App\Models\StandaloneMariadb::class: $activity = StartMariadb::run($database); break; - case 'App\Models\StandaloneKeydb': + case \App\Models\StandaloneKeydb::class: $activity = StartKeydb::run($database); break; - case 'App\Models\StandaloneDragonfly': + case \App\Models\StandaloneDragonfly::class: $activity = StartDragonfly::run($database); break; - case 'App\Models\StandaloneClickhouse': + case \App\Models\StandaloneClickhouse::class: $activity = StartClickhouse::run($database); break; } diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index a514c51b4..d7a3bc697 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -26,7 +26,7 @@ class StartDatabaseProxy $server = data_get($database, 'destination.server'); $containerName = data_get($database, 'uuid'); $proxyContainerName = "{$database->uuid}-proxy"; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $databaseType = $database->databaseType(); // $connectPredefined = data_get($database, 'service.connect_to_docker_network'); $network = $database->service->uuid; @@ -34,54 +34,54 @@ class StartDatabaseProxy $proxyContainerName = "{$database->service->uuid}-proxy"; switch ($databaseType) { case 'standalone-mariadb': - $type = 'App\Models\StandaloneMariadb'; + $type = \App\Models\StandaloneMariadb::class; $containerName = "mariadb-{$database->service->uuid}"; break; case 'standalone-mongodb': - $type = 'App\Models\StandaloneMongodb'; + $type = \App\Models\StandaloneMongodb::class; $containerName = "mongodb-{$database->service->uuid}"; break; case 'standalone-mysql': - $type = 'App\Models\StandaloneMysql'; + $type = \App\Models\StandaloneMysql::class; $containerName = "mysql-{$database->service->uuid}"; break; case 'standalone-postgresql': - $type = 'App\Models\StandalonePostgresql'; + $type = \App\Models\StandalonePostgresql::class; $containerName = "postgresql-{$database->service->uuid}"; break; case 'standalone-redis': - $type = 'App\Models\StandaloneRedis'; + $type = \App\Models\StandaloneRedis::class; $containerName = "redis-{$database->service->uuid}"; break; case 'standalone-keydb': - $type = 'App\Models\StandaloneKeydb'; + $type = \App\Models\StandaloneKeydb::class; $containerName = "keydb-{$database->service->uuid}"; break; case 'standalone-dragonfly': - $type = 'App\Models\StandaloneDragonfly'; + $type = \App\Models\StandaloneDragonfly::class; $containerName = "dragonfly-{$database->service->uuid}"; break; case 'standalone-clickhouse': - $type = 'App\Models\StandaloneClickhouse'; + $type = \App\Models\StandaloneClickhouse::class; $containerName = "clickhouse-{$database->service->uuid}"; break; } } - if ($type === 'App\Models\StandaloneRedis') { + if ($type === \App\Models\StandaloneRedis::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandalonePostgresql') { + } elseif ($type === \App\Models\StandalonePostgresql::class) { $internalPort = 5432; - } elseif ($type === 'App\Models\StandaloneMongodb') { + } elseif ($type === \App\Models\StandaloneMongodb::class) { $internalPort = 27017; - } elseif ($type === 'App\Models\StandaloneMysql') { + } elseif ($type === \App\Models\StandaloneMysql::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneMariadb') { + } elseif ($type === \App\Models\StandaloneMariadb::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneKeydb') { + } elseif ($type === \App\Models\StandaloneKeydb::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneDragonfly') { + } elseif ($type === \App\Models\StandaloneDragonfly::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneClickhouse') { + } elseif ($type === \App\Models\StandaloneClickhouse::class) { $internalPort = 9000; } $configuration_dir = database_proxy_dir($database->uuid); diff --git a/app/Actions/Database/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/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index b2092e2ef..9e9a62170 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -22,7 +22,7 @@ class StopDatabaseProxy { $server = data_get($database, 'destination.server'); $uuid = $database->uuid; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $uuid = $database->service->uuid; $server = data_get($database, 'service.server'); } 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/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 481757162..9f97dd0d4 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -6,12 +6,11 @@ use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\CreatesNewUsers; class CreateNewUser implements CreatesNewUsers { - use PasswordValidationRules; - /** * Validate and create a newly registered user. * @@ -32,7 +31,7 @@ class CreateNewUser implements CreatesNewUsers 'max:255', Rule::unique(User::class), ], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); if (User::count() == 0) { diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php deleted file mode 100644 index 92fcc7532..000000000 --- a/app/Actions/Fortify/PasswordValidationRules.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - protected function passwordRules(): array - { - return ['required', 'string', new Password, 'confirmed']; - } -} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index 7a57c5037..d3727a52c 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -5,12 +5,11 @@ namespace App\Actions\Fortify; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\ResetsUserPasswords; class ResetUserPassword implements ResetsUserPasswords { - use PasswordValidationRules; - /** * Validate and reset the user's forgotten password. * @@ -19,7 +18,7 @@ class ResetUserPassword implements ResetsUserPasswords public function reset(User $user, array $input): void { Validator::make($input, [ - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); $user->forceFill([ diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index 700563905..0c51ec56d 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -5,12 +5,11 @@ namespace App\Actions\Fortify; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\UpdatesUserPasswords; class UpdateUserPassword implements UpdatesUserPasswords { - use PasswordValidationRules; - /** * Validate and update the user's password. * @@ -20,7 +19,7 @@ class UpdateUserPassword implements UpdatesUserPasswords { Validator::make($input, [ 'current_password' => ['required', 'string', 'current_password:web'], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ], [ 'current_password.current_password' => __('The provided password does not match your current password.'), ])->validateWithBag('updatePassword'); diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index 55af1a8c0..26a1ff7bf 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -25,8 +25,6 @@ class CheckResaleLicense // } $base_url = config('coolify.license_url'); $instance_id = config('app.id'); - - ray("Checking license key against $base_url/lemon/validate"); $data = Http::withHeaders([ 'Accept' => 'application/json', ])->get("$base_url/lemon/validate", [ @@ -34,7 +32,6 @@ class CheckResaleLicense 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') { - ray('Valid & active license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -48,7 +45,6 @@ class CheckResaleLicense 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'activated') === true) { - ray('Activated license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -60,7 +56,6 @@ class CheckResaleLicense } throw new \Exception('Cannot activate license key.'); } catch (\Throwable $e) { - ray($e); $settings->update([ 'resale_license' => null, 'is_resale_license_active' => false, diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 03a0beddf..d64804758 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -88,7 +88,6 @@ class CheckProxy $portsToCheck = []; } } catch (\Exception $e) { - ray($e->getMessage()); } if (count($portsToCheck) === 0) { return false; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index f20c10123..2a6ec7d03 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -13,67 +13,63 @@ class StartProxy public function handle(Server $server, bool $async = true, bool $force = false): string|Activity { - try { - $proxyType = $server->proxyType(); - if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { - return 'OK'; - } - $commands = collect([]); - $proxy_path = $server->proxyPath(); - $configuration = CheckConfiguration::run($server); - if (! $configuration) { - throw new \Exception('Configuration is not synced'); - } - SaveConfiguration::run($server, $configuration); - $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); - $server->save(); - if ($server->isSwarm()) { - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo 'Creating required Docker Compose file.'", - "echo 'Starting coolify-proxy.'", - 'docker stack deploy -c docker-compose.yml coolify-proxy', - "echo 'Successfully started coolify-proxy.'", - ]); - } else { - $caddfile = 'import /dynamic/*.caddy'; - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", - "echo 'Creating required Docker Compose file.'", - "echo 'Pulling docker image.'", - 'docker compose pull', - 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then', - " echo 'Stopping and removing existing coolify-proxy.'", - ' docker rm -f coolify-proxy || true', - " echo 'Successfully stopped and removed existing coolify-proxy.'", - 'fi', - "echo 'Starting coolify-proxy.'", - 'docker compose up -d --remove-orphans', - "echo 'Successfully started coolify-proxy.'", - ]); - $commands = $commands->merge(connectProxyToNetworks($server)); - } - - if ($async) { - $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); - - return $activity; - } else { - instant_remote_process($commands, $server); - $server->proxy->set('status', 'running'); - $server->proxy->set('type', $proxyType); - $server->save(); - ProxyStarted::dispatch($server); - - return 'OK'; - } - } catch (\Throwable $e) { - ray($e); - throw $e; + $proxyType = $server->proxyType(); + if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { + return 'OK'; } + $commands = collect([]); + $proxy_path = $server->proxyPath(); + $configuration = CheckConfiguration::run($server); + if (! $configuration) { + throw new \Exception('Configuration is not synced'); + } + SaveConfiguration::run($server, $configuration); + $docker_compose_yml_base64 = base64_encode($configuration); + $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); + $server->save(); + if ($server->isSwarm()) { + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Starting coolify-proxy.'", + 'docker stack deploy -c docker-compose.yml coolify-proxy', + "echo 'Successfully started coolify-proxy.'", + ]); + } else { + $caddfile = 'import /dynamic/*.caddy'; + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", + "echo 'Creating required Docker Compose file.'", + "echo 'Pulling docker image.'", + 'docker compose pull', + 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then', + " echo 'Stopping and removing existing coolify-proxy.'", + ' docker rm -f coolify-proxy || true', + " echo 'Successfully stopped and removed existing coolify-proxy.'", + 'fi', + "echo 'Starting coolify-proxy.'", + 'docker compose up -d --remove-orphans', + "echo 'Successfully started coolify-proxy.'", + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); + } + + if ($async) { + $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + + return $activity; + } else { + instant_remote_process($commands, $server); + $server->proxy->set('status', 'running'); + $server->proxy->set('type', $proxyType); + $server->save(); + ProxyStarted::dispatch($server); + + return 'OK'; + } + } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index 0d36e8863..0819da07a 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -40,7 +40,6 @@ class ConfigureCloudflared ]); instant_remote_process($commands, $server); } catch (\Throwable $e) { - ray($e); $server->settings->is_cloudflare_tunnel = false; $server->settings->save(); throw $e; 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..fd4dd150c 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -16,8 +16,7 @@ class InstallDocker if (! $supported_os_type) { throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type); - $dockerVersion = '24.0'; + $dockerVersion = '26.0'; $config = base64_encode('{ "log-driver": "json-file", "log-opts": { diff --git a/app/Actions/Server/RestartContainer.php b/app/Actions/Server/RestartContainer.php new file mode 100644 index 000000000..63361d8b7 --- /dev/null +++ b/app/Actions/Server/RestartContainer.php @@ -0,0 +1,16 @@ +restartContainer($containerName); + } +} diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/StartLogDrain.php similarity index 96% rename from app/Actions/Server/InstallLogDrain.php rename to app/Actions/Server/StartLogDrain.php index 9b6741211..0e8036cd9 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -5,7 +5,7 @@ namespace App\Actions\Server; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; -class InstallLogDrain +class StartLogDrain { use AsAction; @@ -13,12 +13,16 @@ class InstallLogDrain { if ($server->settings->is_logdrain_newrelic_enabled) { $type = 'newrelic'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_highlight_enabled) { $type = 'highlight'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_axiom_enabled) { $type = 'axiom'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_custom_enabled) { $type = 'custom'; + StopLogDrain::run($server); } else { $type = 'none'; } @@ -151,6 +155,8 @@ services: - ./parsers.conf:/parsers.conf ports: - 127.0.0.1:24224:24224 + labels: + - coolify.managed=true restart: unless-stopped '); $readme = base64_encode('# New Relic Log Drain @@ -202,10 +208,8 @@ Files: throw new \Exception('Unknown log drain type.'); } $restart_command = [ - "echo 'Stopping old Fluent Bit'", - "cd $config_path && docker compose down --remove-orphans || true", "echo 'Starting Fluent Bit'", - "cd $config_path && docker compose up -d --remove-orphans", + "cd $config_path && docker compose up -d", ]; $command = array_merge($command, $add_envs_command, $restart_command); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index b79bc8f67..587ac4a8d 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -9,18 +9,57 @@ class StartSentinel { use AsAction; - public function handle(Server $server, $version = 'latest', bool $restart = false) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) { + if ($server->isSwarm() || $server->isBuildServer()) { + return; + } if ($restart) { StopSentinel::run($server); } - $metrics_history = $server->settings->metrics_history_days; - $refresh_rate = $server->settings->metrics_refresh_rate_seconds; - $token = $server->settings->metrics_token; + $version = $latestVersion ?? get_latest_sentinel_version(); + $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days'); + $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $token = data_get($server, 'settings.sentinel_token'); + $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); + $mountDir = '/data/coolify/sentinel'; + $image = "ghcr.io/coollabsio/sentinel:$version"; + if (! $endpoint) { + throw new \Exception('You should set FQDN in Instance Settings.'); + } + $environments = [ + 'TOKEN' => $token, + 'DEBUG' => $debug ? 'true' : 'false', + 'PUSH_ENDPOINT' => $endpoint, + 'PUSH_INTERVAL_SECONDS' => $pushInterval, + 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory, + ]; + $labels = [ + 'coolify.managed' => 'true', + ]; + if (isDev()) { + // data_set($environments, 'DEBUG', 'true'); + // $image = 'sentinel'; + $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; + } + $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; + $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels)); + $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image"; + instant_remote_process([ - "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", - 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', - 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, true); + 'docker rm -f coolify-sentinel || true', + "mkdir -p $mountDir", + $dockerCommand, + "chown -R 9999:root $mountDir", + "chmod -R 700 $mountDir", + ], $server); + + $server->settings->is_sentinel_enabled = true; + $server->settings->save(); + $server->sentinelHeartbeat(); } } diff --git a/app/Actions/Server/StopLogDrain.php b/app/Actions/Server/StopLogDrain.php index a5bce94a5..96c2466de 100644 --- a/app/Actions/Server/StopLogDrain.php +++ b/app/Actions/Server/StopLogDrain.php @@ -12,7 +12,7 @@ class StopLogDrain public function handle(Server $server) { try { - return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server); + return instant_remote_process(['docker rm -f coolify-log-drain'], $server, false); } catch (\Throwable $e) { return handleError($e); } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php index 21ffca3bd..aecb96c87 100644 --- a/app/Actions/Server/StopSentinel.php +++ b/app/Actions/Server/StopSentinel.php @@ -12,5 +12,6 @@ class StopSentinel public function handle(Server $server) { instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + $server->sentinelHeartbeat(isReset: true); } } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 30664df26..3185c22b7 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -18,32 +18,28 @@ class UpdateCoolify public function handle($manual_update = false) { - try { - $settings = instanceSettings(); - $this->server = Server::find(0); - if (! $this->server) { + $settings = instanceSettings(); + $this->server = Server::find(0); + if (! $this->server) { + return; + } + CleanupDocker::dispatch($this->server)->onQueue('high'); + $this->latestVersion = get_latest_version_of_coolify(); + $this->currentVersion = config('version'); + if (! $manual_update) { + if (! $settings->is_auto_update_enabled) { return; } - CleanupDocker::dispatch($this->server)->onQueue('high'); - $this->latestVersion = get_latest_version_of_coolify(); - $this->currentVersion = config('version'); - if (! $manual_update) { - if (! $settings->is_auto_update_enabled) { - return; - } - if ($this->latestVersion === $this->currentVersion) { - return; - } - if (version_compare($this->latestVersion, $this->currentVersion, '<')) { - return; - } + if ($this->latestVersion === $this->currentVersion) { + return; + } + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + return; } - $this->update(); - $settings->new_version_available = false; - $settings->save(); - } catch (\Throwable $e) { - throw $e; } + $this->update(); + $settings->new_version_available = false; + $settings->save(); } private function update() diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index f28e5490e..9c4b0349c 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -40,7 +40,6 @@ class DeleteService foreach ($commands as $command) { $result = instant_remote_process([$command], $server, false); if ($result !== 0) { - ray("Failed to execute: $command"); } } } diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 06d2e0efb..5b8279221 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -12,7 +12,6 @@ class StartService public function handle(Service $service) { - ray('Starting service: '.$service->name); $service->saveComposeConfigs(); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 5c7bbc2aa..046d94ced 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -28,8 +28,6 @@ class StopService } } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 66c25ec27..da757c99f 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command public function handle() { - ray('Running cleanup stucked resources.'); echo "Running cleanup stucked resources.\n"; $this->cleanup_stucked_resources(); } diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php index d220aa00b..5054d125c 100644 --- a/app/Console/Commands/CloudCleanupSubscriptions.php +++ b/app/Console/Commands/CloudCleanupSubscriptions.php @@ -19,7 +19,6 @@ class CloudCleanupSubscriptions extends Command return; } - ray()->clearAll(); $this->info('Cleaning up subcriptions teams'); $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 36722564c..cda4ca84f 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -15,7 +15,6 @@ use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\StatusChanged; use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupSuccess; -use App\Notifications\Database\DailyBackup; use App\Notifications\Test; use Exception; use Illuminate\Console\Command; @@ -121,28 +120,10 @@ class Emails extends Command $this->mail = (new Test)->toMail(); $this->sendEmail(); break; - case 'database-backup-statuses-daily': - $scheduled_backups = ScheduledDatabaseBackup::all(); - $databases = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - if ($last_days_backups->isEmpty()) { - continue; - } - $failed = $last_days_backups->where('status', 'failed'); - $database = $scheduled_backup->database; - $databases->put($database->name, [ - 'failed_count' => $failed->count(), - ]); - } - $this->mail = (new DailyBackup($databases))->toMail(); - $this->sendEmail(); - break; case 'application-deployment-success-daily': $applications = Application::all(); foreach ($applications as $application) { $deployments = $application->get_last_days_deployments(); - ray($deployments); if ($deployments->isEmpty()) { continue; } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index ad7bff86d..8f68966a5 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -180,7 +180,7 @@ class Init extends Command 'save_s3' => false, 'frequency' => '0 0 * * *', 'database_id' => $database->id, - 'database_type' => 'App\Models\StandalonePostgresql', + 'database_type' => \App\Models\StandalonePostgresql::class, 'team_id' => 0, ]); } @@ -219,7 +219,6 @@ class Init extends Command } $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); foreach ($queued_inprogress_deployments as $deployment) { - ray($deployment->id, $deployment->status); echo "Cleaning up deployment: {$deployment->id}\n"; $deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->save(); diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index 81333b868..f0131b7b2 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -36,8 +36,6 @@ class NotifyDemo extends Command return; } - - ray($channel); } private function showHelp() diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index da04cefce..9720e81ac 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -39,8 +39,8 @@ class ServicesGenerate extends Command $serviceTemplatesJson[$name] = $parsed; } } - $serviceTemplatesJson = json_encode($serviceTemplatesJson); - file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson); + $serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL); } private function process_file($file) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1430fcdd1..cd560eee3 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,17 +2,17 @@ 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\ServerCleanupMux; use App\Jobs\UpdateCoolifyJob; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; @@ -20,14 +20,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 +38,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,28 +101,39 @@ 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 { $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer(); } + // Cleanup multiplexed connections every hour + $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + + // Temporary solution until we have better memory management for Sentinel + if ($server->isSentinelEnabled()) { + $schedule->job(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + })->daily()->onOneServer(); + } } } - private function check_scheduled_backups($schedule) + private function checkScheduledBackups($schedule): void { $scheduled_backups = ScheduledDatabaseBackup::all(); if ($scheduled_backups->isEmpty()) { @@ -137,7 +144,6 @@ class Kernel extends ConsoleKernel continue; } if (is_null(data_get($scheduled_backup, 'database'))) { - ray('database not found'); $scheduled_backup->delete(); continue; @@ -159,7 +165,7 @@ class Kernel extends ConsoleKernel } } - private function check_scheduled_tasks($schedule) + private function checkScheduledTasks($schedule): void { $scheduled_tasks = ScheduledTask::all(); if ($scheduled_tasks->isEmpty()) { @@ -173,7 +179,6 @@ class Kernel extends ConsoleKernel $application = $scheduled_task->application; if (! $application && ! $service) { - ray('application/service attached to scheduled task does not exist'); $scheduled_task->delete(); continue; diff --git a/app/Enums/Role.php b/app/Enums/Role.php new file mode 100644 index 000000000..a37a5076c --- /dev/null +++ b/app/Enums/Role.php @@ -0,0 +1,37 @@ + 1, + self::ADMIN => 2, + self::OWNER => 3, + }; + } + + public function lt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() < $role->rank(); + } + + public function gt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() > $role->rank(); + } +} diff --git a/app/Enums/StaticImageTypes.php b/app/Enums/StaticImageTypes.php new file mode 100644 index 000000000..5f5304bf8 --- /dev/null +++ b/app/Enums/StaticImageTypes.php @@ -0,0 +1,8 @@ +getMessage())->contains('No space left on device')) { return; } - ray('reporting to sentry'); Integration::captureUnhandledException($e); }); } diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index eef179048..46dd8120e 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -132,6 +132,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -236,6 +237,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -339,6 +341,7 @@ class ApplicationsController extends Controller 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], 'install_command' => ['type' => 'string', 'description' => 'The install command.'], 'build_command' => ['type' => 'string', 'description' => 'The build command.'], 'start_command' => ['type' => 'string', 'description' => 'The start command.'], @@ -633,7 +636,7 @@ class ApplicationsController extends Controller private function create_application(Request $request, $type) { - $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server']; + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -672,6 +675,7 @@ class ApplicationsController extends Controller $instantDeploy = $request->instant_deploy; $githubAppUuid = $request->github_app_uuid; $useBuildServer = $request->use_build_server; + $isStatic = $request->is_static; $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); if (! $project) { @@ -700,8 +704,7 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -709,19 +712,21 @@ class ApplicationsController extends Controller 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } + $return = $this->validateDataApplications($request, $server); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } + $application = new Application; removeUnnecessaryFieldsFromRequest($request); @@ -744,11 +749,15 @@ class ApplicationsController extends Controller $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + $application->save(); + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } if (isset($useBuildServer)) { $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); } - $application->save(); $application->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); @@ -782,8 +791,7 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -792,10 +800,10 @@ class ApplicationsController extends Controller 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', - 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -882,8 +890,8 @@ class ApplicationsController extends Controller if ($request->build_pack === 'dockercompose') { $request->offsetSet('ports_exposes', '80'); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + + $validationRules = [ 'git_repository' => 'string|required', 'git_branch' => 'string|required', 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], @@ -892,10 +900,10 @@ class ApplicationsController extends Controller 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose_raw' => 'string|nullable', - 'docker_compose_domains' => 'array|nullable', - 'docker_compose_custom_start_command' => 'string|nullable', - 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); if ($validator->fails()) { return response()->json([ @@ -975,10 +983,13 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'dockerfile-'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + + $validationRules = [ 'dockerfile' => 'string|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1057,12 +1068,14 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'docker-image-'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'docker_registry_image_name' => 'string|required', 'docker_registry_image_tag' => 'string', 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1135,10 +1148,12 @@ class ApplicationsController extends Controller if (! $request->has('name')) { $request->offsetSet('name', 'service'.new Cuid2); } - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'docker_compose_raw' => 'string|required', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', @@ -1488,8 +1503,7 @@ class ApplicationsController extends Controller $server = $application->destination->server; $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server']; - $validator = customApiValidator($request->all(), [ - sharedDataApplications(), + $validationRules = [ 'name' => 'string|max:255', 'description' => 'string|nullable', 'static_image' => 'string', @@ -1499,7 +1513,9 @@ class ApplicationsController extends Controller 'docker_compose_domains' => 'array|nullable', 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', - ]); + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); // Validate ports_exposes if ($request->has('ports_exposes')) { @@ -1563,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/DeployController.php b/app/Http/Controllers/Api/DeployController.php index d1c8f5ea6..666dc55a5 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -292,7 +292,7 @@ class DeployController extends Controller return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } switch ($resource?->getMorphClass()) { - case 'App\Models\Application': + case \App\Models\Application::class: $deployment_uuid = new Cuid2; queue_application_deployment( application: $resource, @@ -301,7 +301,7 @@ class DeployController extends Controller ); $message = "Application {$resource->name} deployment queued."; break; - case 'App\Models\Service': + case \App\Models\Service::class: StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; break; diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 2414b7a42..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/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php index 1fd5792e0..4180cef9a 100644 --- a/app/Http/Controllers/Api/ResourcesController.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -53,7 +53,7 @@ class ResourcesController extends Controller $resources = $resources->flatten(); $resources = $resources->map(function ($resource) { $payload = $resource->toArray(); - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { $payload['status'] = $resource->status(); } else { $payload['status'] = $resource->status; diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index a49515579..af4e008ef 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); @@ -248,7 +249,6 @@ class ServersController extends Controller return $payload; }); $server = $this->removeSensitiveData($server); - ray($server); return response()->json(serializeApiResponse(data_get($server, 'resources'))); } @@ -726,6 +726,7 @@ class ServersController extends Controller return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); } $server->delete(); + DeleteServer::dispatch($server); return response()->json(['message' => 'Server deleted.']); } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 38d9e2272..581118e16 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -112,57 +112,48 @@ class Controller extends BaseController public function accept_invitation() { - try { - $resetPassword = request()->query('reset-password'); - $invitationUuid = request()->route('uuid'); - $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - $invitationValid = $invitation->isValid(); - if ($invitationValid) { - if ($resetPassword) { - $user->update([ - 'password' => Hash::make($invitationUuid), - 'force_password_reset' => true, - ]); - } - if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { - $invitation->delete(); - - return redirect()->route('team.index'); - } - $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $resetPassword = request()->query('reset-password'); + $invitationUuid = request()->route('uuid'); + $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + $invitationValid = $invitation->isValid(); + if ($invitationValid) { + if ($resetPassword) { + $user->update([ + 'password' => Hash::make($invitationUuid), + 'force_password_reset' => true, + ]); + } + if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { $invitation->delete(); - if (auth()->user()?->id !== $user->id) { - return redirect()->route('login'); - } - refreshSession($invitation->team); return redirect()->route('team.index'); - } else { - abort(401); } - } catch (\Throwable $e) { - ray($e->getMessage()); - throw $e; + $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $invitation->delete(); + if (auth()->user()?->id !== $user->id) { + return redirect()->route('login'); + } + refreshSession($invitation->team); + + return redirect()->route('team.index'); + } else { + abort(401); } } public function revoke_invitation() { - try { - $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(auth()->user())) { - return redirect()->route('login'); - } - if (auth()->user()->id !== $user->id) { - abort(401); - } - $invitation->delete(); - - return redirect()->route('team.index'); - } catch (\Throwable $e) { - throw $e; + $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + if (is_null(auth()->user())) { + return redirect()->route('login'); } + if (auth()->user()->id !== $user->id) { + abort(401); + } + $invitation->delete(); + + return redirect()->route('team.index'); } } diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 630d01045..3a3f18c9c 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -35,8 +35,6 @@ class OauthController extends Controller return redirect('/'); } catch (\Exception $e) { - ray($e->getMessage()); - $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback'; return redirect()->route('login')->withErrors([__($errorCode)]); diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index ef85d59e3..8c74f95e5 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -16,7 +16,6 @@ class Bitbucket extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -55,7 +54,6 @@ class Bitbucket extends Controller 'message' => 'Nothing to do. No branch found in the request.', ]); } - ray('Manual webhook bitbucket push event with branch: '.$branch); } if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { $branch = data_get($payload, 'pullrequest.destination.branch.name'); @@ -85,7 +83,6 @@ class Bitbucket extends Controller 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -96,13 +93,11 @@ class Bitbucket extends Controller 'status' => 'failed', 'message' => 'Server is not functional.', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -126,7 +121,6 @@ class Bitbucket extends Controller } if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { @@ -171,7 +165,6 @@ class Bitbucket extends Controller } } if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { - ray('Pull request rejected'); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { $found->delete(); @@ -191,12 +184,9 @@ class Bitbucket extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index e042b74c9..3689ca7b3 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -19,15 +19,12 @@ class Gitea extends Controller $return_payloads = collect([]); $x_gitea_delivery = request()->header('X-Gitea-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) { return Str::contains($file, $x_gitea_delivery); })->first(); if ($gitea_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -67,8 +64,6 @@ class Gitea extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray($changed_files); - ray('Manual Webhook Gitea Push Event with branch: '.$branch); } if ($x_gitea_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -77,7 +72,6 @@ class Gitea extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -99,7 +93,6 @@ class Gitea extends Controller $webhook_secret = data_get($application, 'manual_webhook_secret_gitea'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -122,7 +115,6 @@ class Gitea extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -228,12 +220,9 @@ class Gitea extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 5f3ba933b..3683adaa8 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -25,15 +25,12 @@ class Github extends Controller $return_payloads = collect([]); $x_github_delivery = request()->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -73,7 +70,6 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitHub Push Event with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -82,7 +78,6 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -104,7 +99,6 @@ class Github extends Controller $webhook_secret = data_get($application, 'manual_webhook_secret_github'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -127,7 +121,6 @@ class Github extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -232,12 +225,9 @@ class Github extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -249,15 +239,12 @@ class Github extends Controller $id = null; $x_github_delivery = $request->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -313,7 +300,6 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -322,7 +308,6 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); @@ -356,7 +341,6 @@ class Github extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -460,8 +444,6 @@ class Github extends Controller return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -505,7 +487,6 @@ class Github extends Controller try { $installation_id = $request->get('installation_id'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index ec7f51a0d..f56711bad 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -17,7 +17,6 @@ class Gitlab extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -67,7 +66,6 @@ class Gitlab extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitLab Push Event with branch: '.$branch); } if ($x_gitlab_event === 'merge_request') { $action = data_get($payload, 'object_attributes.action'); @@ -84,7 +82,6 @@ class Gitlab extends Controller return response($return_payloads); } - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } $applications = Application::where('git_repository', 'like', "%$full_name%"); if ($x_gitlab_event === 'push') { @@ -117,7 +114,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -128,7 +124,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Server is not functional', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } @@ -136,7 +131,6 @@ class Gitlab extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -171,7 +165,6 @@ class Gitlab extends Controller 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); - ray('Deployments disabled for '.$application->name); } } if ($x_gitlab_event === 'merge_request') { @@ -207,7 +200,6 @@ class Gitlab extends Controller is_webhook: true, git_type: 'gitlab' ); - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -219,7 +211,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Preview deployments disabled', ]); - ray('Preview deployments disabled for '.$application->name); } } elseif ($action === 'closed' || $action === 'close' || $action === 'merge') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); @@ -253,8 +244,6 @@ class Gitlab extends Controller return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index 164322586..55c94a618 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -22,7 +22,6 @@ class Stripe extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php index ea635836c..dec8ca72d 100644 --- a/app/Http/Controllers/Webhook/Waitlist.php +++ b/app/Http/Controllers/Webhook/Waitlist.php @@ -13,7 +13,6 @@ class Waitlist extends Controller { $email = request()->get('email'); $confirmation_code = request()->get('confirmation_code'); - ray($email, $confirmation_code); try { $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); if ($found) { @@ -36,7 +35,6 @@ class Waitlist extends Controller return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist confirmation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } @@ -58,7 +56,6 @@ class Waitlist extends Controller return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist cancellation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index 471e6d602..dc6be5da3 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -10,7 +10,6 @@ class ApiAllowed { public function handle(Request $request, Closure $next): Response { - ray()->clearAll(); if (isCloud()) { return $next($request); } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9ae383a9f..c80b4a7db 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -208,7 +208,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}"; } } - ray('New container name: ', $this->container_name)->green(); $this->saved_outputs = collect(); @@ -298,7 +297,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR); } - ray($e); $this->fail($e); throw $e; } finally { @@ -389,7 +387,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->dockerImageTag = $this->application->docker_registry_image_tag; } - ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'"); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); $this->generate_image_names(); $this->prepare_builder_image(); @@ -712,38 +709,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { $forceFail = true; if (str($this->application->docker_registry_image_name)->isEmpty()) { - ray('empty docker_registry_image_name'); - return; } if ($this->restart_only) { - ray('restart_only'); - return; } if ($this->application->build_pack === 'dockerimage') { - ray('dockerimage'); - return; } if ($this->use_build_server) { - ray('use_build_server'); $forceFail = true; } if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') { - ray('isSwarm'); $forceFail = true; } if ($this->application->additional_servers->count() > 0) { - ray('additional_servers'); $forceFail = true; } if ($this->is_this_additional_server) { - ray('this is an additional_servers, no pushy pushy'); - return; } - ray('push_to_docker_registry noww: '.$this->production_image_name); try { instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); $this->application_deployment_queue->addLogEntry('----------------------------------------'); @@ -775,7 +760,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($forceFail) { throw new RuntimeException($e->getMessage(), 69420); } - ray($e); } } @@ -1386,8 +1370,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return; } if ($destination_ids->contains($this->destination->id)) { - ray('Same destination found in additional destinations. Skipping.'); - return; } foreach ($destination_ids as $destination_id) { @@ -2449,7 +2431,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if ($this->application->build_pack !== 'dockercompose') { $code = $exception->getCode(); - ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index 6120d1cba..2eefc4dd2 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -31,8 +31,6 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue { try { if ($this->application->is_public_repository()) { - ray('Public repository. Skipping comment update.'); - return; } if ($this->status === ProcessStatus::CLOSED) { @@ -53,16 +51,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue $this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n"; $this->body .= 'Last updated at: '.now()->toDateTimeString().' CET'; - - ray('Updating comment', $this->body); if ($this->preview->pull_request_issue_comment_id) { $this->update_comment(); } else { $this->create_comment(); } } catch (\Throwable $e) { - ray($e); - return $e; } } @@ -73,7 +67,6 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue 'body' => $this->body, ], throwError: false); if (data_get($data, 'message') === 'Not Found') { - ray('Comment not found. Creating new one.'); $this->create_comment(); } } diff --git a/app/Jobs/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php new file mode 100644 index 000000000..788db89ea --- /dev/null +++ b/app/Jobs/CheckAndStartSentinelJob.php @@ -0,0 +1,52 @@ +server, false); + $sentinelFoundJson = json_decode($sentinelFound, true); + $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); + if ($sentinelStatus !== 'running') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + // If sentinel is running, check if it needs an update + $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($runningVersion)) { + $runningVersion = '0.0.0'; + } + if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest'); + + return; + } else { + if (version_compare($runningVersion, $latestVersion, '<')) { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + } + } +} diff --git a/app/Jobs/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/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index b55ae9967..7479867b6 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -22,7 +22,6 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue CheckResaleLicense::run(); } catch (\Throwable $e) { send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage()); - ray($e); throw $e; } } diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index b8ca8b7ed..f185ab781 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -20,18 +20,15 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S public function handle(): void { try { - ray('Cleaning up helper containers on '.$this->server->name); $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false); $containerIds = collect(json_decode($containers))->pluck('ID'); if ($containerIds->count() > 0) { foreach ($containerIds as $containerId) { - ray('Removing container '.$containerId); instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); } } } catch (\Throwable $e) { send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index d9de3f6fe..acbe82338 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -29,13 +29,11 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho // $this->cleanup_waitlist(); } catch (\Throwable $e) { send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); } try { $this->cleanup_invitation_link(); } catch (\Throwable $e) { send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 769739d5e..196af2a83 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -72,7 +72,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue return; } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $this->database = data_get($this->backup, 'database'); $this->server = $this->database->service->server; $this->s3 = $this->backup->s3; @@ -92,11 +92,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $status = str(data_get($this->database, 'status')); if (! $status->startsWith('running') && $this->database->id !== 0) { - ray('database not running'); - return; } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $databaseType = $this->database->databaseType(); $serviceUuid = $this->database->service->uuid; $serviceName = str($this->database->service->name)->slug(); @@ -222,7 +220,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue // Format: db1:collection1,collection2|db2:collection3,collection4 $databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); - ray($databasesToBackup); } elseif (str($databaseType)->contains('mysql')) { // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); @@ -244,7 +241,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } foreach ($databasesToBackup as $database) { $size = 0; - ray('Backing up '.$database); try { if (str($databaseType)->contains('postgres')) { $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; @@ -377,10 +373,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -400,16 +394,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } $commands[] = $backupCommand; - ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -428,10 +419,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -445,16 +434,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } else { $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; } - ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -498,14 +484,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $network = $this->database->service->destination->network; } else { $network = $this->database->destination->network; } - $this->ensureHelperImageAvailable(); - $fullImageName = $this->getFullImageName(); if (isDev()) { @@ -538,35 +522,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/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index 9c0a2b55b..d483fe4c2 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -42,7 +42,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); } catch (\Throwable $e) { send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 4b208fc31..a92e44c6b 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue { @@ -17,29 +16,12 @@ 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]); - } - } - - } catch (\Throwable $e) { - send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } + $helperImage = config('coolify.helper_image'); + $latest_version = instanceSettings()->helper_version; + instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); } } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php deleted file mode 100644 index 32f84e6d5..000000000 --- a/app/Jobs/PullSentinelImageJob.php +++ /dev/null @@ -1,47 +0,0 @@ -server, false); - if (empty($local_version)) { - $local_version = '0.0.0'; - } - if (version_compare($local_version, $version, '<')) { - StartSentinel::run($this->server, $version, true); - - return; - } - ray('Sentinel image is up to date'); - } catch (\Throwable $e) { - // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 72c971033..bde5e6c7a 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -25,7 +25,6 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue if (isDev() || isCloud()) { return; } - ray('PullTemplatesAndVersions service-templates'); $response = Http::retry(3, 1000)->get(config('constants.services.official')); if ($response->successful()) { $services = $response->json(); @@ -35,7 +34,6 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue } } catch (\Throwable $e) { send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php deleted file mode 100644 index 79ebad7a8..000000000 --- a/app/Jobs/PullVersionsFromCDN.php +++ /dev/null @@ -1,39 +0,0 @@ -get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } else { - send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); - } - } - } catch (\Throwable $e) { - throw $e; - } - } -} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php new file mode 100644 index 000000000..74ab8fa40 --- /dev/null +++ b/app/Jobs/PushServerUpdateJob.php @@ -0,0 +1,368 @@ +containers = collect(); + $this->foundApplicationIds = collect(); + $this->foundDatabaseUuids = collect(); + $this->foundServiceApplicationIds = collect(); + $this->foundApplicationPreviewsIds = collect(); + $this->foundServiceDatabaseIds = collect(); + $this->allApplicationIds = collect(); + $this->allDatabaseUuids = collect(); + $this->allTcpProxyUuids = collect(); + $this->allServiceApplicationIds = collect(); + $this->allServiceDatabaseIds = collect(); + } + + public function handle() + { + // TODO: Swarm is not supported yet + if (! $this->data) { + throw new \Exception('No data provided'); + } + $data = collect($this->data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + if ($this->containers->isEmpty()) { + return; + } + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->previews = $this->server->previews(); + $this->services = $this->server->services()->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { + return $application->additional_servers->count() === 0; + })->pluck('id'); + $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { + return $application->additional_servers->count() > 0; + }); + $this->allApplicationPreviewsIds = $this->previews->pluck('id'); + $this->allDatabaseUuids = $this->databases->pluck('uuid'); + $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); + $this->services->each(function ($service) { + $service->applications()->pluck('id')->each(function ($applicationId) { + $this->allServiceApplicationIds->push($applicationId); + }); + $service->databases()->pluck('id')->each(function ($databaseId) { + $this->allServiceDatabaseIds->push($databaseId); + }); + }); + + foreach ($this->containers as $container) { + $containerStatus = data_get($container, 'state', 'exited'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = collect(data_get($container, 'labels')); + $coolify_managed = $labels->has('coolify.managed'); + if ($coolify_managed) { + $name = data_get($container, 'name'); + if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) { + $this->foundLogDrainContainer = true; + } + if ($labels->has('coolify.applicationId')) { + $applicationId = $labels->get('coolify.applicationId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0'); + try { + if ($pullRequestId === '0') { + if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationIds->push($applicationId); + } + $this->updateApplicationStatus($applicationId, $containerStatus); + } else { + if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationPreviewsIds->push($applicationId); + } + $this->updateApplicationPreviewStatus($applicationId, $containerStatus); + } + } catch (\Exception $e) { + } + } elseif ($labels->has('coolify.serviceId')) { + $serviceId = $labels->get('coolify.serviceId'); + $subType = $labels->get('coolify.service.subType'); + $subId = $labels->get('coolify.service.subId'); + if ($subType === 'application' && $this->isRunning($containerStatus)) { + $this->foundServiceApplicationIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } elseif ($subType === 'database' && $this->isRunning($containerStatus)) { + $this->foundServiceDatabaseIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } + + } else { + $uuid = $labels->get('com.docker.compose.service'); + $type = $labels->get('coolify.type'); + if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) { + $this->foundProxy = true; + } elseif ($type === 'service' && $this->isRunning($containerStatus)) { + } else { + if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->foundDatabaseUuids->push($uuid); + if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); + } else { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false); + } + } + } + } + } + } + + $this->updateProxyStatus(); + + $this->updateNotFoundApplicationStatus(); + $this->updateNotFoundApplicationPreviewStatus(); + $this->updateNotFoundDatabaseStatus(); + $this->updateNotFoundServiceStatus(); + + $this->updateAdditionalServersStatus(); + + $this->checkLogDrainContainer(); + } + + private function updateApplicationStatus(string $applicationId, string $containerStatus) + { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus) + { + $application = $this->previews->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateNotFoundApplicationStatus() + { + $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); + if ($notFoundApplicationIds->isNotEmpty()) { + $notFoundApplicationIds->each(function ($applicationId) { + $application = Application::find($applicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + } + + private function updateNotFoundApplicationPreviewStatus() + { + $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); + if ($notFoundApplicationPreviewsIds->isNotEmpty()) { + $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) { + $applicationPreview = ApplicationPreview::find($applicationPreviewId); + if ($applicationPreview) { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + } + }); + } + } + + private function updateProxyStatus() + { + // If proxy is not found, start it + if ($this->server->isProxyShouldRun()) { + if ($this->foundProxy === false) { + try { + if (CheckProxy::run($this->server)) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + + } + + private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false) + { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if (! $database) { + return; + } + $database->status = $containerStatus; + $database->save(); + if ($this->isRunning($containerStatus) && $tcpProxy) { + $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { + return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; + })->first(); + if (! $tcpProxyContainerFound) { + StartDatabaseProxy::dispatch($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } else { + } + } + } + + private function updateNotFoundDatabaseStatus() + { + $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); + if ($notFoundDatabaseUuids->isNotEmpty()) { + $notFoundDatabaseUuids->each(function ($databaseUuid) { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if ($database) { + $database->status = 'exited'; + $database->save(); + if ($database->is_public) { + StopDatabaseProxy::dispatch($database); + } + } + }); + } + } + + private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) + { + $service = $this->services->where('id', $serviceId)->first(); + if (! $service) { + return; + } + if ($subType === 'application') { + $application = $service->applications()->where('id', $subId)->first(); + $application->status = $containerStatus; + $application->save(); + } elseif ($subType === 'database') { + $database = $service->databases()->where('id', $subId)->first(); + $database->status = $containerStatus; + $database->save(); + } else { + } + } + + private function updateNotFoundServiceStatus() + { + $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); + $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + if ($notFoundServiceApplicationIds->isNotEmpty()) { + $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { + $application = ServiceApplication::find($serviceApplicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + if ($notFoundServiceDatabaseIds->isNotEmpty()) { + $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { + $database = ServiceDatabase::find($serviceDatabaseId); + if ($database) { + $database->status = 'exited'; + $database->save(); + } + }); + } + } + + private function updateAdditionalServersStatus() + { + $this->allApplicationsWithAdditionalServers->each(function ($application) { + ComplexStatusCheck::run($application); + }); + } + + private function isRunning(string $containerStatus) + { + return str($containerStatus)->contains('running'); + } + + private function checkLogDrainContainer() + { + if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { + StartLogDrain::dispatch($this->server); + } + } +} diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 070598e71..7af8205fc 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -31,7 +31,6 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue send_user_an_email($mail, $this->email); } catch (\Throwable $e) { send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index 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..553796e87 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\Actions\Server\StartLogDrain; use App\Models\Server; -use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -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,74 +43,64 @@ 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) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } } } catch (\Throwable $e) { - ray($e->getMessage()); - return handleError($e); } } - private function serverStatus() - { - ['uptime' => $uptime] = $this->server->validateConnection(false); - if ($uptime) { - if ($this->server->unreachable_notification_sent === true) { - $this->server->update(['unreachable_notification_sent' => false]); - } - } else { - // $this->server->team?->notify(new Unreachable($this->server)); - foreach ($this->applications as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - - return false; - } - - return true; - - } - private function checkLogDrainContainer() { $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { @@ -123,295 +109,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue if ($foundLogDrainContainer) { $status = data_get($foundLogDrainContainer, 'State.Status'); if ($status !== 'running') { - InstallLogDrain::dispatch($this->server); + StartLogDrain::dispatch($this->server); } } else { - InstallLogDrain::dispatch($this->server); - } - } - - private function containerStatus() - { - - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; - - foreach ($this->containers as $container) { - if ($this->server->isSwarm()) { - $labels = data_get($container, 'Spec.Labels'); - $uuid = data_get($labels, 'coolify.name'); - } else { - $labels = data_get($container, 'Config.Labels'); - } - $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; - $labels = Arr::undot(format_docker_labels_to_json($labels)); - $applicationId = data_get($labels, 'coolify.applicationId'); - if ($applicationId) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - if ($pullRequestId) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $foundApplicationPreviews[] = $preview->id; - $statusFromDb = $preview->status; - if ($statusFromDb !== $containerStatus) { - $preview->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } else { - $application = $this->applications->where('id', $applicationId)->first(); - if ($application) { - $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } - } else { - $uuid = data_get($labels, 'com.docker.compose.service'); - $type = data_get($labels, 'coolify.type'); - - if ($uuid) { - if ($type === 'service') { - $database_id = data_get($labels, 'coolify.service.subId'); - if ($database_id) { - $service_db = ServiceDatabase::where('id', $database_id)->first(); - if ($service_db) { - $uuid = data_get($service_db, 'service.uuid'); - if ($uuid) { - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - } - } - } - } - } - } else { - $database = $this->databases->where('uuid', $uuid)->first(); - if ($database) { - $isPublic = data_get($database, 'is_public'); - $foundDatabases[] = $database->id; - $statusFromDb = $database->status; - if ($statusFromDb !== $containerStatus) { - $database->update(['status' => $containerStatus]); - } - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - } - } - } else { - // Notify user that this container should not be there. - } - } - } - if (data_get($container, 'Name') === '/coolify-db') { - $foundDatabases[] = 0; - } - } - $serviceLabelId = data_get($labels, 'coolify.serviceId'); - if ($serviceLabelId) { - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = $this->services->where('id', $serviceLabelId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); - } else { - $service = $service->databases()->where('id', $subId)->first(); - } - if ($service) { - $foundServices[] = "$service->id-$service->name"; - $statusFromDb = $service->status; - if ($statusFromDb !== $containerStatus) { - // ray('Updating status: ' . $containerStatus); - $service->update(['status' => $containerStatus]); - } - } - } - } - $exitedServices = collect([]); - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - if (in_array("$app->id-$app->name", $foundServices)) { - continue; - } else { - $exitedServices->push($app); - } - } - foreach ($dbs as $db) { - if (in_array("$db->id-$db->name", $foundServices)) { - continue; - } else { - $exitedServices->push($db); - } - } - } - $exitedServices = $exitedServices->unique('id'); - foreach ($exitedServices as $exitedService) { - if (str($exitedService->status)->startsWith('exited')) { - continue; - } - $name = data_get($exitedService, 'name'); - $fqdn = data_get($exitedService, 'fqdn'); - if ($name) { - if ($fqdn) { - $containerName = "$name, available at $fqdn"; - } else { - $containerName = $name; - } - } else { - if ($fqdn) { - $containerName = $fqdn; - } else { - $containerName = null; - } - } - $projectUuid = data_get($service, 'environment.project.uuid'); - $serviceUuid = data_get($service, 'uuid'); - $environmentName = data_get($service, 'environment.name'); - - if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - $exitedService->update(['status' => 'exited']); - } - - $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $this->applications->where('id', $applicationId)->first(); - if (str($application->status)->startsWith('exited')) { - continue; - } - $application->update(['status' => 'exited']); - - $name = data_get($application, 'name'); - $fqdn = data_get($application, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); - - if ($projectUuid && $applicationUuid && $environment) { - $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $this->previews->where('id', $previewId)->first(); - if (str($preview->status)->startsWith('exited')) { - continue; - } - $preview->update(['status' => 'exited']); - - $name = data_get($preview, 'name'); - $fqdn = data_get($preview, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($preview, 'application.environment.project.uuid'); - $environmentName = data_get($preview, 'application.environment.name'); - $applicationUuid = data_get($preview, 'application.uuid'); - - if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $this->databases->where('id', $database)->first(); - if (str($database->status)->startsWith('exited')) { - continue; - } - $database->update(['status' => 'exited']); - - $name = data_get($database, 'name'); - $fqdn = data_get($database, 'fqdn'); - - $containerName = $name; - - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); - - if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + StartLogDrain::dispatch($this->server); } } } diff --git a/app/Jobs/ServerCleanupMux.php b/app/Jobs/ServerCleanupMux.php new file mode 100644 index 000000000..70ea59752 --- /dev/null +++ b/app/Jobs/ServerCleanupMux.php @@ -0,0 +1,42 @@ +server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + SshMultiplexingHelper::removeMuxFile($this->server); + + } catch (\Throwable $e) { + return handleError($e); + } + + } +} diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 1f09d5a3b..084b6bf81 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -32,9 +32,7 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue $servers_count = $servers->count(); $limit = data_get($this->team->limits, 'serverLimit', 2); $number_of_servers_to_disable = $servers_count - $limit; - ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable); if ($number_of_servers_to_disable > 0) { - ray('Disabling servers'); $servers = $servers->sortbyDesc('created_at'); $servers_to_disable = $servers->take($number_of_servers_to_disable); $servers_to_disable->each(function ($server) { @@ -51,7 +49,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue } } catch (\Throwable $e) { send_internal_notification('ServerLimitCheckJob failed with: '.$e->getMessage()); - ray($e->getMessage()); return handleError($e); } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php deleted file mode 100644 index fcc33c859..000000000 --- a/app/Jobs/ServerStatusJob.php +++ /dev/null @@ -1,60 +0,0 @@ -server->isServerReady($this->tries)) { - throw new \RuntimeException('Server is not ready.'); - } - try { - if ($this->server->isFunctional()) { - $this->remove_unnecessary_coolify_yaml(); - if ($this->server->isSentinelEnabled()) { - $this->server->checkSentinel(); - } - } - } catch (\Throwable $e) { - // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - - return handleError($e); - } - - } - - private function remove_unnecessary_coolify_yaml() - { - // This will remote the coolify.yaml file from the server as it is not needed on cloud servers - if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $this->server, false); - } - } -} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 376cb8532..32737cc47 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,46 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 60; - public $containers; - - public $applications; - - public $databases; - - public $services; - - public $previews; - public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Server $server) {} + public function __construct(public Server $server, public ?int $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(); + } + if (! $this->percentage) { + return 'No percentage could be retrieved.'; + } + if ($this->percentage > $serverDiskUsageNotificationThreshold) { + $executed = RateLimiter::attempt( + 'high-disk-usage:'.$this->server->id, + $maxAttempts = 0, + function () use ($team, $serverDiskUsageNotificationThreshold) { + $team->notify(new HighDiskUsage($this->server, $this->percentage, $serverDiskUsageNotificationThreshold)); + }, + $decaySeconds = 3600, + ); + + if (! $executed) { + return 'Too many messages sent!'; + } + } else { + RateLimiter::hit('high-disk-usage:'.$this->server->id, 600); } } catch (\Throwable $e) { - ray($e->getMessage()); - return handleError($e); } diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index b4ef7baa0..aabeecef5 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -27,14 +27,12 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue ]); $mail->subject('Your last payment was failed for Coolify Cloud.'); $this->team->members()->each(function ($member) use ($mail) { - ray($member); if ($member->isAdmin()) { send_user_an_email($mail, $member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionInvoiceFailedJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php index 8635b439c..88a5e06be 100755 --- a/app/Jobs/SubscriptionTrialEndedJob.php +++ b/app/Jobs/SubscriptionTrialEndedJob.php @@ -30,14 +30,12 @@ class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ended email to '.$member->email); send_user_an_email($mail, $member->email); send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php index 244624749..2a76a1097 100755 --- a/app/Jobs/SubscriptionTrialEndsSoonJob.php +++ b/app/Jobs/SubscriptionTrialEndsSoonJob.php @@ -30,14 +30,12 @@ class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ending email to '.$member->email); send_user_an_email($mail, $member->email); send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php index c7cd1bcde..6c3ab83d8 100644 --- a/app/Listeners/MaintenanceModeDisabledNotification.php +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -13,7 +13,6 @@ class MaintenanceModeDisabledNotification public function handle(EventsMaintenanceModeDisabled $event): void { - ray('Maintenance mode disabled!'); $files = Storage::disk('webhooks-during-maintenance')->files(); $files = collect($files); $files = $files->sort(); @@ -41,7 +40,6 @@ class MaintenanceModeDisabledNotification $instance = new $class; $instance->$method($request); } catch (\Throwable $th) { - ray($th); } finally { Storage::disk('webhooks-during-maintenance')->delete($file); } diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php index b2cd8c738..5aab248ea 100644 --- a/app/Listeners/MaintenanceModeEnabledNotification.php +++ b/app/Listeners/MaintenanceModeEnabledNotification.php @@ -17,8 +17,5 @@ class MaintenanceModeEnabledNotification /** * Handle the event. */ - public function handle(EventsMaintenanceModeEnabled $event): void - { - ray('Maintenance mode enabled!'); - } + public function handle(EventsMaintenanceModeEnabled $event): void {} } diff --git a/app/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 52d4674ee..c9c3092b3 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -73,8 +73,6 @@ class Index extends Component } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); - $this->remoteServerPort = $this->remoteServerPort; - $this->remoteServerUser = $this->remoteServerUser; if (isDev()) { $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -87,26 +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/Form.php b/app/Livewire/Destination/Form.php index 87ae83931..a1a0a7b94 100644 --- a/app/Livewire/Destination/Form.php +++ b/app/Livewire/Destination/Form.php @@ -29,7 +29,7 @@ class Form extends Component public function delete() { try { - if ($this->destination->getMorphClass() === 'App\Models\StandaloneDocker') { + if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) { if ($this->destination->attachedTo()) { return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); } 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..61a2a20e9 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -4,6 +4,7 @@ namespace App\Livewire; use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Component; class ForcePasswordReset extends Component @@ -16,14 +17,19 @@ class ForcePasswordReset extends Component public string $password_confirmation; - protected $rules = [ - 'email' => 'required|email', - 'password' => 'required|min:8', - 'password_confirmation' => 'required|same:password', - ]; + public function rules(): array + { + return [ + 'email' => ['required', 'email'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]; + } public function mount() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } $this->email = auth()->user()->email; } @@ -34,6 +40,10 @@ class ForcePasswordReset extends Component public function submit() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } + try { $this->rateLimit(10); $this->validate(); diff --git a/app/Livewire/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/NewActivityMonitor.php b/app/Livewire/NewActivityMonitor.php index 10dbb9ce7..a9334e710 100644 --- a/app/Livewire/NewActivityMonitor.php +++ b/app/Livewire/NewActivityMonitor.php @@ -68,7 +68,6 @@ class NewActivityMonitor extends Component } else { $this->dispatch($this->eventToDispatch); } - ray('Dispatched event: '.$this->eventToDispatch.' with data: '.$this->eventData); } } } diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 65c202b7d..01ba8ffe1 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 = [ @@ -34,7 +35,6 @@ class Discord extends Component try { $this->submit(); } catch (\Throwable $e) { - ray($e->getMessage()); $this->team->discord_enabled = false; $this->validate(); } 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..09a0f1c3c 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 = [ @@ -41,7 +42,6 @@ class Telegram extends Component try { $this->submit(); } catch (\Throwable $e) { - ray($e->getMessage()); $this->team->telegram_enabled = false; $this->validate(); } diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 3be1b05ce..483f68cf8 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -3,6 +3,7 @@ namespace App\Livewire\Profile; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Validate; use Livewire\Component; @@ -48,9 +49,8 @@ class Index extends Component { try { $this->validate([ - 'current_password' => 'required', - 'new_password' => 'required|min:8', - 'new_password_confirmation' => 'required|min:8|same:new_password', + 'current_password' => ['required'], + 'new_password' => ['required', Password::defaults(), 'confirmed'], ]); if (! Hash::check($this->current_password, auth()->user()->password)) { $this->dispatch('error', 'Current password is incorrect.'); diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 5fccce792..6a6fa2482 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -46,8 +46,6 @@ class DeploymentNavbar extends Component try { force_start_deployment($this->application_deployment_queue); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } } @@ -81,8 +79,6 @@ class DeploymentNavbar extends Component } instant_remote_process([$kill_command], $server); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } finally { $this->application_deployment_queue->update([ diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 787bf879c..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,17 +241,6 @@ class General extends Component } } - 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->resetDefaultLabels(); - } - public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -286,20 +274,24 @@ class General extends Component } } - public function resetDefaultLabels() + public function resetDefaultLabels($manualReset = false) { - if ($this->application->settings->is_container_label_readonly_enabled) { - return; + try { + if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { + return; + } + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); + $this->ports_exposes = $this->application->ports_exposes; + $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->ports_exposes = $this->application->ports_exposes; - $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; - $this->application->custom_labels = base64_encode($this->customLabels); - $this->application->save(); - if ($this->application->build_pack === 'dockercompose') { - $this->loadComposeFile(); - } - $this->dispatch('configurationChanged'); } public function checkFqdns($showToaster = true) @@ -338,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(); @@ -407,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); @@ -423,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/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index d9a4b623d..9ff2f48d5 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -24,10 +24,10 @@ class Index extends Component } // No backups if ( - $database->getMorphClass() === 'App\Models\StandaloneRedis' || - $database->getMorphClass() === 'App\Models\StandaloneKeydb' || - $database->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $database->getMorphClass() === 'App\Models\StandaloneClickhouse' + $database->getMorphClass() === \App\Models\StandaloneRedis::class || + $database->getMorphClass() === \App\Models\StandaloneKeydb::class || + $database->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $database->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 7e2e4a12b..8d838ed3b 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 { @@ -74,7 +77,7 @@ class BackupEdit extends Component $this->backup->delete(); - if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $previousUrl = url()->previous(); $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); @@ -135,7 +138,7 @@ class BackupEdit extends Component $backupFolder = null; foreach ($executions as $execution) { - if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $server = $this->backup->database->service->destination->server; } else { $server = $this->backup->database->destination->server; diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index c8c33a022..45c7c30e2 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,10 +2,10 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; -use Livewire\Attributes\On; use Livewire\Component; class BackupExecutions extends Component @@ -28,7 +28,6 @@ class BackupExecutions extends Component return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - 'deleteBackup', ]; } @@ -41,13 +40,14 @@ class BackupExecutions extends Component } } - #[On('deleteBackup')] public function deleteBackup($executionId, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $execution = $this->backup->executions()->where('id', $executionId)->first(); @@ -57,7 +57,7 @@ class BackupExecutions extends Component return; } - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); } else { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 5ed74a6c3..52bced44f 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -66,7 +66,7 @@ class CreateScheduledBackup extends Component } $databaseBackup = ScheduledDatabaseBackup::create($payload); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->dispatch('refreshScheduledBackups', $databaseBackup->id); } else { $this->dispatch('refreshScheduledBackups'); diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index dfaa4461b..cd9818b41 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -77,10 +77,10 @@ class Import extends Component } if ( - $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() == \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() == \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() == \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() == \App\Models\StandaloneClickhouse::class ) { $this->unsupported = true; } @@ -108,19 +108,19 @@ class Import extends Component $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; switch ($this->resource->getMorphClass()) { - case 'App\Models\StandaloneMariadb': + case \App\Models\StandaloneMariadb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMysql': + case \App\Models\StandaloneMysql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandalonePostgresql': + case \App\Models\StandalonePostgresql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMongodb': + case \App\Models\StandaloneMongodb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 72fd95de8..25a96b292 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -11,12 +11,21 @@ use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; + protected $listeners = [ + 'envsUpdated' => 'refresh', + 'refresh', + ]; public Server $server; public StandaloneRedis $database; + public string $redis_username; + + public string $redis_password; + + public string $redis_version; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -25,33 +34,33 @@ class General extends Component 'database.name' => 'required', 'database.description' => 'nullable', 'database.redis_conf' => 'nullable', - 'database.redis_password' => 'required', 'database.image' => 'required', 'database.ports_mappings' => 'nullable', 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', 'database.custom_docker_run_options' => 'nullable', + 'redis_username' => 'required', + 'redis_password' => 'required', ]; protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', 'database.redis_conf' => 'Redis Configuration', - 'database.redis_password' => 'Redis Password', 'database.image' => 'Image', 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', 'database.custom_docker_run_options' => 'Custom Docker Options', + 'redis_username' => 'Redis Username', + 'redis_password' => 'Redis Password', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - + $this->refreshView(); } public function instantSaveAdvanced() @@ -75,13 +84,24 @@ class General extends Component { try { $this->validate(); - if ($this->database->redis_conf === '') { - $this->database->redis_conf = null; + + if (version_compare($this->redis_version, '6.0', '>=')) { + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_USERNAME'], + ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] + ); } + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_PASSWORD'], + ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] + ); + $this->database->save(); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); + } finally { + $this->dispatch('refreshEnvs'); } } @@ -119,10 +139,25 @@ class General extends Component public function refresh(): void { $this->database->refresh(); + $this->refreshView(); + } + + private function refreshView() + { + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + $this->redis_version = $this->database->getRedisVersion(); + $this->redis_username = $this->database->redis_username; + $this->redis_password = $this->database->redis_password; } public function render() { return view('livewire.project.database.redis.general'); } + + public function isSharedVariable($name) + { + return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists(); + } } diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index 8021e25d3..412240bd4 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -29,7 +29,7 @@ class ScheduledBackups extends Component $this->setSelectedBackup($this->selectedBackupId, true); } $this->parameters = get_route_parameters(); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->type = 'service-database'; } else { $this->type = 'database'; diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index e01741770..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/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index d3f5b5261..417fb2ea0 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -46,7 +46,6 @@ class DockerImage extends Component $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - ray($image, $tag); $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index fbeb5601f..2f4f5a25c 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -153,7 +153,6 @@ class GithubPrivateRepository extends Component protected function loadBranchByPage() { - ray('Loading page '.$this->page); $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}"); $json = $response->json(); if ($response->status() !== 200) { diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 971d4700b..55eb5c77d 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -213,7 +213,7 @@ class PublicGitRepository extends Component return; } - if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') { + if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) { ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); $this->branchFound = true; @@ -317,6 +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/Database.php b/app/Livewire/Project/Service/Database.php index 9804fb5ba..f95075de6 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -96,7 +96,6 @@ class Database extends Component updateCompose($this->database); $this->dispatch('success', 'Database saved.'); } catch (\Throwable $e) { - ray($e); } finally { $this->dispatch('generateDockerCompose'); } diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 70e8006c7..e89aeda85 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Models\ServiceApplication; use Livewire\Component; +use Spatie\Url\Url; class EditDomain extends Component { @@ -21,20 +22,21 @@ class EditDomain extends Component $this->application = ServiceApplication::find($this->applicationId); } - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } - public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -42,14 +44,18 @@ class EditDomain extends Component if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->application->service->parse(); $this->dispatch('refresh'); $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 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..2c5198387 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -27,7 +27,6 @@ class Navbar extends Component public function mount() { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { - ray('isConfigurationChanged init'); $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); } @@ -39,6 +38,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/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 7f2416e3e..2c751aa92 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -34,6 +34,7 @@ class StackForm extends Component $value = data_get($field, 'value'); $rules = data_get($field, 'rules', 'nullable'); $isPassword = data_get($field, 'isPassword', false); + $customHelper = data_get($field, 'customHelper', false); $this->fields->put($key, [ 'serviceName' => $serviceName, 'key' => $key, @@ -41,6 +42,7 @@ class StackForm extends Component 'value' => $value, 'isPassword' => $isPassword, 'rules' => $rules, + 'customHelper' => $customHelper, ]); $this->rules["fields.$key.value"] = $rules; 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/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 5a711259b..787d33a69 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -35,7 +35,7 @@ class All extends Component public function mount() { $this->resourceClass = get_class($this->resource); - $resourceWithPreviews = ['App\Models\Application']; + $resourceWithPreviews = [\App\Models\Application::class]; $simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile')); if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 463ceecad..e71cd9f42 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() @@ -56,7 +58,7 @@ class Show extends Component public function mount() { - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { $this->isSharedVariable = true; } $this->modalId = new Cuid2; @@ -78,7 +80,7 @@ class Show extends Component public function serialize() { data_forget($this->env, 'real_value'); - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { data_forget($this->env, 'is_build_time'); } } @@ -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..43fd97c34 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -39,12 +39,12 @@ class GetLogs extends Component public ?bool $showTimeStamps = true; - public int $numberOfLines = 100; + public ?int $numberOfLines = 100; public function mount() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->showTimeStamps = $this->resource->settings->is_include_timestamps; } else { if ($this->servicesubtype) { @@ -53,7 +53,7 @@ class GetLogs extends Component $this->showTimeStamps = $this->resource->is_include_timestamps; } } - if ($this->resource?->getMorphClass() === 'App\Models\Application') { + if ($this->resource?->getMorphClass() === \App\Models\Application::class) { if (str($this->container)->contains('-pr-')) { $this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); } @@ -69,11 +69,11 @@ class GetLogs extends Component public function instantSave() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->resource->settings->is_include_timestamps = $this->showTimeStamps; $this->resource->settings->save(); } - if ($this->resource->getMorphClass() === 'App\Models\Service') { + if ($this->resource->getMorphClass() === \App\Models\Service::class) { $serviceName = str($this->container)->beforeLast('-')->value(); $subType = $this->resource->applications()->where('name', $serviceName)->first(); if ($subType) { @@ -95,10 +95,10 @@ class GetLogs extends Component if (! $this->server->isFunctional()) { return; } - if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { + if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) { return; } - if ($this->numberOfLines <= 0) { + if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) { $this->numberOfLines = 1000; } if ($this->container) { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 5af0a6a50..e2b57aab3 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -109,7 +109,6 @@ class Logs extends Component $this->containers = $this->containers->filter(function ($container) { return str_contains($container, $this->query['pull_request_id']); }); - ray($this->containers); } 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/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index ec09eb80f..414ebba03 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -41,7 +41,7 @@ class ResourceOperations extends Component } $uuid = (string) new Cuid2; $server = $new_destination->server; - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -78,14 +78,14 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ( - $this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index 27e0c6e44..afa27a6bd 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -83,7 +83,7 @@ class Add extends Component ]); $this->file_storage_path = trim($this->file_storage_path); $this->file_storage_path = str($this->file_storage_path)->start('/')->value(); - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; } LocalFileVolume::create( 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..87b42cf3c --- /dev/null +++ b/app/Livewire/Server/Advanced.php @@ -0,0 +1,115 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable $e) { + return redirect()->route('server.show'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->server->settings->concurrent_builds = $this->concurrentBuilds; + $this->server->settings->dynamic_timeout = $this->dynamicTimeout; + $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; + $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; + $this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold; + $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold; + $this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes; + $this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks; + $this->server->settings->save(); + } else { + $this->concurrentBuilds = $this->server->settings->concurrent_builds; + $this->dynamicTimeout = $this->server->settings->dynamic_timeout; + $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup; + $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency; + $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold; + $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold; + $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes; + $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks; + } + } + + public function instantSave() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + // $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCleanup() + { + try { + DockerCleanupJob::dispatch($this->server, true); + $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + if (! validate_cron_expression($this->dockerCleanupFrequency)) { + $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency'); + throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.'); + } + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.advanced'); + } +} diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php index 0921c7fa4..d879de65d 100644 --- a/app/Livewire/Server/Charts.php +++ b/app/Livewire/Server/Charts.php @@ -19,6 +19,15 @@ class Charts extends Component public bool $poll = true; + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function pollData() { if ($this->poll || $this->interval <= 10) { @@ -34,12 +43,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..f16962bca --- /dev/null +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -0,0 +1,51 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled; + $this->server->settings->save(); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCloudflareConfig() + { + $this->isCloudflareTunnelsEnabled = true; + $this->server->settings->is_cloudflare_tunnel = true; + $this->server->settings->save(); + $this->server->refresh(); + $this->dispatch('success', 'Cloudflare Tunnels enabled.'); + } + + public function render() + { + return view('livewire.server.cloudflare-tunnels'); + } +} diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index ed2345b2a..b9e3944b5 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -2,6 +2,9 @@ namespace App\Livewire\Server; +use App\Actions\Server\DeleteServer; +use App\Models\InstanceSettings; +use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -11,14 +14,25 @@ class Delete extends Component { use AuthorizesRequests; - public $server; + public Server $server; + + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function delete($password) { - if (! 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 +42,7 @@ class Delete extends Component return; } $this->server->delete(); + DeleteServer::dispatch($this->server); return redirect()->route('server.index'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destination/Show.php index 986e16cbf..71c051a74 100644 --- a/app/Livewire/Server/Destination/Show.php +++ b/app/Livewire/Server/Destination/Show.php @@ -3,27 +3,87 @@ namespace App\Livewire\Server\Destination; use App\Models\Server; +use App\Models\StandaloneDocker; +use App\Models\SwarmDocker; +use Illuminate\Support\Collection; use Livewire\Component; class Show extends Component { - public ?Server $server = null; + public Server $server; - public $parameters = []; + public Collection $networks; - public function mount() + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->networks = collect(); + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + loggy($this->server); } catch (\Throwable $e) { return handleError($e, $this); } } + private function createNetworkAndAttachToProxy() + { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + + public function add($name) + { + if ($this->server->isSwarm()) { + $found = $this->server->swarmDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + SwarmDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $this->name, + 'server_id' => $this->server->id, + ]); + } + } else { + $found = $this->server->standaloneDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + StandaloneDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $name, + 'server_id' => $this->server->id, + ]); + } + $this->createNetworkAndAttachToProxy(); + } + } + + public function scan() + { + if ($this->server->isSwarm()) { + $alreadyAddedNetworks = $this->server->swarmDockers; + } else { + $alreadyAddedNetworks = $this->server->standaloneDockers; + } + $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); + $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { + return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; + })->filter(function ($network) use ($alreadyAddedNetworks) { + return ! $alreadyAddedNetworks->contains('network', $network['Name']); + }); + if ($this->networks->count() === 0) { + $this->dispatch('success', 'No new destinations found on this server.'); + + return; + } + $this->dispatch('success', 'Scan done.'); + } + public function render() { return view('livewire.server.destination.show'); diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index c4f25c79d..697c7b76f 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,20 @@ 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.is_sentinel_debug_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 +66,19 @@ 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.is_sentinel_debug_enabled' => 'Debug', + '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) @@ -95,17 +86,32 @@ class Form extends Component $this->server = $server; $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->wildcard_domain = $this->server->settings->wildcard_domain; - $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; - $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; - $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes; - $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) { if ($field === 'server.settings.docker_cleanup_frequency') { $frequency = $this->server->settings->docker_cleanup_frequency; - if (empty($frequency) || ! validate_cron_expression($frequency)) { + if (! validate_cron_expression($frequency)) { $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); $this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; } @@ -131,21 +137,40 @@ 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 updatedServerSettingsIsSentinelDebugEnabled() + { + $this->restartSentinel(); + } + public function instantSave() { try { + + $this->validate(); refresh_server_connection($this->server->privateKey); $this->validateServer(false); @@ -153,33 +178,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); } @@ -236,16 +267,10 @@ class Form extends Component } refresh_server_connection($this->server->privateKey); $this->server->settings->wildcard_domain = $this->wildcard_domain; - if ($this->server->settings->force_docker_cleanup) { - $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; - } else { - $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; - } $currentTimezone = $this->server->settings->getOriginal('server_timezone'); $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(); @@ -255,29 +280,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/LogDrains.php b/app/Livewire/Server/LogDrains.php index 6e09eecdd..2bc24eff8 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -2,84 +2,96 @@ namespace App\Livewire\Server; -use App\Actions\Server\InstallLogDrain; +use App\Actions\Server\StartLogDrain; use App\Actions\Server\StopLogDrain; use App\Models\Server; +use Livewire\Attributes\Rule; use Livewire\Component; class LogDrains extends Component { public Server $server; - public $parameters = []; + #[Rule(['boolean'])] + public bool $isLogDrainNewRelicEnabled = false; - protected $rules = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]; + #[Rule(['boolean'])] + public bool $isLogDrainCustomEnabled = false; - protected $validationAttributes = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', - 'server.settings.logdrain_newrelic_license_key' => 'New Relic license key', - 'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI', - 'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain', - 'server.settings.logdrain_highlight_project_id' => 'Highlight project ID', - 'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain', - 'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name', - 'server.settings.logdrain_axiom_api_key' => 'Axiom API key', - 'server.settings.is_logdrain_custom_enabled' => 'Custom log drain', - 'server.settings.logdrain_custom_config' => 'Custom log drain configuration', - 'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser', - ]; + #[Rule(['boolean'])] + public bool $isLogDrainAxiomEnabled = false; - public function mount() + #[Rule(['string', 'nullable'])] + public ?string $logDrainNewRelicLicenseKey = null; + + #[Rule(['url', 'nullable'])] + public ?string $logDrainNewRelicBaseUri = null; + + #[Rule(['string', 'nullable'])] + public ?string $logDrainAxiomDatasetName = null; + + #[Rule(['string', 'nullable'])] + public ?string $logDrainAxiomApiKey = null; + + #[Rule(['string', 'nullable'])] + public ?string $logDrainCustomConfig = null; + + #[Rule(['string', 'nullable'])] + public ?string $logDrainCustomConfigParser = null; + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($server)) { - return redirect()->route('server.index'); - } - $this->server = $server; + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function configureLogDrain() + public function syncData(bool $toModel = false) + { + + if ($toModel) { + $this->validate(); + $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; + $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; + $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; + + $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; + $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; + $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; + $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; + $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; + $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; + + $this->server->settings->save(); + } else { + $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; + $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; + $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; + + $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; + $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; + $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; + $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; + $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + } + + } + + public function instantSave() { try { - InstallLogDrain::run($this->server); - if (! $this->server->isLogDrainEnabled()) { - $this->dispatch('serverRefresh'); + $this->syncData(true); + if ($this->server->isLogDrainEnabled()) { + StartLogDrain::run($this->server); + $this->dispatch('success', 'Log drain service started.'); + } else { + StopLogDrain::run($this->server); $this->dispatch('success', 'Log drain service stopped.'); - - return; } - $this->dispatch('serverRefresh'); - $this->dispatch('success', 'Log drain service started.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave(string $type) - { - try { - $ok = $this->submit($type); - if (! $ok) { - return; - } - $this->configureLogDrain(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -88,79 +100,10 @@ class LogDrains extends Component public function submit(string $type) { try { - $this->resetErrorBag(); - if ($type === 'newrelic') { - $this->validate([ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->validate([ - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->validate([ - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->validate([ - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - ]); - } - if (! $this->server->isLogDrainEnabled()) { - StopLogDrain::dispatch($this->server); - } - $this->server->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Settings saved.'); - - return true; } catch (\Throwable $e) { - if ($type === 'newrelic') { - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->server->settings->update([ - 'is_logdrain_axiom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->server->settings->update([ - 'is_logdrain_custom_enabled' => false, - ]); - } - handleError($e, $this); - - return false; + return handleError($e, $this); } } diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index 0ad820428..64aa1884b 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -8,26 +8,63 @@ use Livewire\Component; class Show extends Component { - public ?Server $server = null; + public Server $server; public $privateKeys = []; public $parameters = []; - public function mount() + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); } catch (\Throwable $e) { return handleError($e, $this); } } + public function setPrivateKey($privateKeyId) + { + $ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId); + if (is_null($ownedPrivateKey)) { + $this->dispatch('error', 'You are not allowed to use this private key.'); + + return; + } + + $originalPrivateKeyId = $this->server->getOriginal('private_key_id'); + try { + $this->server->update(['private_key_id' => $privateKeyId]); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); + if ($uptime) { + $this->dispatch('success', 'Private key updated successfully.'); + } else { + throw new \Exception($error); + } + } catch (\Exception $e) { + $this->server->update(['private_key_id' => $originalPrivateKeyId]); + $this->server->validateConnection(); + $this->dispatch('error', $e->getMessage()); + } + } + + public function checkConnection() + { + try { + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + } else { + $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); + + return; + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.server.private-key.show'); diff --git a/app/Livewire/Server/Proxy/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..afc7682a4 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -3,41 +3,203 @@ namespace App\Livewire\Server; use App\Models\Server; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Rule; use Livewire\Component; class Show extends Component { - use AuthorizesRequests; + public Server $server; - public ?Server $server = null; + #[Rule(['required'])] + public string $name; - public $parameters = []; + #[Rule(['nullable'])] + public ?string $description; - protected $listeners = ['refreshServerShow']; + #[Rule(['required'])] + public string $ip; - public function mount() + #[Rule(['required'])] + public string $user; + + #[Rule(['required'])] + public string $port; + + #[Rule(['nullable'])] + public ?string $validationLogs = null; + + #[Rule(['nullable', 'url'])] + public ?string $wildcardDomain; + + #[Rule(['required'])] + public bool $isReachable; + + #[Rule(['required'])] + public bool $isUsable; + + #[Rule(['required'])] + public bool $isSwarmManager; + + #[Rule(['required'])] + public bool $isSwarmWorker; + + #[Rule(['required'])] + public bool $isBuildServer; + + #[Rule(['required'])] + public bool $isMetricsEnabled; + + #[Rule(['required'])] + public string $sentinelToken; + + #[Rule(['nullable'])] + public ?string $sentinelUpdatedAt; + + #[Rule(['required', 'integer', 'min:1'])] + public int $sentinelMetricsRefreshRateSeconds; + + #[Rule(['required', 'integer', 'min:1'])] + public int $sentinelMetricsHistoryDays; + + #[Rule(['required', 'integer', 'min:10'])] + public int $sentinelPushIntervalSeconds; + + #[Rule(['nullable', 'url'])] + public ?string $sentinelCustomUrl; + + #[Rule(['required'])] + public bool $isSentinelEnabled; + + #[Rule(['required'])] + public bool $isSentinelDebugEnabled; + + #[Rule(['required'])] + public string $serverTimezone; + + public array $timezones; + + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => '$refresh', + 'refreshServerShow' => '$refresh', + ]; + } + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function refreshServerShow() + public function syncData(bool $toModel = false) { - $this->server->refresh(); - $this->dispatch('$refresh'); + if ($toModel) { + $this->validate(); + $this->server->name = $this->name; + $this->server->description = $this->description; + $this->server->ip = $this->ip; + $this->server->user = $this->user; + $this->server->port = $this->port; + $this->server->validation_logs = $this->validationLogs; + $this->server->save(); + + $this->server->settings->is_swarm_manager = $this->isSwarmManager; + $this->server->settings->is_swarm_worker = $this->isSwarmWorker; + $this->server->settings->is_build_server = $this->isBuildServer; + $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; + $this->server->settings->sentinel_token = $this->sentinelToken; + $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; + $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; + $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; + $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; + $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; + $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; + $this->server->settings->server_timezone = $this->serverTimezone; + $this->server->settings->save(); + } else { + $this->name = $this->server->name; + $this->description = $this->server->description; + $this->ip = $this->server->ip; + $this->user = $this->server->user; + $this->port = $this->server->port; + $this->wildcardDomain = $this->server->settings->wildcard_domain; + $this->isReachable = $this->server->settings->is_reachable; + $this->isUsable = $this->server->settings->is_usable; + $this->isSwarmManager = $this->server->settings->is_swarm_manager; + $this->isSwarmWorker = $this->server->settings->is_swarm_worker; + $this->isBuildServer = $this->server->settings->is_build_server; + $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; + $this->sentinelToken = $this->server->settings->sentinel_token; + $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; + $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; + $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled; + $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; + $this->sentinelUpdatedAt = $this->server->settings->updated_at; + $this->serverTimezone = $this->server->settings->server_timezone; + } + } + + public function validateServer($install = true) + { + try { + $this->validationLogs = $this->server->validation_logs = null; + $this->server->save(); + $this->dispatch('init', $install); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function checkLocalhostConnection() + { + $this->syncData(true); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + $this->server->settings->is_reachable = $this->isReachable = true; + $this->server->settings->is_usable = $this->isUsable = true; + $this->server->settings->save(); + $this->dispatch('proxyStatusUpdated'); + } else { + $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); + + return; + } + } + + public function regenerateSentinelToken() + { + try { + $this->server->settings->generateSentinelToken(); + $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + $this->submit(); } public function submit() { - $this->dispatch('serverRefresh', false); + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php deleted file mode 100644 index 92869c44b..000000000 --- a/app/Livewire/Server/ShowPrivateKey.php +++ /dev/null @@ -1,50 +0,0 @@ -server->update(['private_key_id' => $privateKey->id]); - $this->server->refresh(); - $this->dispatch('success', 'Private key updated successfully.'); - } catch (\Exception $e) { - $this->dispatch('error', 'Failed to update private key: '.$e->getMessage()); - } - } - - public function checkConnection() - { - try { - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - } else { - ray($error); - $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); - - return; - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - 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..79f8269f3 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(); } @@ -47,7 +50,6 @@ class License extends Component $this->dispatch('reloadWindow'); } catch (\Throwable $e) { session()->flash('error', 'Something went wrong. Please contact support.
Error: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('settings.license'); } diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 9240aa96d..38f7e548a 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); @@ -78,7 +84,7 @@ class SettingsBackup extends Component 'postgres_password' => $postgres_password, 'postgres_db' => $postgres_db, 'status' => 'running', - 'destination_type' => 'App\Models\StandaloneDocker', + 'destination_type' => \App\Models\StandaloneDocker::class, 'destination_id' => 0, ]); $this->backup = ScheduledDatabaseBackup::create([ @@ -87,7 +93,7 @@ class SettingsBackup extends Component 'save_s3' => false, 'frequency' => '0 0 * * *', 'database_id' => $this->database->id, - 'database_type' => 'App\Models\StandalonePostgresql', + 'database_type' => \App\Models\StandalonePostgresql::class, 'team_id' => currentTeam()->id, ]); $this->database->refresh(); @@ -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..c9dabcb5c 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; @@ -58,29 +59,27 @@ class AdminView extends Component foreach ($servers as $server) { $resources = $server->definedResources(); foreach ($resources as $resource) { - ray('Deleting resource: '.$resource->name); $resource->forceDelete(); } - ray('Deleting server: '.$server->name); $server->forceDelete(); } $projects = $team->projects; foreach ($projects as $project) { - ray('Deleting project: '.$project->name); $project->forceDelete(); } $team->members()->detach($user->id); - ray('Deleting team: '.$team->name); $team->delete(); } public function delete($id, $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'); @@ -88,29 +87,23 @@ class AdminView extends Component $user = User::find($id); $teams = $user->teams; foreach ($teams as $team) { - ray($team->name); $user_alone_in_team = $team->members->count() === 1; if ($team->id === 0) { if ($user_alone_in_team) { - ray('user is alone in the root team, do nothing'); - return $this->dispatch('error', 'User is alone in the root team, cannot delete'); } } if ($user_alone_in_team) { - ray('user is alone in the team'); $this->finalizeDeletion($user, $team); continue; } - ray('user is not alone in the team'); if ($user->isOwner()) { $found_other_owner_or_admin = $team->members->filter(function ($member) { return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; })->where('id', '!=', $user->id)->first(); if ($found_other_owner_or_admin) { - ray('found other owner or admin'); $team->members()->detach($user->id); continue; @@ -119,24 +112,19 @@ class AdminView extends Component return $member->pivot->role === 'member'; })->first(); if ($found_other_member_who_is_not_owner) { - ray('found other member who is not owner'); $found_other_member_who_is_not_owner->pivot->role = 'owner'; $found_other_member_who_is_not_owner->pivot->save(); $team->members()->detach($user->id); } else { - // This should never happen as if the user is the only member in the team, the team should be deleted already. - ray('found no other member who is not owner'); $this->finalizeDeletion($user, $team); } continue; } } else { - ray('user is not owner'); $team->members()->detach($user->id); } } - ray('Deleting user: '.$user->name); $user->delete(); $this->getUsers(); } diff --git a/app/Livewire/Team/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..890d640a0 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -2,6 +2,7 @@ namespace App\Livewire\Team; +use App\Enums\Role; use App\Models\User; use Illuminate\Support\Facades\Cache; use Livewire\Component; @@ -12,29 +13,66 @@ class Member extends Component public function makeAdmin() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeOwner() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::OWNER) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeReadonly() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function remove() { - $this->member->teams()->detach(currentTeam()); - Cache::forget("team:{$this->member->id}"); - Cache::remember('team:'.$this->member->id, 3600, function () { - return $this->member->teams()->first(); - }); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->detach(currentTeam()); + Cache::forget("team:{$this->member->id}"); + Cache::remember('team:'.$this->member->id, 3600, function () { + return $this->member->teams()->first(); + }); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } + } + + private function getMemberRole() + { + return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role; } } diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php index d1f79c835..9d1fdab98 100644 --- a/app/Livewire/VerifyEmail.php +++ b/app/Livewire/VerifyEmail.php @@ -17,8 +17,6 @@ class VerifyEmail extends Component $this->dispatch('success', 'Email verification link sent!'); } catch (\Exception $e) { - ray($e); - return handleError($e, $this); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 07aeb4c5b..c747c75c5 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -221,7 +221,6 @@ class Application extends BaseModel { if ($this->build_pack === 'dockercompose') { $server = data_get($this, 'destination.server'); - ray('Deleting volumes'); instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false); } else { if ($persistentStorages->count() === 0) { @@ -937,7 +936,7 @@ class Application extends BaseModel $source_html_url_host = $url['host']; $source_html_url_scheme = $url['scheme']; - if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->getMorphClass() == \App\Models\GithubApp::class) { if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; @@ -1246,13 +1245,11 @@ class Application extends BaseModel return; } if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) { - ray('custom_labels is not base64 encoded'); $this->custom_labels = str($customLabels)->replace(',', "\n"); $this->custom_labels = base64_encode($customLabels); } $customLabels = base64_decode($this->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - ray('custom_labels contains non-ascii characters'); $customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n"); } $this->custom_labels = base64_encode($customLabels); @@ -1400,13 +1397,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 +1412,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,9 +1475,9 @@ class Application extends BaseModel return $config; } - public function setConfig($config) { - $config = $config; + public function setConfig($config) + { $validator = Validator::make(['config' => $config], [ 'config' => 'required|json', ]); diff --git a/app/Models/Environment.php b/app/Models/Environment.php index c892d7ba1..d0bb5d2a6 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -27,7 +27,6 @@ class Environment extends Model static::deleting(function ($environment) { $shared_variables = $environment->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting environment shared variable: '.$shared_variable->name); $shared_variable->delete(); } 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..0b0e93b12 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(); @@ -60,7 +65,7 @@ class GithubApp extends BaseModel { return Attribute::make( get: function () { - if ($this->getMorphClass() === 'App\Models\GithubApp') { + if ($this->getMorphClass() === \App\Models\GithubApp::class) { return 'github'; } }, diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index a789a7e65..2112a4a66 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -9,6 +9,11 @@ class GitlabApp extends BaseModel 'app_secret', ]; + public static function ownedByCurrentTeam() + { + return GitlabApp::whereTeamId(currentTeam()->id); + } + public function applications() { return $this->morphMany(Application::class, 'source'); diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 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/Project.php b/app/Models/Project.php index 5a9dd964a..3a09b0b8f 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -47,7 +47,6 @@ class Project extends BaseModel $project->settings()->delete(); $shared_variables = $project->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting project shared variable: '.$shared_variable->name); $shared_variable->delete(); } }); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index ce5d3a87f..473fc7b4b 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -39,9 +39,14 @@ class ScheduledDatabaseBackup extends BaseModel public function server() { if ($this->database) { - if ($this->database->destination && $this->database->destination->server) { - $server = $this->database->destination->server; - + if ($this->database instanceof ServiceDatabase) { + $destination = data_get($this->database->service, 'destination'); + $server = data_get($destination, 'server'); + } else { + $destination = data_get($this->database, 'destination'); + $server = data_get($destination, 'server'); + } + if ($server) { return $server; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 8864deef1..a9e3acde4 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,10 +3,16 @@ namespace App\Models; use App\Actions\Server\InstallDocker; +use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; -use App\Jobs\PullSentinelImageJob; +use App\Helpers\SshMultiplexingHelper; +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 +49,7 @@ use Symfony\Component\Yaml\Yaml; class Server extends BaseModel { - use SchemalessAttributesTrait; + use SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -59,6 +65,11 @@ class Server extends BaseModel } $server->forceFill($payload); }); + static::saved(function ($server) { + if ($server->privateKey->isDirty()) { + refresh_server_connection($server->privateKey); + } + }); static::created(function ($server) { ServerSetting::create([ 'server_id' => $server->id, @@ -95,7 +106,8 @@ class Server extends BaseModel } } }); - static::deleting(function ($server) { + + static::forceDeleting(function ($server) { $server->destinations()->each(function ($destination) { $destination->delete(); }); @@ -103,12 +115,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 +142,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 +229,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 +530,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 +581,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 +602,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 +615,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 +624,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 +916,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 +981,115 @@ $schema://$host { return data_get($this, 'settings.is_swarm_worker'); } - public function validateConnection($isManualCheck = true) + 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; + // If the server is reachable, send the reachable notification if it was sent before + if ($isReachable === true) { + if ($unreachableNotificationSent === true) { + $this->sendReachableNotification(); + } + } else { + // If the server is unreachable, send the unreachable notification if it was not sent before + if ($unreachableNotificationSent === false) { + $this->sendUnreachableNotification(); + } + } + } + + public function sendReachableNotification() + { + $this->unreachable_notification_sent = false; + $this->save(); + $this->refresh(); + $this->team->notify(new Reachable($this)); + } + + public function sendUnreachableNotification() + { + $this->unreachable_notification_sent = true; + $this->save(); + $this->refresh(); + $this->team->notify(new Unreachable($this)); + } + + public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false) { config()->set('constants.ssh.mux_enabled', ! $isManualCheck); - // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false')); - $server = Server::find($this->id); - if (! $server) { - return ['uptime' => false, 'error' => 'Server not found.']; - } - if ($server->skipServer()) { + SshMultiplexingHelper::removeMuxFile($this); + + 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 ($justCheckingNewKey) { + return ['uptime' => false, 'error' => 'This key is not valid for this server.']; + } + if ($this->settings->is_reachable === true) { + $this->settings->is_reachable = false; + $this->settings->save(); + } return ['uptime' => false, 'error' => $e->getMessage()]; } @@ -1225,4 +1244,27 @@ $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; + } + + public function restartContainer(string $containerName) + { + return instant_remote_process(['docker restart '.$containerName], $this, false); + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index c44a393b4..bca16536e 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,77 @@ 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; + 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 bcdb74f8c..c4f4c2d3c 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -288,6 +288,21 @@ class Service extends BaseModel continue; } switch ($image) { + case $image->contains('castopod'): + $data = collect([]); + $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first(); + if ($disable_https) { + $data = $data->merge([ + 'Disable HTTPS' => [ + 'key' => 'CP_DISABLE_HTTPS', + 'value' => data_get($disable_https, 'value'), + 'rules' => 'required', + 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS', + ], + ]); + } + $fields->put('Castopod', $data->toArray()); + break; case $image->contains('label-studio'): $data = collect([]); $username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first(); @@ -304,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, @@ -344,18 +359,17 @@ 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', ], ]); } $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first(); - ray('password', $password); 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, @@ -369,7 +383,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', ], @@ -377,7 +391,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, @@ -472,7 +486,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, @@ -519,7 +533,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, @@ -542,7 +556,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, @@ -904,7 +918,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', @@ -914,7 +928,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, @@ -924,7 +938,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', ], @@ -982,8 +996,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']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT']; + $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(); @@ -1216,7 +1230,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"); } @@ -1300,4 +1313,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..3dd13b633 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -71,7 +71,6 @@ class StandalonePostgresql extends BaseModel } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - ray('Deleting volume: '.$storage->name); instant_remote_process(["docker volume rm -f $storage->name"], $server, false); } } @@ -268,37 +267,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..49b019725 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.'], @@ -90,27 +93,22 @@ class Team extends Model implements SendsDiscord, SendsEmail static::deleting(function ($team) { $keys = $team->privateKeys; foreach ($keys as $key) { - ray('Deleting key: '.$key->name); $key->delete(); } $sources = $team->sources(); foreach ($sources as $source) { - ray('Deleting source: '.$source->name); $source->delete(); } $tags = Tag::whereTeamId($team->id)->get(); foreach ($tags as $tag) { - ray('Deleting tag: '.$tag->name); $tag->delete(); } $shared_variables = $team->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting team shared variable: '.$shared_variable->name); $shared_variable->delete(); } $s3s = $team->s3s; foreach ($s3s as $s3) { - ray('Deleting s3: '.$s3->name); $s3->delete(); } }); @@ -164,8 +162,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..946a622ca 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; @@ -51,7 +52,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue $channels = setNotificationChannels($notifiable, 'deployments'); if (isCloud()) { // TODO: Make batch notifications work with email - $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']); + $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]); } return $channels; @@ -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/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 413d3de53..af9af978d 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -32,7 +32,6 @@ class EmailChannel if ($error === 'No email settings found.') { throw $e; } - ray($e->getMessage()); $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; if (isset($recipients)) { $message .= implode(', ', $recipients); diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index b1a607651..4b1fa49dd 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -18,23 +18,23 @@ class TelegramChannel $topicsInstance = get_class($notification); switch ($topicsInstance) { - case 'App\Notifications\Test': + case \App\Notifications\Test::class: $topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id'); break; - case 'App\Notifications\Application\StatusChanged': - case 'App\Notifications\Container\ContainerRestarted': - case 'App\Notifications\Container\ContainerStopped': + case \App\Notifications\Application\StatusChanged::class: + case \App\Notifications\Container\ContainerRestarted::class: + case \App\Notifications\Container\ContainerStopped::class: $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); break; - case 'App\Notifications\Application\DeploymentSuccess': - case 'App\Notifications\Application\DeploymentFailed': + case \App\Notifications\Application\DeploymentSuccess::class: + case \App\Notifications\Application\DeploymentFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id'); break; - case 'App\Notifications\Database\BackupSuccess': - case 'App\Notifications\Database\BackupFailed': + case \App\Notifications\Database\BackupSuccess::class: + case \App\Notifications\Database\BackupFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id'); break; - case 'App\Notifications\ScheduledTask\TaskFailed': + case \App\Notifications\ScheduledTask\TaskFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); break; } 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..5128c8ed6 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -44,15 +45,22 @@ class BackupSuccess extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; + $message = new DiscordMessage( + title: ':white_check_mark: Database backup successful', + description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.", + color: DiscordMessage::successColor(), + ); + + $message->addField('Frequency', $this->frequency, true); + + return $message; } public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; - ray($message); return [ 'message' => $message, diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php deleted file mode 100644 index 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..a43b1e153 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -2,10 +2,12 @@ namespace App\Notifications; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Queue\Middleware\RateLimited; class Test extends Notification implements ShouldQueue { @@ -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::class => [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/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8b4c2eef2..1fffd0399 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; +use Illuminate\Validation\Rules\Password; use Laravel\Sanctum\Sanctum; class AppServiceProvider extends ServiceProvider @@ -15,6 +16,14 @@ class AppServiceProvider extends ServiceProvider { Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); + Password::defaults(function () { + $rule = Password::min(8); + + return $this->app->isProduction() + ? $rule->mixedCase()->letters()->numbers()->symbols() + : $rule; + }); + Http::macro('github', function (string $api_url, ?string $github_access_token = null) { if ($github_access_token) { return Http::withHeaders([ diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php new file mode 100644 index 000000000..07e0e8709 --- /dev/null +++ b/app/Providers/DuskServiceProvider.php @@ -0,0 +1,21 @@ +visit('/login') + ->type('email', 'test@example.com') + ->type('password', 'password') + ->press('Login'); + }); + } +} diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php deleted file mode 100644 index f968b6d0c..000000000 --- a/app/View/Components/Server/Sidebar.php +++ /dev/null @@ -1,27 +0,0 @@ - 'string', 'build_pack' => Rule::enum(BuildPackTypes::class), 'is_static' => 'boolean', + 'static_image' => Rule::enum(StaticImageTypes::class), 'domains' => 'string', 'redirect' => Rule::enum(RedirectTypes::class), 'git_commit_sha' => 'string', @@ -176,4 +178,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('github_app_uuid'); $request->offsetUnset('private_key_uuid'); $request->offsetUnset('use_build_server'); + $request->offsetUnset('is_static'); } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index d8dc26a48..303fcab8e 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -40,6 +40,7 @@ const DATABASE_DOCKER_IMAGES = [ ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', + 'minio/minio', 'svhd/logto', ]; 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/docker.php b/bootstrap/helpers/docker.php index 397bce029..2795ae295 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -207,12 +207,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica } function generateServiceSpecificFqdns(ServiceApplication|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\ServiceApplication') { + if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'service.server'); $environment_variables = data_get($resource, 'service.environment_variables'); $type = $resource->serviceType(); - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'destination.server'); $environment_variables = data_get($resource, 'environment_variables'); @@ -279,7 +279,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_ingress_network={$network}"); } foreach ($domains as $loop => $domain) { - $loop = $loop; $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); @@ -335,10 +334,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { return explode(',', $matches[1]); } + return null; })->flatten() - ->filter() - ->unique(); + ->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { @@ -388,7 +388,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($path !== '/') { // Middleware handling $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } @@ -402,7 +402,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -417,7 +417,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_gzip_enabled) { $middlewares->push('gzip'); - } + } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 97deb0b1c..0b5f7034b 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m if (is_null($source)) { throw new \Exception('Not implemented yet.'); } - if ($source->getMorphClass() == 'App\Models\GithubApp') { + if ($source->getMorphClass() == \App\Models\GithubApp::class) { if ($source->is_public) { $response = Http::github($source->api_url)->$method($endpoint); } else { 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/services.php b/bootstrap/helpers/services.php index eba88d000..94c3c5f45 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -24,7 +24,7 @@ function replaceVariables(string $variable): Stringable function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false) { try { - if ($oneService->getMorphClass() === 'App\Models\Application') { + if ($oneService->getMorphClass() === \App\Models\Application::class) { $workdir = $oneService->workdir(); $server = $oneService->destination->server; } else { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 487a6f107..89462215c 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,6 @@ function refreshSession(?Team $team = null): void } function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { - ray($error); if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); @@ -142,6 +142,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 +168,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'; } } @@ -368,6 +369,9 @@ function translate_cron_expression($expression_to_validate): string } function validate_cron_expression($expression_to_validate): bool { + if (empty($expression_to_validate)) { + return false; + } $isValid = false; $expression = new CronExpression($expression_to_validate); $isValid = $expression->isValid(); @@ -643,7 +647,7 @@ function queryResourcesByUuid(string $uuid) return $resource; } -function generatTagDeployWebhook($tag_name) +function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); $api = Url::fromString($baseUrl).'/api/v1'; @@ -667,7 +671,7 @@ function generateGitManualWebhook($resource, $type) if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { return null; } - if ($resource->getMorphClass() === 'App\Models\Application') { + if ($resource->getMorphClass() === \App\Models\Application::class) { $baseUrl = base_url(); $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; @@ -683,7 +687,7 @@ function removeAnsiColors($text) function getTopLevelNetworks(Service|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -738,7 +742,7 @@ function getTopLevelNetworks(Service|Application $resource) return $topLevelNetworks->keys(); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { @@ -1145,7 +1149,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { - if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') { + if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') { $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); $domains = collect($domains); } else { @@ -1174,10 +1178,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); } } } @@ -1193,10 +1197,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null if ($domains->contains($naked_domain)) { if (data_get($resource, 'uuid')) { if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); } } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); } } } @@ -1338,13 +1342,6 @@ function isAnyDeploymentInprogress() exit(0); } -function generateSentinelToken() -{ - $token = Str::random(64); - - return $token; -} - function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; @@ -1416,7 +1413,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull if ($source->value() === '/tmp' || $source->value() === '/tmp/') { return $volume; } - if (get_class($resource) === "App\Models\Application") { + if (get_class($resource) === \App\Models\Application::class) { $dir = base_configuration_dir().'/applications/'.$resource->uuid; } else { $dir = base_configuration_dir().'/services/'.$resource->service->uuid; @@ -1456,7 +1453,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull } } $slugWithoutUuid = Str::slug($source, '-'); - if (get_class($resource) === "App\Models\Application") { + if (get_class($resource) === \App\Models\Application::class) { $name = "{$resource->uuid}_{$slugWithoutUuid}"; } else { $name = "{$resource->service->uuid}_{$slugWithoutUuid}"; @@ -1499,7 +1496,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -2213,7 +2210,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { return collect([]); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { @@ -3099,7 +3096,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } - if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) { + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { $fqdn = "$fqdn$path"; @@ -3181,6 +3178,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } elseif ($isService) { $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); } + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ 'key' => $key->value(), $nameOfId => $resource->id, @@ -3568,6 +3566,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(':'); @@ -3582,11 +3581,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 @@ -3597,6 +3598,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; @@ -3610,6 +3612,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'value' => $value, 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); } @@ -3786,7 +3789,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int service_name: $serviceName, image: $image, predefinedPort: $predefinedPort - )); } } @@ -3895,6 +3897,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 { @@ -3982,13 +3986,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"]); @@ -4006,6 +4011,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 2fed52ef0..e8b6ab098 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,8 @@ 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 1c030534b..0e83ff40e 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +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_10_081444_remove_constraint_from_service_applications_fqdn.php b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php new file mode 100644 index 000000000..feb144de6 --- /dev/null +++ b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php @@ -0,0 +1,34 @@ +dropUnique(['fqdn']); + }); + Schema::table('applications', function (Blueprint $table) { + $table->dropUnique(['fqdn']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + Schema::table('applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + } +}; 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/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php new file mode 100644 index 000000000..d8ab1313b --- /dev/null +++ b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php @@ -0,0 +1,28 @@ +boolean('is_sentinel_debug_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_sentinel_debug_enabled'); + }); + } +}; 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/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php index e2543ee02..d528179c0 100644 --- a/database/seeders/PopulateSshKeysDirectorySeeder.php +++ b/database/seeders/PopulateSshKeysDirectorySeeder.php @@ -33,7 +33,6 @@ class PopulateSshKeysDirectorySeeder extends Seeder } } catch (\Throwable $e) { echo "Error: {$e->getMessage()}\n"; - ray($e->getMessage()); } } } 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/audiobookshelf.svg b/public/svgs/audiobookshelf.svg new file mode 100644 index 000000000..d641b765b --- /dev/null +++ b/public/svgs/audiobookshelf.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/azimutt.png b/public/svgs/azimutt.png new file mode 100644 index 000000000..ef69062cd Binary files /dev/null and b/public/svgs/azimutt.png differ diff --git a/public/svgs/bookstack.png b/public/svgs/bookstack.png new file mode 100644 index 000000000..d10b3ca43 Binary files /dev/null and b/public/svgs/bookstack.png differ 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/castopod.svg b/public/svgs/castopod.svg new file mode 100644 index 000000000..c73008400 --- /dev/null +++ b/public/svgs/castopod.svg @@ -0,0 +1,5 @@ + + + + + \ 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/forgejo.svg b/public/svgs/forgejo.svg new file mode 100644 index 000000000..804b05e28 --- /dev/null +++ b/public/svgs/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/joplin.png b/public/svgs/joplin.png new file mode 100644 index 000000000..d17a1d2c1 Binary files /dev/null and b/public/svgs/joplin.png differ diff --git a/public/svgs/keycloak.svg b/public/svgs/keycloak.svg new file mode 100644 index 000000000..849ac2759 --- /dev/null +++ b/public/svgs/keycloak.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/mattermost.svg b/public/svgs/mattermost.svg new file mode 100644 index 000000000..b01d38eb7 --- /dev/null +++ b/public/svgs/mattermost.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/svgs/mautic.svg b/public/svgs/mautic.svg new file mode 100644 index 000000000..b528f72ef --- /dev/null +++ b/public/svgs/mautic.svg @@ -0,0 +1,17 @@ + + + + + + + + + + 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/onedev.svg b/public/svgs/onedev.svg new file mode 100644 index 000000000..fb9c9c060 --- /dev/null +++ b/public/svgs/onedev.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + 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/paperless.svg b/public/svgs/paperless.svg new file mode 100644 index 000000000..347b1e759 --- /dev/null +++ b/public/svgs/paperless.svg @@ -0,0 +1,12 @@ + + + + + + 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/js/terminal.js b/resources/js/terminal.js index 4cdab0e07..59c9a79a8 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -184,10 +184,6 @@ export function initializeTerminalComponent() { // Copy and paste functionality this.term.attachCustomKeyEventHandler((arg) => { if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") { - navigator.clipboard.readText() - .then(text => { - this.socket.send(JSON.stringify({ message: text })); - }); return false; } diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 7d615885f..d6c3edf84 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -5,6 +5,13 @@ Coolify
+ @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif
@csrf @@ -27,28 +34,22 @@ {{ __('auth.forgot_password') }}? @endenv - {{ __('auth.login') }} + {{ __('auth.login') }} + + @if (session('error')) +
+ {{ session('error') }} +
+ @endif @if (!$is_registration_enabled)
{{ __('auth.registration_disabled') }}
@endif - @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach -
- @endif @if (session('status'))
{{ session('status') }}
@endif - @if (session('error')) -
- {{ session('error') }} -
- @endif
@if ($is_registration_enabled) diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 1d236a6c4..370f75656 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -32,25 +32,19 @@ $email = getOldOrLocal('email', 'test3@example.com'); label="{{ __('input.name') }}" /> -
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 439fc4ad2..f5eb5778a 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -14,7 +14,7 @@ 'w-full' => $fullWidth, ])> @if (!$hideLabel) -