diff --git a/.dockerignore b/.dockerignore index d6abd1451..3a0ec49f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,4 @@ yarn-error.log /_data .rnd /.ssh +.ignition.json diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/BUG_REPORT.yml rename to .github/ISSUE_TEMPLATE/01_BUG_REPORT.yml diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml rename to .github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml diff --git a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml new file mode 100644 index 000000000..d00853964 --- /dev/null +++ b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml @@ -0,0 +1,17 @@ +name: Lock closed Issues, Discussions, and PRs + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + lock-threads: + runs-on: ubuntu-latest + steps: + - name: Lock threads after 30 days of inactivity + uses: dessant/lock-threads@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + issue-inactive-days: '30' + pr-inactive-days: '30' + discussion-inactive-days: '30' diff --git a/.github/workflows/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml new file mode 100644 index 000000000..2afc996cb --- /dev/null +++ b/.github/workflows/chore-manage-stale-issues-and-prs.yml @@ -0,0 +1,28 @@ +name: Manage Stale Issues and PRs + +on: + schedule: + - cron: '0 2 * * *' + +jobs: + manage-stale: + runs-on: ubuntu-latest + steps: + - name: Manage stale issues and PRs + uses: actions/stale@v9 + id: stale + with: + stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.' + stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.' + close-issue-message: 'This issue has been automatically closed due to inactivity.' + close-pr-message: 'This pull request has been automatically closed due to inactivity.' + days-before-stale: 14 + days-before-close: 7 + stale-issue-label: '⏱︎ Stale' + stale-pr-label: '⏱︎ Stale' + only-labels: '💤 Waiting for feedback' + remove-stale-when-updated: true + operations-per-run: 100 + labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback' + close-issue-reason: 'not_planned' + exempt-all-milestones: false 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 fd4be2f11..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,26 +98,45 @@ 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: webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml new file mode 100644 index 000000000..771687d4b --- /dev/null +++ b/.github/workflows/coolify-production-build.yml @@ -0,0 +1,143 @@ +name: Production Build (v4) + +on: + push: + branches: ["main"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/service-templates.json + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + labels: | + coolify.managed=true + + aarch64: + runs-on: [self-hosted, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + labels: | + coolify.managed=true + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml new file mode 100644 index 000000000..7e937d17a --- /dev/null +++ b/.github/workflows/coolify-realtime-next.yml @@ -0,0 +1,146 @@ +name: Coolify Realtime Development + +on: + push: + branches: [ "next" ] + paths: + - .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: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify-realtime" + +jobs: + amd64: + runs-on: ubuntu-latest + 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: 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 and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/amd64 + push: true + 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: + 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: 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 and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/aarch64 + push: true + 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: + 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 ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|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 }}-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_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 75e3f1681..97bfd52eb 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -1,8 +1,8 @@ -name: Coolify Realtime (v4) +name: Coolify Realtime on: push: - branches: [ "main", "next" ] + branches: [ "main" ] paths: - .github/workflows/coolify-realtime.yml - docker/coolify-realtime/Dockerfile @@ -11,7 +11,8 @@ on: - docker/coolify-realtime/soketi-entrypoint.sh env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-realtime" jobs: @@ -22,27 +23,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} labels: | coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -50,27 +63,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,25 +103,43 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml new file mode 100644 index 000000000..dd5e6ebd6 --- /dev/null +++ b/.github/workflows/coolify-staging-build.yml @@ -0,0 +1,129 @@ +name: Staging Build + +on: + push: + branches-ignore: ["main", "v3"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/service-templates.json + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + labels: | + coolify.managed=true + + aarch64: + runs-on: [self-hosted, arm64] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + labels: | + coolify.managed=true + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 5fdc32991..95a228114 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -1,14 +1,15 @@ -name: Coolify Testing Host (v4-non-prod) +name: Coolify Testing Host on: push: - branches: [ "main", "next" ] + branches: [ "next" ] paths: - .github/workflows/coolify-testing-host.yml - docker/testing-host/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-testing-host" jobs: @@ -19,21 +20,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + labels: | + coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +55,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + labels: | + coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,21 +90,36 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml deleted file mode 100644 index 268b885ac..000000000 --- a/.github/workflows/development-build.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Development Build (v4) - -on: - push: - branches-ignore: ["main", "v3"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 0edaa4f1c..000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Docker Image CI - -on: - # push: - # branches: [ "main" ] - # pull_request: - # branches: [ "*" ] - push: - branches: ["this-does-not-exist"] - pull_request: - branches: ["this-does-not-exist"] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: | - /usr/local/share/ca-certificates - /var/cache/apt/archives - /var/lib/apt/lists - ~/.cache - key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }} - restore-keys: | - ${{ runner.os }}-docker- - - name: Build the Docker image - run: | - cp .env.example .env - docker run --rm -u "$(id -u):$(id -g)" \ - -v "$(pwd):/app" \ - -w /app composer:2 \ - composer install --ignore-platform-reqs - ./vendor/bin/spin build - - name: Start the stack - run: | - ./vendor/bin/spin up -d - ./vendor/bin/spin exec coolify php artisan key:generate - ./vendor/bin/spin exec coolify php artisan migrate:fresh --seed - - name: Test (missing E2E tests) - run: | - ./vendor/bin/spin exec coolify php artisan test diff --git a/.github/workflows/fix-php-code-style-issues b/.github/workflows/fix-php-code-style-issues deleted file mode 100644 index aebce91bc..000000000 --- a/.github/workflows/fix-php-code-style-issues +++ /dev/null @@ -1,25 +0,0 @@ -name: Fix PHP code style issues - -on: [push] - -permissions: - contents: write - -jobs: - php-code-styling: - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.4 - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Fix styling diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml deleted file mode 100644 index d7a680170..000000000 --- a/.github/workflows/pr-build.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: PR Build (v4) - -on: - pull_request: - types: - - opened - 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 - permissions: - contents: read - packages: write - attestations: write - id-token: write - actions: 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/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - attestations: write - id-token: write - actions: 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.event.number }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - actions: 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.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }} - - 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..09504afee 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ _ide_helper_models.php .rnd /.ssh scripts/load-test/* +.ignition.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a3e0e538..80ec0614e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,19 +6,16 @@ # Contributing to Coolify ## Table of Contents -- [Contributing to Coolify](#contributing-to-coolify) - - [Table of Contents](#table-of-contents) - - [1. Setup Development Environment](#1-setup-development-environment) - - [2. Verify Installation (Optional)](#2-verify-installation-optional) - - [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository) - - [4. Set up Environment Variables](#4-set-up-environment-variables) - - [5. Start Coolify](#5-start-coolify) - - [6. Start Development](#6-start-development) - - [7. Development Notes](#7-development-notes) - - [8. Create a Pull Request](#8-create-a-pull-request) - - [Additional Contribution Guidelines](#additional-contribution-guidelines) - - [Contributing a New Service](#contributing-a-new-service) - - [Contributing to Documentation](#contributing-to-documentation) +1. [Setup Development Environment](#1-setup-development-environment) +2. [Verify Installation](#2-verify-installation-optional) +3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) +4. [Set up Environment Variables](#4-set-up-environment-variables) +5. [Start Coolify](#5-start-coolify) +6. [Start Development](#6-start-development) +7. [Create a Pull Request](#7-create-a-pull-request) +8. [Development Notes](#development-notes) +9. [Resetting Development Environment](#resetting-development-environment) +10. [Additional Contribution Guidelines](#additional-contribution-guidelines) ## 1. Setup Development Environment @@ -29,15 +26,15 @@ ## 1. Setup Development Environment 1. Install `docker-ce`, Docker Desktop (or similar): - Docker CE (recommended): - - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install) - - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/) + - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify) + - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify) - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide - Install Docker Desktop (easier): - - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) + - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify) - Ensure WSL2 backend is enabled in Docker Desktop settings 2. Install Spin: - - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2) + - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify) @@ -46,12 +43,12 @@ ## 1. Setup Development Environment 1. Install Orbstack, Docker Desktop (or similar): - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): - - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation) + - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify) - Docker Desktop: - - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) + - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify) 2. Install Spin: - - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin) + - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify) @@ -60,12 +57,12 @@ ## 1. Setup Development Environment 1. Install Docker Engine, Docker Desktop (or similar): - Docker Engine (recommended, as there is no VM overhead): - - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/) for your Linux distribution + - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution - Docker Desktop: - - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/) + - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify) 2. Install Spin: - - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions) + - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify) @@ -89,14 +86,14 @@ ## 3. Fork and Setup Local Repository | Editor | Platform | Download Link | |--------|----------|---------------| - | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) | - | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) | - | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) | + | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) | + | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) | + | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) | 3. Clone the Coolify Repository from your fork to your local machine - Use `git clone` in the command line, or - Use GitHub Desktop (recommended): - - Download and install from [https://desktop.github.com/](https://desktop.github.com/) + - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify) - Open GitHub Desktop and login with your GitHub account - Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone` @@ -149,7 +146,36 @@ ## 6. Start Development > TELESCOPE_ENABLED=true > ``` -## 7. Development Notes +## 7. Create a Pull Request + +1. After making changes or adding a new service: + - Commit your changes to your forked repository. + - Push the changes to your GitHub account. + +2. Creating the Pull Request (PR): + - Navigate to the main Coolify repository on GitHub. + - Click the "Pull requests" tab. + - Click the green "New pull request" button. + - Choose your fork and branch as the compare branch. + - Click "Create pull request". + +3. Filling out the PR details: + - Give your PR a descriptive title. + - Use the Pull Request Template provided and fill in the details. + +> [!IMPORTANT] +> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. + +4. Submit your PR: + - Review your changes one last time. + - Click "Create pull request" to submit. + +> [!NOTE] +> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers. + +After submission, maintainers will review your PR and may request changes or provide feedback. + +## Development Notes When working on Coolify, keep the following in mind: @@ -168,35 +194,41 @@ ## 7. Development Notes > [!IMPORTANT] > Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. -## 8. Create a Pull Request +## Resetting Development Environment -1. After making changes or adding a new service: - - Commit your changes to your forked repository. - - Push the changes to your GitHub account. +If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`): -2. Creating the Pull Request (PR): - - Navigate to the main Coolify repository on GitHub. - - Click the "Pull requests" tab. - - Click the green "New pull request" button. - - Choose your fork and branch as the compare branch. - - Click "Create pull request". +1. Stop all running containers `ctrl + c`. -3. Filling out the PR details: - - Give your PR a descriptive title. - - In the description, explain the changes you've made. - - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". +2. Remove all Coolify containers: + ```bash + docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail + ``` + +3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command): + ```bash + docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data + ``` + +4. Remove unused images: + ```bash + docker image prune -a + ``` + +5. Start Coolify again: + ```bash + spin up + ``` + +6. Run database migrations and seeders: + ```bash + docker exec -it coolify php artisan migrate:fresh --seed + ``` + +After completing these steps, you'll have a fresh development setup. > [!IMPORTANT] -> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. - -4. Submit your PR: - - Review your changes one last time. - - Click "Create pull request" to submit. - -> [!NOTE] -> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers. - -After submission, maintainers will review your PR and may request changes or provide feedback. +> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data. ## Additional Contribution Guidelines diff --git a/RELEASE.md b/RELEASE.md index 2cb96b72b..d9f05f17d 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,35 +2,120 @@ # Coolify Release Guide This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed. +## Table of Contents +- [Release Process](#release-process) +- [Version Types](#version-types) + - [Stable](#stable) + - [Nightly](#nightly) + - [Beta](#beta) +- [Version Availability](#version-availability) + - [Self-Hosted](#self-hosted) + - [Cloud](#cloud) +- [Manually Update to Specific Versions](#manually-update-to-specific-versions) + ## Release Process -1. **Development on `next` or separate branches** - - Changes, fixes and new features are developed on the `next` or even separate branches. +1. **Development on `next` or Feature Branches** + - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches. 2. **Merging to `main`** - - Once changes are ready, they are merged from `next` into the `main` branch. + - Once ready, changes are merged from the `next` branch into the `main` branch. -3. **Building the release** - - After merging to `main`, a new release is built. - - Note: A push to `main` does not automatically mean a new version is released. +3. **Building the Release** + - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag. -4. **Creating a GitHub release** - - A new release is created on GitHub with the new version details. +4. **Creating a GitHub Release** + - A new GitHub release is manually created with details of the changes made in the version. 5. **Updating the CDN** - - The final step is updating the version information on the CDN: - [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json) + - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json) > [!NOTE] -> The CDN update may not occur immediately after the GitHub release. It can happen hours or even days later due to additional testing, stability checks, or potential hotfixes. +> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.** +## Version Types + +
+ Stable (coming soon) + +- **Stable** + - The production version suitable for stable, production environments (generally recommended). + - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes. + - **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release. + - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`). + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` + +
+ +
+ Nightly + +- **Nightly** + - The latest development version, suitable for testing the latest changes and experimenting with new features. + - **Update Frequency:** Daily or bi-weekly updates. + - **Release Size:** Smaller, more frequent releases. + - **Versioning Scheme:** TO BE DETERMINED + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next + ``` + +
+ +
+ Beta + +- **Beta** + - Test releases for the upcoming stable version. + - **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable. + - **Update Frequency:** Available if we think beta testing is necessary. + - **Release Size:** Same size as stable release as it will become the next stabe release after some time. + - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`). + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` + +
+ +> [!WARNING] +> Do not use nightly/beta builds in production as there is no guarantee of stability. ## Version Availability -It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update). +When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types. + +### Self-Hosted + +- **Update Frequency:** More frequent updates, especially on the nightly release channel. +- **Update Availability:** New versions are available once the CDN has been updated. +- **Update Methods:** + 1. **Manual Update in Instance Settings:** + - Go to `Settings > Update Check Frequency` and click the `Check Manually` button. + - If an update is available, an upgrade button will appear on the sidebar. + 2. **Automatic Update:** + - If enabled, the instance will update automatically at the time set in the settings. + 3. **Re-run Installation Script:** + - Run the installation script again to upgrade to the latest version available on the CDN: + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` > [!IMPORTANT] -> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released. +> If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release. + +### Cloud + +- **Update Frequency:** Less frequent as it's a managed service. +- **Update Availability:** New versions are available once Andras has updated the cloud version manually. +- **Update Method:** + - Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it. + +> [!IMPORTANT] +> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready. ## Manually Update to Specific Versions @@ -42,4 +127,4 @@ ## Manually Update to Specific Versions ```bash curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s ``` --> Replace `` with the version you want to update to (for example `4.0.0-beta.332`). +Replace `` with the version you want to update to (for example `4.0.0-beta.332`). diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php new file mode 100644 index 000000000..69365f921 --- /dev/null +++ b/app/Actions/Application/GenerateConfig.php @@ -0,0 +1,17 @@ +clearAll(); + return $application->generateConfig(is_json: $is_json); + } +} diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 352c6a59f..3ee46a2e1 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -46,9 +46,6 @@ public function handle(StandaloneDragonfly $database) 'networks' => [ $this->database->destination->network, ], - 'ulimits' => [ - 'memlock' => '-1', - ], 'labels' => [ 'coolify.managed' => 'true', ], diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 0779da31d..ed563eaae 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -651,8 +651,9 @@ private function old_way() // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); } - // Check if proxy is running - $this->server->proxyType(); + 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'; diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index f8882d12a..481757162 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -2,7 +2,6 @@ namespace App\Actions\Fortify; -use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; @@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers */ public function create(array $input): User { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { abort(403); } @@ -48,7 +47,7 @@ public function create(array $input): User $team = $user->teams()->first(); // Disable registration after first user is created - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $settings->is_registration_enabled = false; $settings->save(); } else { diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index dcb4058c0..55af1a8c0 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -2,7 +2,6 @@ namespace App\Actions\License; -use App\Models\InstanceSettings; use Illuminate\Support\Facades\Http; use Lorisleiva\Actions\Concerns\AsAction; @@ -13,7 +12,7 @@ class CheckResaleLicense public function handle() { try { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (isDev()) { $settings->update([ 'is_resale_license_active' => true, diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index f4fe650c5..bdeafd061 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -22,7 +22,7 @@ public function handle(Server $server, bool $reset = false) ]; $proxy_configuration = instant_remote_process($payload, $server, false); if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { - $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value; + $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value(); } if (! $proxy_configuration || is_null($proxy_configuration)) { throw new \Exception('Could not generate proxy configuration'); diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index cf0f6015c..03a0beddf 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -2,14 +2,17 @@ namespace App\Actions\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class CheckProxy { use AsAction; - public function handle(Server $server, $fromUI = false) + // It should return if the proxy should be started (true) or not (false) + public function handle(Server $server, $fromUI = false): bool { if (! $server->isFunctional()) { return false; @@ -62,22 +65,42 @@ public function handle(Server $server, $fromUI = false) $ip = 'host.docker.internal'; } - $connection80 = @fsockopen($ip, '80'); - $connection443 = @fsockopen($ip, '443'); - $port80 = is_resource($connection80) && fclose($connection80); - $port443 = is_resource($connection443) && fclose($connection443); - if ($port80) { - if ($fromUI) { - throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + $portsToCheck = ['80', '443']; + + try { + if ($server->proxyType() !== ProxyTypes::NONE->value) { + $proxyCompose = CheckConfiguration::run($server); + if (isset($proxyCompose)) { + $yaml = Yaml::parse($proxyCompose); + $portsToCheck = []; + if ($server->proxyType() === ProxyTypes::TRAEFIK->value) { + $ports = data_get($yaml, 'services.traefik.ports'); + } elseif ($server->proxyType() === ProxyTypes::CADDY->value) { + $ports = data_get($yaml, 'services.caddy.ports'); + } + if (isset($ports)) { + foreach ($ports as $port) { + $portsToCheck[] = str($port)->before(':')->value(); + } + } + } } else { - return false; + $portsToCheck = []; } + } catch (\Exception $e) { + ray($e->getMessage()); } - if ($port443) { - if ($fromUI) { - throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); - } else { - return false; + if (count($portsToCheck) === 0) { + return false; + } + foreach ($portsToCheck as $port) { + $connection = @fsockopen($ip, $port); + if (is_resource($connection) && fclose($connection)) { + if ($fromUI) { + throw new \Exception("Port $port is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } else { + return false; + } } } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index f025e5661..f20c10123 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -26,7 +26,7 @@ public function handle(Server $server, bool $async = true, bool $force = false): } 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->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); $server->save(); if ($server->isSwarm()) { $commands = $commands->merge([ @@ -35,7 +35,7 @@ public function handle(Server $server, bool $async = true, bool $force = false): "echo 'Creating required Docker Compose file.'", "echo 'Starting coolify-proxy.'", 'docker stack deploy -c docker-compose.yml coolify-proxy', - "echo 'Proxy started successfully.'", + "echo 'Successfully started coolify-proxy.'", ]); } else { $caddfile = 'import /dynamic/*.caddy'; @@ -46,12 +46,14 @@ public function handle(Server $server, bool $async = true, bool $force = false): "echo 'Creating required Docker Compose file.'", "echo 'Pulling docker image.'", 'docker compose pull', - "echo 'Stopping existing coolify-proxy.'", - 'docker stop -t 10 coolify-proxy || true', - 'docker rm coolify-proxy || true', + '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 'Proxy started successfully.'", + "echo 'Successfully started coolify-proxy.'", ]); $commands = $commands->merge(connectProxyToNetworks($server)); } diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 1034c13d6..dc6ac12bf 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -2,7 +2,6 @@ namespace App\Actions\Server; -use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -12,28 +11,29 @@ class CleanupDocker public function handle(Server $server) { + $settings = instanceSettings(); + $helperImageVersion = data_get($settings, 'helper_version'); + $helperImage = config('coolify.helper_image'); + $helperImageWithVersion = "$helperImage:$helperImageVersion"; - $commands = $this->getCommands(); + $commands = [ + 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', + 'docker image prune -af --filter "label!=coolify.managed=true"', + 'docker builder prune -af', + "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", + ]; + + $serverSettings = $server->settings; + if ($serverSettings->delete_unused_volumes) { + $commands[] = 'docker volume prune -af'; + } + + if ($serverSettings->delete_unused_networks) { + $commands[] = 'docker network prune -f'; + } foreach ($commands as $command) { instant_remote_process([$command], $server, false); } } - - private function getCommands(): array - { - $settings = InstanceSettings::get(); - $helperImageVersion = data_get($settings, 'helper_version'); - $helperImage = config('coolify.helper_image'); - $helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion; - - $commonCommands = [ - 'docker container prune -f --filter "label=coolify.managed=true"', - 'docker image prune -af --filter "label!=coolify.managed=true"', - 'docker builder prune -af', - "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi", - ]; - - return $commonCommands; - } } diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index f671f2d2a..2e1df8185 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -17,7 +17,7 @@ public function handle(Server $server) 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/StartSentinel.php b/app/Actions/Server/StartSentinel.php index b79bc8f67..a1d36a041 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -9,18 +10,48 @@ class StartSentinel { use AsAction; - public function handle(Server $server, $version = 'latest', bool $restart = false) + public function handle(Server $server, $version = 'next', bool $restart = false) { 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; + $metrics_history = data_get($server, 'settings.sentinel_metrics_history_days'); + $refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $push_interval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $token = data_get($server, 'settings.sentinel_token'); + $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $mount_dir = '/data/coolify/sentinel'; + $image = "ghcr.io/coollabsio/sentinel:$version"; + if (! $endpoint) { + throw new \Exception('You should set FQDN in Instance Settings.'); + } + $environments = [ + 'TOKEN' => $token, + 'PUSH_ENDPOINT' => $endpoint, + 'PUSH_INTERVAL_SECONDS' => $push_interval, + 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history, + ]; + if (isDev()) { + data_set($environments, 'DEBUG', 'true'); + $mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; + $image = 'sentinel'; + } + $docker_environments = '-e "' . implode('" -e "', array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . '"'; + + $docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image"; + instant_remote_process([ - "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 $mount_dir", + $docker_command, + "chown -R 9999:root $mount_dir", + "chmod -R 700 $mount_dir", + ], $server); + + $server->settings->is_sentinel_enabled = true; + $server->settings->save(); + $server->sentinelUpdateAt(); } } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php index 21ffca3bd..68972f0f2 100644 --- a/app/Actions/Server/StopSentinel.php +++ b/app/Actions/Server/StopSentinel.php @@ -3,6 +3,7 @@ namespace App\Actions\Server; use App\Models\Server; +use Carbon\Carbon; use Lorisleiva\Actions\Concerns\AsAction; class StopSentinel @@ -12,5 +13,6 @@ class StopSentinel public function handle(Server $server) { instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + $server->sentinelUpdateAt(isReset: true); } } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 901f2cf77..30664df26 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -3,7 +3,6 @@ namespace App\Actions\Server; use App\Jobs\PullHelperImageJob; -use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -20,7 +19,7 @@ class UpdateCoolify public function handle($manual_update = false) { try { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $this->server = Server::find(0); if (! $this->server) { return; diff --git a/app/Console/Commands/CheckApplicationDeploymentQueue.php b/app/Console/Commands/CheckApplicationDeploymentQueue.php new file mode 100644 index 000000000..e89d26f2c --- /dev/null +++ b/app/Console/Commands/CheckApplicationDeploymentQueue.php @@ -0,0 +1,50 @@ +option('seconds'); + $deployments = ApplicationDeploymentQueue::whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS, + ApplicationDeploymentStatus::QUEUED, + ])->where('created_at', '<=', now()->subSeconds($seconds))->get(); + if ($deployments->isEmpty()) { + $this->info('No deployments found in the last '.$seconds.' seconds.'); + + return; + } + + $this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.'); + + foreach ($deployments as $deployment) { + if ($this->option('force')) { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + $this->cancelDeployment($deployment); + } else { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + if ($this->confirm('Do you want to cancel this deployment?', true)) { + $this->cancelDeployment($deployment); + } + } + } + } + + private function cancelDeployment(ApplicationDeploymentQueue $deployment) + { + $deployment->update(['status' => ApplicationDeploymentStatus::FAILED]); + if ($deployment->server?->isFunctional()) { + remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false); + } + } +} diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php index f068e3eb2..3aae28ae6 100644 --- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php +++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php @@ -7,9 +7,9 @@ class CleanupApplicationDeploymentQueue extends Command { - protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; + protected $signature = 'cleanup:deployment-queue {--team-id=}'; - protected $description = 'CleanupApplicationDeploymentQueue'; + protected $description = 'Cleanup application deployment queue.'; public function handle() { diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index dfd09d4b7..66c25ec27 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -4,6 +4,7 @@ use App\Jobs\CleanupHelperContainersJob; use App\Models\Application; +use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; @@ -47,6 +48,17 @@ private function cleanup_stucked_resources() } catch (\Throwable $e) { echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; } + try { + $applicationsDeploymentQueue = ApplicationDeploymentQueue::get(); + foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) { + if (is_null($applicationDeploymentQueue->application)) { + echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n"; + $applicationDeploymentQueue->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n"; + } try { $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index 964b8e46e..20a2667c3 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -48,6 +48,13 @@ public function init() echo "Generating APP_KEY.\n"; Artisan::call('key:generate'); } + + // Generate STORAGE link if not exists + if (! file_exists(public_path('storage'))) { + echo "Generating STORAGE link.\n"; + Artisan::call('storage:link'); + } + // Seed database if it's empty $settings = InstanceSettings::find(0); if (! $settings) { diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 2f5d36140..ad7bff86d 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -7,7 +7,6 @@ use App\Enums\ApplicationDeploymentStatus; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; @@ -69,7 +68,7 @@ public function handle() } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! is_null(env('AUTOUPDATE', null))) { if (env('AUTOUPDATE') == true) { $settings->update(['is_auto_update_enabled' => true]); @@ -196,7 +195,7 @@ private function send_alive_signal() { $id = config('app.id'); $version = config('version'); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $do_not_track = data_get($settings, 'do_not_track'); if ($do_not_track == true) { echo "Skipping alive as do_not_track is enabled\n"; diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index de64afefa..9720e81ac 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -39,8 +39,8 @@ public function handle() $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) @@ -78,7 +78,7 @@ private function process_file($file) if ($logo->count() > 0) { $logo = str($logo[0])->after('# logo:')->trim()->value(); } else { - $logo = 'svgs/unknown.svg'; + $logo = 'svgs/coolify.png'; } $minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values(); if ($minversion->count() > 0) { diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 03d479400..a689b35b8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,13 +13,13 @@ use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; use App\Jobs\UpdateCoolifyJob; -use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Illuminate\Support\Carbon; class Kernel extends ConsoleKernel { @@ -28,7 +28,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { $this->all_servers = Server::all(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -66,7 +66,7 @@ protected function schedule(Schedule $schedule): void private function pull_images($schedule) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { @@ -88,7 +88,7 @@ private function pull_images($schedule) private function schedule_updates($schedule) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $updateCheckFrequency = $settings->update_check_frequency; $schedule->job(new CheckForUpdatesJob) @@ -115,7 +115,11 @@ private function check_resources($schedule) $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); } foreach ($servers as $server) { - $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + $last_sentinel_update = $server->sentinel_updated_at; + if (Carbon::parse($last_sentinel_update)->isBefore(now()->subMinutes(4))) { + $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + } + // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer(); $serverTimezone = $server->settings->server_timezone; if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); diff --git a/app/Enums/ContainerStatusTypes.php b/app/Enums/ContainerStatusTypes.php new file mode 100644 index 000000000..ffcb6d5b5 --- /dev/null +++ b/app/Enums/ContainerStatusTypes.php @@ -0,0 +1,14 @@ +settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); if ($this->settings->do_not_track) { return; } diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index b0a832605..1a2146799 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -94,7 +94,9 @@ public static function generateScpCommand(Server $server, string $source, string $muxPersistTime = config('constants.ssh.mux_persist_time'); $scp_command = "timeout $timeout scp "; - + if ($server->isIpv6()) { + $scp_command .= '-6 '; + } if (self::isMultiplexingEnabled()) { $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; self::ensureMultiplexedConnection($server); @@ -136,8 +138,8 @@ public static function generateSshCommand(Server $server, string $command) $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); - $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $delimiter = Hash::make($command); + $delimiter = base64_encode($delimiter); $command = str_replace($delimiter, '', $command); $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 48e126f27..2a1f846d3 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -132,6 +132,7 @@ public function applications(Request $request) '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.'], @@ -177,6 +178,7 @@ public function applications(Request $request) 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -235,6 +237,7 @@ public function create_public_application(Request $request) '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.'], @@ -279,6 +282,7 @@ public function create_public_application(Request $request) 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -337,6 +341,7 @@ public function create_private_gh_app_application(Request $request) '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.'], @@ -381,6 +386,7 @@ public function create_private_gh_app_application(Request $request) 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -468,6 +474,7 @@ public function create_private_deploy_key_application(Request $request) 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -552,6 +559,7 @@ public function create_dockerfile_application(Request $request) 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -602,6 +610,7 @@ public function create_dockerimage_application(Request $request) 'name' => ['type' => 'string', 'description' => 'The application name.'], 'description' => ['type' => 'string', 'description' => 'The application description.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -627,7 +636,7 @@ public function create_dockercompose_application(Request $request) 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']; + $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(); @@ -665,6 +674,8 @@ private function create_application(Request $request, $type) $fqdn = $request->domains; $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) { @@ -693,8 +704,7 @@ private function create_application(Request $request, $type) 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)], @@ -702,19 +712,21 @@ private function create_application(Request $request, $type) '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); @@ -738,6 +750,14 @@ private function create_application(Request $request, $type) $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->refresh(); if (! $application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); @@ -771,8 +791,7 @@ private function create_application(Request $request, $type) 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)], @@ -781,10 +800,10 @@ private function create_application(Request $request, $type) '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.', @@ -833,6 +852,10 @@ private function create_application(Request $request, $type) $application->environment_id = $environment->id; $application->source_type = $githubApp->getMorphClass(); $application->source_id = $githubApp->id; + 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) { @@ -867,8 +890,8 @@ private function create_application(Request $request, $type) 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)], @@ -877,10 +900,10 @@ private function create_application(Request $request, $type) '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([ @@ -925,6 +948,10 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + 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) { @@ -956,10 +983,13 @@ private function create_application(Request $request, $type) 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.', @@ -1004,6 +1034,10 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1034,12 +1068,14 @@ private function create_application(Request $request, $type) 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.', @@ -1062,6 +1098,10 @@ private function create_application(Request $request, $type) $application->destination_id = $destination->id; $application->destination_type = $destination->getMorphClass(); $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } $application->git_repository = 'coollabsio/coolify'; $application->git_branch = 'main'; @@ -1108,10 +1148,12 @@ private function create_application(Request $request, $type) 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.', @@ -1259,16 +1301,10 @@ public function application_by_uuid(Request $request) format: 'uuid', ) ), - new OA\Parameter( - name: 'cleanup', - in: 'query', - description: 'Delete configurations and volumes.', - required: false, - schema: new OA\Schema( - type: 'boolean', - default: true, - ) - ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -1316,10 +1352,14 @@ public function delete_by_uuid(Request $request) 'message' => 'Application not found', ], 404); } + DeleteResourceJob::dispatch( resource: $application, - deleteConfigurations: $cleanup, - deleteVolumes: $cleanup); + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Application deletion request queued.', @@ -1404,6 +1444,7 @@ public function delete_by_uuid(Request $request) 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], ], )), ]), @@ -1460,10 +1501,9 @@ public function update_by_uuid(Request $request) ], 404); } $server = $application->destination->server; - $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy']; + $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', @@ -1473,7 +1513,9 @@ public function update_by_uuid(Request $request) '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')) { @@ -1538,6 +1580,13 @@ public function update_by_uuid(Request $request) } $instantDeploy = $request->instant_deploy; + $use_build_server = $request->use_build_server; + + if (isset($use_build_server)) { + $application->settings->is_build_server_enabled = $use_build_server; + $application->settings->save(); + } + removeUnnecessaryFieldsFromRequest($request); $data = $request->all(); diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index a205704cc..65873f818 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1541,16 +1541,10 @@ public function create_database(Request $request, NewDatabaseTypes $type) format: 'uuid', ) ), - new OA\Parameter( - name: 'cleanup', - in: 'query', - description: 'Delete configurations and volumes.', - required: false, - schema: new OA\Schema( - type: 'boolean', - default: true, - ) - ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -1595,10 +1589,14 @@ public function delete_by_uuid(Request $request) if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } + DeleteResourceJob::dispatch( resource: $database, - deleteConfigurations: $cleanup, - deleteVolumes: $cleanup); + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Database deletion request queued.', diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index c085b88a5..2414b7a42 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -86,7 +86,7 @@ public function enable_api(Request $request) if ($teamId !== '0') { return response()->json(['message' => 'You are not allowed to enable the API.'], 403); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $settings->update(['is_api_enabled' => true]); return response()->json(['message' => 'API enabled.'], 200); @@ -138,7 +138,7 @@ public function disable_api(Request $request) if ($teamId !== '0') { return response()->json(['message' => 'You are not allowed to disable the API.'], 403); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $settings->update(['is_api_enabled' => false]); return response()->json(['message' => 'API disabled.'], 200); diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 5f0d6bb12..6d512e578 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -23,7 +23,7 @@ private function removeSensitiveDataFromSettings($settings) return serializeApiResponse($settings); } $settings = $settings->makeHidden([ - 'metrics_token', + 'sentinel_token', ]); return serializeApiResponse($settings); @@ -308,7 +308,7 @@ public function domains_by_server(Request $request) $projects = Project::where('team_id', $teamId)->get(); $domains = collect(); $applications = $projects->pluck('applications')->flatten(); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($applications->count() > 0) { foreach ($applications as $application) { $ip = $application->destination->server->ip; diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 0a6154410..89418517b 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -432,6 +432,10 @@ public function service_by_uuid(Request $request) tags: ['Services'], parameters: [ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), ], responses: [ new OA\Response( @@ -476,7 +480,14 @@ public function delete_by_uuid(Request $request) if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - DeleteResourceJob::dispatch($service); + + DeleteResourceJob::dispatch( + resource: $service, + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + ); return response()->json([ 'message' => 'Service deletion request queued.', @@ -516,7 +527,8 @@ public function delete_by_uuid(Request $request) items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -619,7 +631,8 @@ public function envs(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -738,7 +751,8 @@ public function update_env_by_uuid(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -853,7 +867,8 @@ public function create_bulk_envs(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -953,7 +968,8 @@ public function create_env(Request $request) ] ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1025,9 +1041,11 @@ public function delete_env_by_uuid(Request $request) type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1101,9 +1119,11 @@ public function action_deploy(Request $request) type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', @@ -1177,9 +1197,11 @@ public function action_stop(Request $request) type: 'object', properties: [ 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'], - ]) + ] + ) ), - ]), + ] + ), new OA\Response( response: 401, ref: '#/components/responses/401', diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 9569e8cfa..630d01045 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -22,7 +21,7 @@ public function callback(string $provider) $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); if (! $user) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { abort(403, 'Registration is disabled'); } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index 648720ba4..471e6d602 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -14,7 +14,7 @@ public function handle(Request $request, Closure $next): Response if (isCloud()) { return $next($request); } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->is_api_enabled === false) { return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 24565b389..9ae383a9f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -12,7 +12,6 @@ use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; -use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; @@ -962,7 +961,7 @@ private function save_environment_variables() } } if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { - if ($this->application->compose_parsing_version === '3') { + if ((int) $this->application->compose_parsing_version >= 3) { $envs->push("COOLIFY_URL={$this->application->fqdn}"); } else { $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); @@ -970,7 +969,7 @@ private function save_environment_variables() } if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); - if ($this->application->compose_parsing_version === '3') { + if ((int) $this->application->compose_parsing_version >= 3) { $envs->push("COOLIFY_FQDN={$url}"); } else { $envs->push("COOLIFY_URL={$url}"); @@ -1334,7 +1333,7 @@ private function create_workdir() private function prepare_builder_image() { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $helperImage = config('coolify.helper_image'); $helperImage = "{$helperImage}:{$settings->helper_version}"; // Get user home directory diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index 747a9a98a..f2348118a 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Models\InstanceSettings; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -22,7 +21,7 @@ public function handle(): void if (isDev() || isCloud()) { return; } - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 947dc4317..769739d5e 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -2,9 +2,7 @@ namespace App\Jobs; -use App\Actions\Database\StopDatabase; use App\Events\BackupCreated; -use App\Models\InstanceSettings; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; @@ -25,7 +23,6 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; -use Visus\Cuid2\Cuid2; class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { @@ -64,32 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public function __construct($backup) { $this->backup = $backup; - $this->team = Team::find($backup->team_id); - if (is_null($this->team)) { - return; - } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->service->server; - $this->s3 = $this->backup->s3; - } else { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->destination->server; - $this->s3 = $this->backup->s3; - } } public function handle(): void { try { - // Check if team is exists - if (is_null($this->team)) { - $this->backup->update(['status' => 'failed']); - StopDatabase::run($this->database); - $this->database->delete(); + $this->team = Team::find($this->backup->team_id); + if (! $this->team) { + $this->backup->delete(); return; } + if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->service->server; + $this->s3 = $this->backup->s3; + } else { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->destination->server; + $this->s3 = $this->backup->s3; + } + if (is_null($this->server)) { + throw new \Exception('Server not found?!'); + } + if (is_null($this->database)) { + throw new \Exception('Database not found?!'); + } BackupCreated::dispatch($this->team->id); @@ -239,7 +236,6 @@ public function handle(): void } } $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; - if ($this->database->name === 'coolify-db') { $databasesToBackup = ['coolify']; $this->directory_name = $this->container_name = 'coolify-db'; @@ -252,6 +248,9 @@ public function handle(): void try { if (str($databaseType)->contains('postgres')) { $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -280,6 +279,9 @@ public function handle(): void $this->backup_standalone_mongodb($database); } elseif (str($databaseType)->contains('mysql')) { $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -289,6 +291,9 @@ public function handle(): void $this->backup_standalone_mysql($database); } elseif (str($databaseType)->contains('mariadb')) { $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -327,7 +332,9 @@ public function handle(): void send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); throw $e; } finally { - BackupCreated::dispatch($this->team->id); + if ($this->team) { + BackupCreated::dispatch($this->team->id); + } } } @@ -386,7 +393,11 @@ private function backup_standalone_postgresql(string $database): void if ($this->postgres_password) { $backupCommand .= " -e PGPASSWORD=$this->postgres_password"; } - $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + if ($this->backup->dump_all) { + $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location"; + } else { + $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + } $commands[] = $backupCommand; ray($commands); @@ -407,8 +418,11 @@ private function backup_standalone_mysql(string $database): void { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; - ray($commands); + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; + } $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { @@ -426,7 +440,11 @@ private function backup_standalone_mariadb(string $database): void { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location"; + } 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); @@ -468,34 +486,6 @@ private function remove_old_backups(): void } } - // private function upload_to_s3(): void - // { - // try { - // if (is_null($this->s3)) { - // return; - // } - // $key = $this->s3->key; - // $secret = $this->s3->secret; - // // $region = $this->s3->region; - // $bucket = $this->s3->bucket; - // $endpoint = $this->s3->endpoint; - // $this->s3->testConnection(shouldSave: true); - // $configName = new Cuid2; - - // $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/'); - // $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'"; - // $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'"; - // instant_remote_process($commands, $this->server); - // $this->add_to_backup_output('Uploaded to S3.'); - // } catch (\Throwable $e) { - // $this->add_to_backup_output($e->getMessage()); - // throw $e; - // } finally { - // $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'"; - // $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'"; - // instant_remote_process($removeConfigCommands, $this->server, false); - // } - // } private function upload_to_s3(): void { try { @@ -517,10 +507,27 @@ private function upload_to_s3(): void $this->ensureHelperImageAvailable(); $fullImageName = $this->getFullImageName(); - $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; - $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + + if (isDev()) { + if ($this->database->name === 'coolify-db') { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } else { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } + } else { + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; + } + if ($this->s3->isHetzner()) { + $endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value(); + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret"; + } else { + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + } $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); + $this->add_to_backup_output('Uploaded to S3.'); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); @@ -562,7 +569,7 @@ private function pullHelperImage(string $fullImageName): void private function getFullImageName(): string { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $helperImage = config('coolify.helper_image'); $latestVersion = $settings->helper_version; diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index ac34d064e..2442d5b06 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -31,10 +31,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, - public bool $deleteConfigurations, - public bool $deleteVolumes, - public bool $dockerCleanup, - public bool $deleteConnectedNetworks + public bool $deleteConfigurations = true, + public bool $deleteVolumes = true, + public bool $dockerCleanup = true, + public bool $deleteConnectedNetworks = true ) {} public function handle() diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index a961fae4c..900bae99c 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -23,7 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?string $usageBefore = null; - public function __construct(public Server $server) {} + public function __construct(public Server $server, public bool $manualCleanup = false) {} public function handle(): void { @@ -31,8 +31,9 @@ public function handle(): void if (! $this->server->isFunctional()) { return; } - if ($this->server->settings->force_docker_cleanup) { - Log::info('DockerCleanupJob force cleanup on '.$this->server->name); + + if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { + Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name); CleanupDocker::run(server: $this->server); return; diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index ef1659680..4b208fc31 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -26,7 +25,7 @@ public function handle(): void $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { $versions = $response->json(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $latest_version = data_get($versions, 'coolify.helper.version'); $current_version = $settings->helper_version; if (version_compare($latest_version, $current_version, '>')) { diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php new file mode 100644 index 000000000..82e311d47 --- /dev/null +++ b/app/Jobs/PushServerUpdateJob.php @@ -0,0 +1,404 @@ +containers = collect(); + $this->foundApplicationIds = collect(); + $this->foundDatabaseUuids = collect(); + $this->foundServiceApplicationIds = collect(); + $this->foundApplicationPreviewsIds = collect(); + $this->foundServiceDatabaseIds = collect(); + $this->allApplicationIds = collect(); + $this->allDatabaseUuids = collect(); + $this->allTcpProxyUuids = collect(); + $this->allServiceApplicationIds = collect(); + $this->allServiceDatabaseIds = collect(); + } + + public function handle() + { + try { + if (! $this->data) { + throw new \Exception('No data provided'); + } + $data = collect($this->data); + + $this->serverStatus(); + + $this->server->sentinelUpdateAt(); + + $this->containers = collect(data_get($data, 'containers')); + if ($this->containers->isEmpty()) { + return; + } + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->previews = $this->server->previews(); + $this->services = $this->server->services()->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { + return $application->additional_servers->count() === 0; + })->pluck('id'); + $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { + return $application->additional_servers->count() > 0; + }); + $this->allApplicationPreviewsIds = $this->previews->pluck('id'); + $this->allDatabaseUuids = $this->databases->pluck('uuid'); + $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); + $this->services->each(function ($service) { + $service->applications()->pluck('id')->each(function ($applicationId) { + $this->allServiceApplicationIds->push($applicationId); + }); + $service->databases()->pluck('id')->each(function ($databaseId) { + $this->allServiceDatabaseIds->push($databaseId); + }); + }); + + ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]); + + foreach ($this->containers as $container) { + $containerStatus = data_get($container, 'state', 'exited'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = collect(data_get($container, 'labels')); + $coolify_managed = $labels->has('coolify.managed'); + if ($coolify_managed) { + $name = data_get($container, 'name'); + if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) { + $this->foundLogDrainContainer = true; + } + if ($labels->has('coolify.applicationId')) { + $applicationId = $labels->get('coolify.applicationId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0'); + try { + if ($pullRequestId === '0') { + if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationIds->push($applicationId); + } + $this->updateApplicationStatus($applicationId, $containerStatus); + } else { + if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationPreviewsIds->push($applicationId); + } + $this->updateApplicationPreviewStatus($applicationId, $containerStatus); + } + } catch (\Exception $e) { + ray()->error($e); + } + } elseif ($labels->has('coolify.serviceId')) { + $serviceId = $labels->get('coolify.serviceId'); + $subType = $labels->get('coolify.service.subType'); + $subId = $labels->get('coolify.service.subId'); + if ($subType === 'application' && $this->isRunning($containerStatus)) { + $this->foundServiceApplicationIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } elseif ($subType === 'database' && $this->isRunning($containerStatus)) { + $this->foundServiceDatabaseIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } + + } else { + $uuid = $labels->get('com.docker.compose.service'); + $type = $labels->get('coolify.type'); + if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) { + $this->foundProxy = true; + } elseif ($type === 'service' && $this->isRunning($containerStatus)) { + ray("Service: $uuid, $containerStatus"); + } else { + if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->foundDatabaseUuids->push($uuid); + if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); + } else { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false); + } + } + } + } + } + } + + $this->updateProxyStatus(); + + $this->updateNotFoundApplicationStatus(); + $this->updateNotFoundApplicationPreviewStatus(); + $this->updateNotFoundDatabaseStatus(); + $this->updateNotFoundServiceStatus(); + + $this->updateAdditionalServersStatus(); + + $this->checkLogDrainContainer(); + + } catch (\Exception $e) { + throw $e; + } + + } + + private function serverStatus(){ + if ($this->server->isFunctional() === false) { + throw new \Exception('Server is not ready.'); + } + if ($this->server->status() === false) { + throw new \Exception('Server is not reachable.'); + } + } + private function updateApplicationStatus(string $applicationId, string $containerStatus) + { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]); + } + + private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus) + { + $application = $this->previews->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]); + } + + private function updateNotFoundApplicationStatus() + { + $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); + if ($notFoundApplicationIds->isNotEmpty()) { + ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]); + $notFoundApplicationIds->each(function ($applicationId) { + ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']); + $application = Application::find($applicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']); + } + }); + } + } + + private function updateNotFoundApplicationPreviewStatus() + { + $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); + if ($notFoundApplicationPreviewsIds->isNotEmpty()) { + ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]); + $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) { + ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']); + $applicationPreview = ApplicationPreview::find($applicationPreviewId); + if ($applicationPreview) { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']); + } + }); + } + } + + private function updateProxyStatus() + { + // If proxy is not found, start it + if ($this->server->isProxyShouldRun()) { + if ($this->foundProxy === false) { + try { + if (CheckProxy::run($this->server)) { + StartProxy::run($this->server, false); + } + } catch (\Throwable $e) { + logger()->error($e); + } + } else { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + + } + + private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false) + { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if (! $database) { + return; + } + $database->status = $containerStatus; + $database->save(); + ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]); + if ($this->isRunning($containerStatus) && $tcpProxy) { + $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { + return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; + })->first(); + if (! $tcpProxyContainerFound) { + ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]); + StartDatabaseProxy::dispatch($database); + } else { + ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]); + } + } + } + + private function updateNotFoundDatabaseStatus() + { + $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); + if ($notFoundDatabaseUuids->isNotEmpty()) { + ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]); + $notFoundDatabaseUuids->each(function ($databaseUuid) { + ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']); + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if ($database) { + $database->status = 'exited'; + $database->save(); + ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']); + ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]); + if ($database->is_public) { + ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]); + StopDatabaseProxy::dispatch($database); + } + } + }); + } + } + + private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) + { + $service = $this->services->where('id', $serviceId)->first(); + if (! $service) { + return; + } + if ($subType === 'application') { + $application = $service->applications()->where('id', $subId)->first(); + $application->status = $containerStatus; + $application->save(); + ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } elseif ($subType === 'database') { + $database = $service->databases()->where('id', $subId)->first(); + $database->status = $containerStatus; + $database->save(); + ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } else { + ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]); + } + } + + private function updateNotFoundServiceStatus() + { + $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); + $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + if ($notFoundServiceApplicationIds->isNotEmpty()) { + ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]); + $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { + ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']); + $application = ServiceApplication::find($serviceApplicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']); + } + }); + } + if ($notFoundServiceDatabaseIds->isNotEmpty()) { + ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]); + $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { + ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']); + $database = ServiceDatabase::find($serviceDatabaseId); + if ($database) { + $database->status = 'exited'; + $database->save(); + ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']); + } + }); + } + } + + private function updateAdditionalServersStatus() + { + $this->allApplicationsWithAdditionalServers->each(function ($application) { + ray('Updating additional servers status for application', ['application_id' => $application->id]); + ComplexStatusCheck::run($application); + }); + } + + private function isRunning(string $containerStatus) + { + return str($containerStatus)->contains('running'); + } + + private function checkLogDrainContainer(){ + if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { + InstallLogDrain::dispatch($this->server); + } + } +} diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 7fde44f49..39d4aa0c0 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -69,7 +69,9 @@ public function handle() return 'No containers found.'; } GetContainersStatus::run($this->server, $this->containers, $containerReplicates); - $this->checkLogDrainContainer(); + if ($this->server->isLogDrainEnabled()) { + $this->checkLogDrainContainer(); + } } } catch (\Throwable $e) { @@ -115,9 +117,6 @@ private function serverStatus() private function checkLogDrainContainer() { - if (! $this->server->isLogDrainEnabled()) { - return; - } $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { return data_get($value, 'Name') === '/coolify-log-drain'; })->first(); diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index b2c816f5d..1f09d5a3b 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\; use Illuminate\Queue\SerializesModels; class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php new file mode 100644 index 000000000..376cb8532 --- /dev/null +++ b/app/Jobs/ServerStorageCheckJob.php @@ -0,0 +1,59 @@ +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.'%'); + } + + } catch (\Throwable $e) { + ray($e->getMessage()); + + return handleError($e); + } + + } +} diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php index 4c65a711f..2cc705e4a 100644 --- a/app/Jobs/UpdateCoolifyJob.php +++ b/app/Jobs/UpdateCoolifyJob.php @@ -3,7 +3,6 @@ namespace App\Jobs; use App\Actions\Server\UpdateCoolify; -use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -23,7 +22,7 @@ public function handle(): void { try { CheckForUpdatesJob::dispatchSync(); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->new_version_available) { Log::info('No new version available. Skipping update.'); diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 1f0b68dd3..d18a7689e 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -30,7 +30,7 @@ public function mount() public function cleanup_queue() { - Artisan::queue('cleanup:application-deployment-queue', [ + Artisan::queue('cleanup:deployment-queue', [ '--team-id' => currentTeam()->id, ]); } diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index d6dc0d521..934e81661 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -47,7 +47,7 @@ public function submit() ] ); $mail->subject("[HELP]: {$this->subject}"); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); if (! $type) { $url = 'https://app.coolify.io/api/feedback'; @@ -61,6 +61,7 @@ public function submit() send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); } $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); + $this->reset('description', 'subject'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 2960ed226..53673292e 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -172,7 +172,7 @@ public function submitResend() public function copyFromInstanceSettings() { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->smtp_enabled) { $team = currentTeam(); $team->update([ diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index d2700f444..2e327d80f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -2,9 +2,11 @@ namespace App\Livewire\Project\Application; +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 @@ -182,9 +184,7 @@ public function instantSave() $storage->save(); }); } - } - } public function loadComposeFile($isInit = false) @@ -241,16 +241,6 @@ public function updatedApplicationBaseDirectory() } } - 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() { @@ -287,18 +277,22 @@ public function getWildcardDomain() public function resetDefaultLabels() { - if ($this->application->settings->is_container_label_readonly_enabled) { - return; + try { + if ($this->application->settings->is_container_label_readonly_enabled) { + 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) @@ -320,7 +314,7 @@ public function checkFqdns($showToaster = true) public function set_redirect() { try { - $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); + $has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count(); if ($has_www === 0 && $this->application->redirect === 'www') { $this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.

Please add www to your domain list and as an A DNS record (if applicable).'); @@ -337,15 +331,18 @@ public function set_redirect() 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(','); + $this->resetDefaultLabels(); + + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->checkFqdns(); @@ -408,9 +405,25 @@ public function submit($showToaster = true) $this->application->save(); $showToaster && $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); + $fileName = str($this->application->name)->slug()->append('_config.json'); + + return response()->streamDownload(function () use ($config) { + echo $config; + }, $fileName, [ + 'Content-Type' => 'application/json', + '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 @@ public function resetToDefault() public function generate_real_url() { if (data_get($this->application, 'fqdn')) { - $firstFqdn = str($this->application->fqdn)->before(','); - $url = Url::fromString($firstFqdn); - $host = $url->getHost(); - $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + try { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } catch (\Exception $e) { + $this->dispatch('error', 'Invalid FQDN.'); + } } } diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index ec87beead..7e2e4a12b 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -31,6 +31,7 @@ class BackupEdit extends Component 'backup.save_s3' => 'required|boolean', 'backup.s3_storage_id' => 'nullable|integer', 'backup.databases_to_backup' => 'nullable', + 'backup.dump_all' => 'required|boolean', ]; protected $validationAttributes = [ @@ -40,6 +41,7 @@ class BackupEdit extends Component 'backup.save_s3' => 'Save to S3', 'backup.s3_storage_id' => 'S3 Storage', 'backup.databases_to_backup' => 'Databases to Backup', + 'backup.dump_all' => 'Backup All Databases', ]; protected $messages = [ @@ -182,7 +184,7 @@ public function render() { return view('livewire.project.database.backup-edit', [ 'checkboxes' => [ - ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'], + ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')], // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.'] // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.'] ], diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 765213f60..49884ff9a 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -78,7 +78,7 @@ public function render() { return view('livewire.project.database.heading', [ 'checkboxes' => [ - ['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'], + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], ], ]); } diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index beb5a9c39..8021e25d3 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -26,7 +26,7 @@ class ScheduledBackups extends Component public function mount(): void { if ($this->selectedBackupId) { - $this->setSelectedBackup($this->selectedBackupId); + $this->setSelectedBackup($this->selectedBackupId, true); } $this->parameters = get_route_parameters(); if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { @@ -37,10 +37,13 @@ public function mount(): void $this->s3s = currentTeam()->s3s; } - public function setSelectedBackup($backupId) + public function setSelectedBackup($backupId, $force = false) { + if ($this->selectedBackupId === $backupId && ! $force) { + return; + } $this->selectedBackupId = $backupId; - $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); + $this->selectedBackup = $this->database->scheduledBackups->find($backupId); if (is_null($this->selectedBackup)) { $this->selectedBackupId = null; } diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0e4f15a5c..f8eb838be 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -18,7 +18,11 @@ class Index extends Component public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) { + $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]); + + return $project; + }); $this->servers = Server::ownedByCurrentTeam()->count(); } diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index b5c5cb1db..971d4700b 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -31,10 +31,12 @@ class PublicGitRepository extends Component public bool $isStatic = false; + public bool $checkCoolifyConfig = true; + public ?string $publish_directory = null; // In case of docker compose - public ?string $base_directory = null; + public string $base_directory = '/'; public ?string $docker_compose_location = '/docker-compose.yaml'; // End of docker compose @@ -97,6 +99,7 @@ public function updatedBaseDirectory() $this->base_directory = '/'.$this->base_directory; } } + } public function updatedDockerComposeLocation() @@ -275,6 +278,7 @@ public function submit() 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } else { $application_init = [ @@ -289,6 +293,7 @@ public function submit() 'source_id' => $this->git_source->id, 'source_type' => $this->git_source->getMorphClass(), 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } @@ -303,11 +308,15 @@ public function submit() $application->settings->is_static = $this->isStatic; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; $application->save(); - + if ($this->checkCoolifyConfig) { + // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id); + // if ($config) { + // $application->setConfig($config); + // } + } return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, 'environment_name' => $environment->name, diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 3c5f3901b..7f8247597 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -49,11 +49,8 @@ class Select extends Component public ?string $existingPostgresqlUrl = null; - public ?string $search = null; - protected $queryString = [ 'server_id', - 'search', ]; public function mount() @@ -90,40 +87,119 @@ public function updatedSelectedEnvironment() // } // } - public function updatedSearch() + public function loadServices() { - $this->loadServices(); - } + $services = get_service_templates(true); + $services = collect($services)->map(function ($service, $key) { + return [ + 'name' => str($key)->headline(), + 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')), + ] + (array) $service; + })->all(); + $gitBasedApplications = [ + [ + 'id' => 'public', + 'name' => 'Public Repository', + 'description' => 'You can deploy any kind of public repositories from the supported git providers.', + 'logo' => asset('svgs/git.svg'), + ], + [ + 'id' => 'private-gh-app', + 'name' => 'Private Repository (with GitHub App)', + 'description' => 'You can deploy public & private repositories through your GitHub Apps.', + 'logo' => asset('svgs/github.svg'), + ], + [ + 'id' => 'private-deploy-key', + 'name' => 'Private Repository (with Deploy Key)', + 'description' => 'You can deploy private repositories with a deploy key.', + 'logo' => asset('svgs/git.svg'), + ], + ]; + $dockerBasedApplications = [ + [ + 'id' => 'dockerfile', + 'name' => 'Dockerfile', + 'description' => 'You can deploy a simple Dockerfile, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-compose-empty', + 'name' => 'Docker Compose Empty', + 'description' => 'You can deploy complex application easily with Docker Compose, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-image', + 'name' => 'Docker Image', + 'description' => 'You can deploy an existing Docker Image from any Registry, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + ]; + $databases = [ + [ + 'id' => 'postgresql', + 'name' => 'PostgreSQL', + 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.', + 'logo' => ' +', + ], + [ + 'id' => 'mysql', + 'name' => 'MySQL', + 'description' => 'MySQL is an open-source relational database management system. ', + 'logo' => ' + + + +', - public function loadServices(bool $force = false) - { - try { - $this->loadingServices = true; - if (count($this->allServices) > 0 && ! $force) { - if (! $this->search) { - $this->services = $this->allServices; + ], + [ + 'id' => 'mariadb', + 'name' => 'MariaDB', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source software under the GNU General Public License.', + 'logo' => '', + ], + [ + 'id' => 'redis', + 'name' => 'Redis', + 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.', + 'logo' => '', + ], + [ + 'id' => 'keydb', + 'name' => 'KeyDB', + 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.', + 'logo' => '
', + ], + [ + 'id' => 'dragonfly', + 'name' => 'Dragonfly', + 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.', + 'logo' => '
', + ], + [ + 'id' => 'mongodb', + 'name' => 'MongoDB', + 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.', + 'logo' => '', + ], + [ + 'id' => 'clickhouse', + 'name' => 'ClickHouse', + 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.', + 'logo' => '
', + ], - return; - } - $this->services = $this->allServices->filter(function ($service, $key) { - $tags = collect(data_get($service, 'tags', [])); + ]; - return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) { - return str_contains(strtolower($tag), strtolower($this->search)); - }); - }); - } else { - $this->search = null; - $this->allServices = get_service_templates($force); - $this->services = $this->allServices->filter(function ($service, $key) { - return str_contains(strtolower($key), strtolower($this->search)); - }); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->loadingServices = false; - } + return [ + 'services' => $services, + 'gitBasedApplications' => $gitBasedApplications, + 'dockerBasedApplications' => $dockerBasedApplications, + 'databases' => $databases, + ]; } public function instantSave() @@ -141,6 +217,7 @@ public function instantSave() public function setType(string $type) { + $type = str($type)->lower()->slug()->value(); if ($this->loading) { return; } 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 @@ public function mount() } $this->project = $project; $this->environment = $environment; - $this->applications = $this->environment->applications->load(['tags']); $this->applications = $this->applications->map(function ($application) { if (data_get($application, 'environment.project.uuid')) { diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 70e8006c7..4138f720e 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -4,6 +4,7 @@ use App\Models\ServiceApplication; use Livewire\Component; +use Spatie\Url\Url; class EditDomain extends Component { @@ -20,21 +21,16 @@ public function mount() { $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(','); check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -44,12 +40,15 @@ public function submit() } else { $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/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 42c9357fd..fa76ee26f 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -39,6 +39,7 @@ public function getListeners() return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', + "envsUpdated" => '$refresh', ]; } @@ -108,8 +109,23 @@ public function restart() return; } + StopService::run(service: $this->service, dockerCleanup: false); + $this->service->parse(); + $this->dispatch('imagePulled'); + $activity = StartService::run($this->service); + $this->dispatch('activityMonitor', $activity->id); + } + + public function pullAndRestartEvent() + { + $this->checkDeployments(); + if ($this->isDeploymentProgress) { + $this->dispatch('error', 'There is a deployment in progress.'); + + return; + } PullImage::run($this->service); - StopService::run($this->service); + StopService::run(service: $this->service, dockerCleanup: false); $this->service->parse(); $this->dispatch('imagePulled'); $activity = StartService::run($this->service); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 56b506043..ba37313fd 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Component; +use Spatie\Url\Url; class ServiceApplicationView extends Component { @@ -31,13 +32,7 @@ class ServiceApplicationView 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->application->save(); + } public function instantSave() @@ -83,6 +78,14 @@ public function mount() 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(','); + check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -92,10 +95,13 @@ public function submit() } else { $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 @@ public function mount() $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 @@ public function mount() '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 543e64539..c05260899 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -91,10 +91,12 @@ public function mount() public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (isProduction()) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if (! $this->resource) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index a859c90b0..0dbf0f957 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -48,14 +48,6 @@ public function mount() public function submit() { $this->validate(); - // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - // $type = str($this->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; - // } - // } $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 055788b57..5a711259b 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -53,30 +53,16 @@ public function instantSave() public function sortEnvironmentVariables() { - if ($this->resource->type() === 'application') { - $this->resource->load(['environment_variables', 'environment_variables_preview']); - } else { - $this->resource->load(['environment_variables']); + if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) { + if ($this->resource->environment_variables) { + $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values(); + } + + if ($this->resource->environment_variables_preview) { + $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values(); + } } - $sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order'; - - $sortFunction = function ($variables) use ($sortBy) { - if (! $variables) { - return $variables; - } - if ($sortBy === 'key') { - return $variables->sortBy(function ($item) { - return strtolower($item->key); - }, SORT_NATURAL | SORT_FLAG_CASE)->values(); - } else { - return $variables->sortBy('order')->values(); - } - }; - - $this->resource->environment_variables = $sortFunction($this->resource->environment_variables); - $this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview); - $this->getDevView(); } @@ -121,6 +107,8 @@ public function submit($data = null) $this->sortEnvironmentVariables(); } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->refreshEnvs(); } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 463ceecad..0538a6bdb 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -37,6 +37,7 @@ class Show extends Component 'env.is_literal' => 'required|boolean', 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', + 'env.is_required' => 'required|boolean', ]; protected $validationAttributes = [ @@ -46,6 +47,7 @@ class Show extends Component 'env.is_multiline' => 'Multiline', 'env.is_literal' => 'Literal', 'env.is_shown_once' => 'Shown Once', + 'env.is_required' => 'Required', ]; public function refresh() @@ -109,14 +111,14 @@ public function submit() } 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->env->is_required && str($this->env->real_value)->isEmpty()) { + $oldValue = $this->env->getOriginal('value'); + $this->env->value = $oldValue; + $this->dispatch('error', 'Required environment variable cannot be empty.'); + + return; + } $this->serialize(); $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index d95443621..90419caed 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -11,6 +11,8 @@ class ExecuteContainerCommand extends Component { + public $selected_container = 'default'; + public $container; public Collection $containers; @@ -83,11 +85,14 @@ public function loadContainers() $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); } foreach ($containers as $container) { - $payload = [ - 'server' => $server, - 'container' => $container, - ]; - $this->containers = $this->containers->push($payload); + // if container state is running + if (data_get($container, 'State') === 'running') { + $payload = [ + 'server' => $server, + 'container' => $container, + ]; + $this->containers = $this->containers->push($payload); + } } } elseif (data_get($this->parameters, 'database_uuid')) { if ($this->resource->isRunning()) { @@ -100,7 +105,6 @@ public function loadContainers() } } elseif (data_get($this->parameters, 'service_uuid')) { $this->resource->applications()->get()->each(function ($application) { - ray($application); if ($application->isRunning()) { $this->containers->push([ 'server' => $this->resource->server, @@ -131,9 +135,14 @@ public function loadContainers() #[On('connectToContainer')] public function connectToContainer() { + if ($this->selected_container === 'default') { + $this->dispatch('error', 'Please select a container.'); + + return; + } try { - $container_name = data_get($this->container, 'container.Names'); - if (is_null($container_name)) { + $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container); + if (is_null($container)) { throw new \RuntimeException('Container not found.'); } $server = data_get($this->container, 'server'); @@ -141,11 +150,11 @@ public function connectToContainer() if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - - $this->dispatch('send-terminal-command', - true, - $container_name, - $server->uuid, + $this->dispatch( + 'send-terminal-command', + isset($container), + data_get($container, 'container.Names'), + data_get($container, 'server.uuid') ); } catch (\Throwable $e) { 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 @@ public function pollData() 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/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 27be46227..916db650f 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -34,9 +34,9 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid) if ($status !== 'running') { return; } - $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); } else { - $command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); } // ssh command is sent back to frontend then to websocket diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php new file mode 100644 index 000000000..dea842651 --- /dev/null +++ b/app/Livewire/Project/Shared/UploadConfig.php @@ -0,0 +1,41 @@ +config = '{ + "build_pack": "nixpacks", + "base_directory": "/nodejs", + "publish_directory": "/", + "ports_exposes": "3000", + "settings": { + "is_static": false + } +}'; + } + } + public function uploadConfig() + { + try { + $application = Application::findOrFail($this->applicationId); + $application->setConfig($this->config); + $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/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index ff8679d21..fe68a8ba5 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -2,6 +2,7 @@ namespace App\Livewire\Security; +use App\Models\InstanceSettings; use Livewire\Component; class ApiTokens extends Component @@ -14,8 +15,12 @@ class ApiTokens extends Component public bool $readOnly = true; + public bool $rootAccess = false; + public array $permissions = ['read-only']; + public $isApiEnabled; + public function render() { return view('livewire.security.api-tokens'); @@ -23,6 +28,7 @@ public function render() public function mount() { + $this->isApiEnabled = InstanceSettings::get()->is_api_enabled; $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); } @@ -31,12 +37,11 @@ public function updatedViewSensitiveData() if ($this->viewSensitiveData) { $this->permissions[] = 'view:sensitive'; $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; } else { $this->permissions = array_diff($this->permissions, ['view:sensitive']); } - if (count($this->permissions) == 0) { - $this->permissions = ['*']; - } + $this->makeSureOneIsSelected(); } public function updatedReadOnly() @@ -44,11 +49,30 @@ public function updatedReadOnly() if ($this->readOnly) { $this->permissions[] = 'read-only'; $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; } else { $this->permissions = array_diff($this->permissions, ['read-only']); } - if (count($this->permissions) == 0) { + $this->makeSureOneIsSelected(); + } + + public function updatedRootAccess() + { + if ($this->rootAccess) { $this->permissions = ['*']; + $this->readOnly = false; + $this->viewSensitiveData = false; + } else { + $this->readOnly = true; + $this->permissions = ['read-only']; + } + } + + public function makeSureOneIsSelected() + { + if (count($this->permissions) == 0) { + $this->permissions = ['read-only']; + $this->readOnly = true; } } @@ -58,12 +82,6 @@ public function addNewToken() $this->validate([ 'description' => 'required|min:3|max:255', ]); - // if ($this->viewSensitiveData) { - // $this->permissions[] = 'view:sensitive'; - // } - // if ($this->readOnly) { - // $this->permissions[] = 'read-only'; - // } $token = auth()->user()->createToken($this->description, $this->permissions); $this->tokens = auth()->user()->tokens; session()->flash('token', $token->plainTextToken); diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php index 0921c7fa4..09b31c0b0 100644 --- a/app/Livewire/Server/Charts.php +++ b/app/Livewire/Server/Charts.php @@ -34,12 +34,12 @@ public function loadData() 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]]; - }); + // $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/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index a69a5e15d..f58d7b6be 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -30,6 +30,11 @@ public function alreadyConfigured() public function submit() { try { + if (str($this->ssh_domain)->contains('https://')) { + $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); + // remove / from the end + $this->ssh_domain = str($this->ssh_domain)->replace('/', ''); + } $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); ConfigureCloudflared::dispatch($server, $this->cloudflare_token); $server->settings->is_cloudflare_tunnel = true; diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 3cb3305b5..dadf9033d 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -4,8 +4,10 @@ use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; +use App\Jobs\DockerCleanupJob; use App\Jobs\PullSentinelImageJob; use App\Models\Server; +use Illuminate\Support\Facades\Http; use Livewire\Component; class Form extends Component @@ -24,6 +26,10 @@ class Form extends Component public $timezones; + public $delete_unused_volumes = false; + + public $delete_unused_networks = false; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -49,15 +55,19 @@ public function getListeners() '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', + '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', 'wildcard_domain' => 'nullable|url', - 'server.settings.is_server_api_enabled' => 'required|boolean', + 'server.settings.sentinel_custom_url' => 'nullable|url', + 'server.settings.is_sentinel_enabled' => 'required|boolean', 'server.settings.server_timezone' => 'required|string|timezone', 'server.settings.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 = [ @@ -74,11 +84,15 @@ public function getListeners() 'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.dynamic_timeout' => 'Dynamic Timeout', 'server.settings.is_metrics_enabled' => 'Metrics', - 'server.settings.metrics_token' => 'Metrics Token', - 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', - 'server.settings.metrics_history_days' => 'Metrics History', - 'server.settings.is_server_api_enabled' => 'Server API', + 'server.settings.sentinel_token' => 'Metrics Token', + 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval', + 'server.settings.sentinel_metrics_history_days' => 'Metrics History', + 'server.settings.sentinel_push_interval_seconds' => 'Push Interval', + 'server.settings.is_sentinel_enabled' => 'Server API', + 'server.settings.sentinel_custom_url' => 'Sentinel 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) @@ -88,6 +102,24 @@ public function mount(Server $server) $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->generateSentinelToken(); + $this->server->settings->refresh(); + $this->dispatch('success', 'Sentinel token regenerated. Please restart your Sentinel.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function updated($field) @@ -120,40 +152,47 @@ public function updatedServerSettingsIsBuildServer() $this->dispatch('proxyStatusUpdated'); } - public function checkPortForServerApi() - { - try { - if ($this->server->settings->is_server_api_enabled === true) { - $this->server->checkServerApi(); - $this->dispatch('success', 'Server API is reachable.'); - } - } catch (\Throwable $e) { - return handleError($e, $this); + public function updatedServerSettingsIsSentinelEnabled($value){ + if($value === false){ + StopSentinel::dispatch($this->server); + $this->server->settings->is_metrics_enabled = false; + $this->server->settings->save(); + $this->server->sentinelUpdateAt(isReset: true); + } else { + StartSentinel::run($this->server); } } + public function updatedServerSettingsIsMetricsEnabled(){ + $this->restartSentinel(); + } + + public function instantSave() { try { refresh_server_connection($this->server->privateKey); $this->validateServer(false); + $this->server->settings->save(); $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); - } + + // 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_sentinel_enabled') && $this->server->settings->is_sentinel_enabled === true) { + // ray('Starting sentinel'); + // } + // } else { + // ray('Sentinel is not enabled'); + // StopSentinel::dispatch($this->server); + // } + $this->server->settings->save(); // $this->checkPortForServerApi(); } catch (\Throwable $e) { @@ -166,7 +205,7 @@ public function restartSentinel() try { $version = get_latest_sentinel_version(); StartSentinel::run($this->server, $version, true); - $this->dispatch('success', 'Sentinel restarted.'); + $this->dispatch('success', 'Sentinel started.'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -232,22 +271,24 @@ public function submit() $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(); + $this->dispatch('success', 'Server updated.'); } catch (\Throwable $e) { return handleError($e, $this); } } - public function updatedServerSettingsServerTimezone($value) + public function manualCleanup() { - $this->server->settings->server_timezone = $value; - $this->server->settings->save(); - $this->dispatch('success', 'Server timezone updated.'); + 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() diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php index d23d7fc20..f4f18381f 100644 --- a/app/Livewire/Server/Proxy/Status.php +++ b/app/Livewire/Server/Proxy/Status.php @@ -4,7 +4,7 @@ use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; -use App\Jobs\ContainerStatusJob; +use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -44,11 +44,18 @@ public function checkProxy(bool $notification = false) } $this->numberOfPolls++; } - CheckProxy::run($this->server, true); + $shouldStart = CheckProxy::run($this->server, true); + if ($shouldStart) { + StartProxy::run($this->server, false); + } $this->dispatch('proxyStatusUpdated'); if ($this->server->proxy->status === 'running') { $this->polling = false; $notification && $this->dispatch('success', 'Proxy is running.'); + } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) { + $notification && $this->dispatch('error', 'Proxy has exited.'); + } elseif ($this->server->proxy->force_stop) { + $notification && $this->dispatch('error', 'Proxy is stopped manually.'); } else { $notification && $this->dispatch('error', 'Proxy is not running.'); } diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index c52970258..eb492e691 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -28,6 +28,7 @@ class Index extends Component protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected Server $server; + public $timezones; protected $rules = [ 'settings.fqdn' => 'nullable', @@ -53,14 +54,14 @@ class Index extends Component 'settings.is_auto_update_enabled' => 'Auto Update Enabled', 'auto_update_frequency' => 'Auto Update Frequency', 'update_check_frequency' => 'Update Check Frequency', + 'settings.instance_timezone' => 'Instance Timezone', ]; - public $timezones; public function mount() { if (isInstanceAdmin()) { - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $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; @@ -162,7 +163,7 @@ public function checkManually() { CheckForUpdatesJob::dispatchSync(); $this->dispatch('updateAvailable'); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->new_version_available) { $this->dispatch('success', 'New version available!'); } else { @@ -170,12 +171,6 @@ public function checkManually() } } - public function updatedSettingsInstanceTimezone($value) - { - $this->settings->instance_timezone = $value; - $this->settings->save(); - $this->dispatch('success', 'Instance timezone updated.'); - } public function render() { diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index f9402fd7b..ca0c9c1ae 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -29,7 +29,7 @@ public function mount() abort(404); } $this->instance_id = config('app.id'); - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); } public function render() diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 99b8f8d49..9240aa96d 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -42,7 +42,7 @@ class SettingsBackup extends Component public function mount() { if (isInstanceAdmin()) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $this->database = StandalonePostgresql::whereName('coolify-db')->first(); $s3s = S3Storage::whereTeamId(0)->get() ?? []; if ($this->database) { diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 3eb8ea646..4515df9a7 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -43,7 +43,7 @@ class SettingsEmail extends Component public function mount() { if (isInstanceAdmin()) { - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $this->emails = auth()->user()->email; } else { return redirect()->route('dashboard'); diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 75d7fd04a..193b650ff 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -99,7 +99,7 @@ public function mount() return redirect()->route('source.all'); } $this->applications = $this->github_app->applications; - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->name = str($this->github_app->name)->kebab(); 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/Storage/Create.php b/app/Livewire/Storage/Create.php index a05834ecc..c5250e1e3 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -43,15 +43,17 @@ class Create extends Component 'endpoint' => 'Endpoint', ]; - public function mount() + public function updatedEndpoint($value) { - if (isDev()) { - $this->name = 'Local MinIO'; - $this->description = 'Local MinIO'; - $this->key = 'minioadmin'; - $this->secret = 'minioadmin'; - $this->bucket = 'local'; - $this->endpoint = 'http://coolify-minio:9000'; + if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) { + $this->endpoint = 'https://'.$value; + $value = $this->endpoint; + } + + if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) { + $this->bucket = str($value)->after('//')->before('.'); + } elseif (str($value)->contains('your-objectstorage.com')) { + $this->bucket = $this->bucket ?: str($value)->after('//')->before('.'); } } diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index c278bf58e..df450cf7e 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -23,7 +23,7 @@ public function mount() if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { return redirect()->route('subscription.show'); } - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 55006745a..3002f1f45 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -9,6 +9,7 @@ use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use OpenApi\Attributes as OA; use RuntimeException; @@ -104,7 +105,7 @@ class Application extends BaseModel { use SoftDeletes; - private static $parserVersion = '3'; + private static $parserVersion = '4'; protected $guarded = []; @@ -143,6 +144,9 @@ protected static function booted() } $application->tags()->detach(); $application->previews()->delete(); + foreach ($application->deployment_queue as $deployment) { + $deployment->delete(); + } }); } @@ -209,7 +213,7 @@ public function delete_configurations() $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - instant_remote_process(['rm -rf '.$this->workdir()], $server, false); + instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); } } @@ -304,7 +308,7 @@ public function failedTaskLink($task_uuid) 'application_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $url = Url::fromString($route); $url = $url->withPort(null); @@ -344,7 +348,7 @@ public function type() public function publishDirectory(): Attribute { return Attribute::make( - set: fn ($value) => $value ? '/'.ltrim($value, '/') : null, + set: fn($value) => $value ? '/' . ltrim($value, '/') : null, ); } @@ -426,7 +430,7 @@ public function gitCommitLink($link): string $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); $url = $url->withUserInfo(''); - $url = $url->withPath($url->getPath().'/commits/'.$link); + $url = $url->withPath($url->getPath() . '/commits/' . $link); return $url->__toString(); } @@ -479,21 +483,21 @@ public function dockerComposeLocation(): Attribute public function baseDirectory(): Attribute { return Attribute::make( - set: fn ($value) => '/'.ltrim($value, '/'), + set: fn($value) => '/' . ltrim($value, '/'), ); } public function portsMappings(): Attribute { return Attribute::make( - set: fn ($value) => $value === '' ? null : $value, + set: fn($value) => $value === '' ? null : $value, ); } public function portsMappingsArray(): Attribute { return Attribute::make( - get: fn () => is_null($this->ports_mappings) + get: fn() => is_null($this->ports_mappings) ? [] : explode(',', $this->ports_mappings), @@ -610,7 +614,7 @@ public function status(): Attribute public function portsExposesArray(): Attribute { return Attribute::make( - get: fn () => is_null($this->ports_exposes) + get: fn() => is_null($this->ports_exposes) ? [] : explode(',', $this->ports_exposes) ); @@ -710,6 +714,11 @@ public function previews() return $this->hasMany(ApplicationPreview::class); } + public function deployment_queue() + { + return $this->hasMany(ApplicationDeploymentQueue::class); + } + public function destination() { return $this->morphTo(); @@ -822,7 +831,7 @@ public function isHealthcheckDisabled(): bool public function workdir() { - return application_configuration_dir()."/{$this->uuid}"; + return application_configuration_dir() . "/{$this->uuid}"; } public function isLogDrainEnabled() @@ -832,7 +841,7 @@ public function isLogDrainEnabled() public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect; + $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -886,7 +895,7 @@ public function generateBaseDir(string $uuid) public function dirOnServer() { - return application_configuration_dir()."/{$this->uuid}"; + return application_configuration_dir() . "/{$this->uuid}"; } public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) @@ -1010,7 +1019,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1018,14 +1027,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); } } @@ -1054,7 +1063,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1062,14 +1071,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit); } } @@ -1122,7 +1131,7 @@ public function oldRawParser() } if ($source->startsWith('.')) { $source = $source->after('.'); - $source = $workdir.$source; + $source = $workdir . $source; } $commands->push("mkdir -p $source > /dev/null 2>&1 || true"); } @@ -1133,7 +1142,7 @@ public function oldRawParser() $labels->push('coolify.managed=true'); } if (! $labels->contains('coolify.applicationId')) { - $labels->push('coolify.applicationId='.$this->id); + $labels->push('coolify.applicationId=' . $this->id); } if (! $labels->contains('coolify.type')) { $labels->push('coolify.type=application'); @@ -1150,7 +1159,7 @@ public function oldRawParser() public function parse(int $pull_request_id = 0, ?int $preview_id = null) { - if ($this->compose_parsing_version === '3') { + if ((int) $this->compose_parsing_version >= 3) { return newParser($this, $pull_request_id, $preview_id); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); @@ -1255,7 +1264,7 @@ public function parseContainerLabels(?ApplicationPreview $preview = null) public function fqdns(): Attribute { return Attribute::make( - get: fn () => is_null($this->fqdn) + get: fn() => is_null($this->fqdn) ? [] : explode(',', $this->fqdn), ); @@ -1316,10 +1325,10 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false continue; } if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) { - $healthcheckCommand .= ' '.trim($trimmedLine, '\\ '); + $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ '); } if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) { - $healthcheckCommand .= ' '.$trimmedLine; + $healthcheckCommand .= ' ' . $trimmedLine; break; } } @@ -1391,13 +1400,21 @@ public static function getDomainsByUuid(string $uuid): array 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); + if (isDev() && $server->id === 0) { + $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from"); + if ($process->failed()) { + throw new \Exception($process->errorOutput()); + } + $metrics = $process->output(); + } else { + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + } if (str($metrics)->contains('error')) { $error = json_decode($metrics, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -1406,17 +1423,106 @@ public function getMetrics(int $mins = 5) } 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]; - }); + $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; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + if (isDev() && $server->id === 0) { + $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from"); + if ($process->failed()) { + throw new \Exception($process->errorOutput()); + } + $metrics = $process->output(); + } else { + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + } + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + logger($metric); + return [(int)$metric['time'], (float)$metric['used']]; + }); + return $parsedCollection->toArray(); + } + } + + public function generateConfig($is_json = false) + { + $config = collect([]); + if ($this->build_pack = 'nixpacks') { + $config = collect([ + 'build_pack' => 'nixpacks', + 'docker_registry_image_name' => $this->docker_registry_image_name, + 'docker_registry_image_tag' => $this->docker_registry_image_tag, + 'install_command' => $this->install_command, + 'build_command' => $this->build_command, + 'start_command' => $this->start_command, + 'base_directory' => $this->base_directory, + 'publish_directory' => $this->publish_directory, + 'custom_docker_run_options' => $this->custom_docker_run_options, + 'ports_exposes' => $this->ports_exposes, + 'ports_mappings' => $this->ports_mapping, + 'settings' => collect([ + 'is_static' => $this->settings->is_static, + ]), + ]); + } + $config = $config->filter(function ($value) { + return str($value)->isNotEmpty(); + }); + if ($is_json) { + return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + return $config; + } + public function setConfig($config) + { + + $config = $config; + $validator = Validator::make(['config' => $config], [ + 'config' => 'required|json', + ]); + if ($validator->fails()) { + throw new \Exception('Invalid JSON format'); + } + $config = json_decode($config, true); + + $deepValidator = Validator::make(['config' => $config], [ + 'config.build_pack' => 'required|string', + 'config.base_directory' => 'required|string', + 'config.publish_directory' => 'required|string', + 'config.ports_exposes' => 'required|string', + 'config.settings.is_static' => 'required|boolean', + ]); + if ($deepValidator->fails()) { + throw new \Exception('Invalid data'); + } + $config = $deepValidator->validated()['config']; + + try { + $settings = data_get($config, 'settings', []); + data_forget($config, 'settings'); + $this->update($config); + $this->settings()->update($settings); + } catch (\Exception $e) { + throw new \Exception('Failed to update application settings'); + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index 90d7608cc..c261c30c6 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; use OpenApi\Attributes as OA; @@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model { protected $guarded = []; + public function application(): Attribute + { + return Attribute::make( + get: fn () => Application::find($this->application_id), + ); + } + + public function server(): Attribute + { + return Attribute::make( + get: fn () => Server::find($this->server_id), + ); + } + public function setStatus(string $status) { $this->update([ diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 138775aba..531c8fa40 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() { @@ -126,15 +126,17 @@ public function realValue(): Attribute $env = $this->get_real_environment_variables($this->value, $resource); return data_get($env, 'value', $env); - if (is_string($env)) { - return $env; - } - - return $env->value; } ); } + protected function isReallyRequired(): Attribute + { + return Attribute::make( + get: fn () => $this->is_required && str($this->real_value)->isEmpty(), + ); + } + protected function isShared(): Attribute { return Attribute::make( diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 27a181ee4..3ee142050 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -21,6 +21,7 @@ class InstanceSettings extends Model implements SendsEmail 'is_auto_update_enabled' => 'boolean', 'auto_update_frequency' => 'string', 'update_check_frequency' => 'string', + 'sentinel_token' => 'encrypted', ]; public function fqdn(): Attribute @@ -85,4 +86,17 @@ public function getTitleDisplayName(): string return "[{$instanceName}]"; } + + public function helperVersion(): Attribute + { + return Attribute::make( + get: function ($value) { + if (isDev()) { + return 'latest'; + } + + return $value; + } + ); + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index 18481751c..5a9dd964a 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -24,9 +24,11 @@ class Project extends BaseModel { protected $guarded = []; + protected $appends = ['default_environment']; + public static function ownedByCurrentTeam() { - return Project::whereTeamId(currentTeam()->id)->orderBy('name'); + return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)'); } protected static function booted() @@ -131,7 +133,7 @@ public function databases() return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } - public function default_environment() + public function getDefaultEnvironmentAttribute() { $default = $this->environments()->where('name', 'production')->first(); if ($default) { diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 4c7faaa6f..a432a6e9c 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -40,6 +40,16 @@ public function awsUrl() return "{$this->endpoint}/{$this->bucket}"; } + public function isHetzner() + { + return str($this->endpoint)->contains('your-objectstorage.com'); + } + + public function isDigitalOcean() + { + return str($this->endpoint)->contains('digitaloceanspaces.com'); + } + public function testConnection(bool $shouldSave = false) { try { diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index ce5d3a87f..3921e32e4 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -39,13 +39,19 @@ public function get_last_days_backup_status($days = 7) 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; } } + return null; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index adc8aa7e4..3639d9263 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -7,6 +7,7 @@ use App\Jobs\PullSentinelImageJob; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; @@ -16,6 +17,7 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; +use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; #[OA\Schema( @@ -36,6 +38,8 @@ 'validation_logs' => ['type' => 'string'], 'log_drain_notification_sent' => ['type' => 'boolean'], 'swarm_cluster' => ['type' => 'string'], + 'delete_unused_volumes' => ['type' => 'boolean'], + 'delete_unused_networks' => ['type' => 'boolean'], ] )] @@ -105,6 +109,8 @@ protected static function booted() 'proxy' => SchemalessAttributes::class, 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', + 'delete_unused_volumes' => 'boolean', + 'delete_unused_networks' => 'boolean', ]; protected $schemalessAttributes = [ @@ -162,7 +168,7 @@ public function proxySet() public function setupDefault404Redirect() { - $dynamic_conf_path = $this->proxyPath().'/dynamic'; + $dynamic_conf_path = $this->proxyPath() . '/dynamic'; $proxy_type = $this->proxyType(); $redirect_url = $this->proxy->redirect_url; if ($proxy_type === ProxyTypes::TRAEFIK->value) { @@ -176,8 +182,8 @@ public function setupDefault404Redirect() respond 404 }'; $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . $conf; $base64 = base64_encode($conf); instant_remote_process([ @@ -205,10 +211,13 @@ public function setupDefault404Redirect() 1 => 'https', ], 'service' => 'noop', - 'rule' => 'HostRegexp(`{catchall:.*}`)', + 'rule' => 'HostRegexp(`.+`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], 'priority' => 1, 'middlewares' => [ - 0 => 'redirect-regexp@file', + 0 => 'redirect-regexp', ], ], ], @@ -236,8 +245,8 @@ public function setupDefault404Redirect() ]; $conf = Yaml::dump($dynamic_conf, 12, 2); $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . $conf; $base64 = base64_encode($conf); @@ -246,8 +255,8 @@ public function setupDefault404Redirect() redir $redirect_url }"; $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . $conf; $base64 = base64_encode($conf); } @@ -264,8 +273,8 @@ public function setupDefault404Redirect() public function setupDynamicProxyConfiguration() { - $settings = \App\Models\InstanceSettings::get(); - $dynamic_config_path = $this->proxyPath().'/dynamic'; + $settings = instanceSettings(); + $dynamic_config_path = $this->proxyPath() . '/dynamic'; if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { $file = "$dynamic_config_path/coolify.yaml"; if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) { @@ -384,8 +393,8 @@ public function setupDynamicProxyConfiguration() } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". + "# This file is automatically generated by Coolify.\n" . + "# Do not edit it manually (only if you know what are you doing).\n\n" . $yaml; $base64 = base64_encode($yaml); @@ -444,11 +453,19 @@ public function proxyPath() // Should move everything except /caddy and /nginx to /traefik // The code needs to be modified as well, so maybe it does not worth it if ($proxyType === ProxyTypes::TRAEFIK->value) { - $proxy_path = $proxy_path; + // Do nothing } elseif ($proxyType === ProxyTypes::CADDY->value) { - $proxy_path = $proxy_path.'/caddy'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'; + } else { + $proxy_path = $proxy_path . '/caddy'; + } } elseif ($proxyType === ProxyTypes::NGINX->value) { - $proxy_path = $proxy_path.'/nginx'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; + } else { + $proxy_path = $proxy_path . '/nginx'; + } } return $proxy_path; @@ -456,15 +473,6 @@ public function proxyPath() public function proxyType() { - // $proxyType = $this->proxy->get('type'); - // if ($proxyType === ProxyTypes::NONE->value) { - // return $proxyType; - // } - // if (is_null($proxyType)) { - // $this->proxy->type = ProxyTypes::TRAEFIK->value; - // $this->proxy->status = ProxyStatus::EXITED->value; - // $this->save(); - // } return data_get($this->proxy, 'type'); } @@ -519,6 +527,45 @@ public function forceDisableServer() Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function generateSentinelUrl() { + if ($this->isLocalhost()) { + return 'http://host.docker.internal:8000'; + } + $settings = InstanceSettings::get(); + if ($settings->fqdn) { + return $settings->fqdn; + } + if ($settings->ipv4) { + return $settings->ipv4 . ':8000'; + } + if ($settings->ipv6) { + return $settings->ipv6 . ':8000'; + } + return null; + } + public function generateSentinelToken() + { + $data = [ + 'server_uuid' => $this->uuid, + ]; + $token = json_encode($data); + $encrypted = encrypt($token); + $this->settings->sentinel_token = $encrypted; + $this->settings->save(); + + return $encrypted; + } + + public function sentinelUpdateAt(bool $isReset = false) + { + $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now(); + $this->save(); + } + public function isSentinelLive() + { + return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4)); + } + public function isSentinelEnabled() { return $this->isMetricsEnabled() || $this->isServerApiEnabled(); @@ -531,7 +578,7 @@ public function isMetricsEnabled() public function isServerApiEnabled() { - return $this->settings->is_server_api_enabled; + return $this->settings->is_sentinel_enabled; } public function checkServerApi() @@ -549,7 +596,6 @@ public function checkServerApi() ray($process->exitCode(), $process->output(), $process->errorOutput()); throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); } - } } @@ -573,7 +619,15 @@ 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); + if (isDev() && $this->id === 0) { + $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from"); + if ($process->failed()) { + throw new \Exception($process->errorOutput()); + } + $cpu = $process->output(); + } else { + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + } if (str($cpu)->contains('error')) { $error = json_decode($cpu, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -582,17 +636,12 @@ public function getCpuMetrics(int $mins = 5) } 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; - return $parsedCollection->toArray(); } } @@ -600,7 +649,15 @@ public function getMemoryMetrics(int $mins = 5) { 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); + if (isDev() && $this->id === 0) { + $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from"); + if ($process->failed()) { + throw new \Exception($process->errorOutput()); + } + $memory = $process->output(); + } else { + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + } if (str($memory)->contains('error')) { $error = json_decode($memory, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); @@ -609,14 +666,9 @@ public function getMemoryMetrics(int $mins = 5) } 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(); @@ -971,7 +1023,8 @@ public function team() 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; } @@ -1035,6 +1088,37 @@ public function isSwarmWorker() return data_get($this, 'settings.is_swarm_worker'); } + public function status(): bool + { + ['uptime' => $uptime] = $this->validateConnection(false); + if ($uptime) { + if ($this->unreachable_notification_sent === true) { + $this->update(['unreachable_notification_sent' => false]); + } + } else { + // $this->server->team?->notify(new Unreachable($this->server)); + foreach ($this->applications as $application) { + $application->update(['status' => 'exited']); + } + foreach ($this->databases as $database) { + $database->update(['status' => 'exited']); + } + foreach ($this->services as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->update(['status' => 'exited']); + } + foreach ($dbs as $db) { + $db->update(['status' => 'exited']); + } + } + + return false; + } + + return true; + } public function validateConnection($isManualCheck = true) { config()->set('constants.ssh.mux_enabled', ! $isManualCheck); @@ -1048,6 +1132,10 @@ public function validateConnection($isManualCheck = true) return ['uptime' => false, 'error' => 'Server skipped.']; } try { + // Make sure the private key is stored + if ($server->privateKey) { + $server->privateKey->storeInFileSystem(); + } instant_remote_process(['ls /'], $server); $server->settings()->update([ 'is_reachable' => true, @@ -1204,4 +1292,18 @@ public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null return $this; } + + public function storageCheck(): ?string + { + $commands = [ + 'df / --output=pcent | tr -cd 0-9', + ]; + + return instant_remote_process($commands, $this, false); + } + + public function isIpv6(): bool + { + return str($this->ip)->contains(':'); + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index c44a393b4..f2eba4854 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -24,7 +24,7 @@ '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 @@ '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,8 @@ class ServerSetting extends Model protected $casts = [ 'force_docker_cleanup' => 'boolean', 'docker_cleanup_threshold' => 'integer', + 'sentinel_token' => 'encrypted', ]; - public function server() { return $this->belongsTo(Server::class); diff --git a/app/Models/Service.php b/app/Models/Service.php index d236869ba..16e11ecb6 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -42,7 +42,7 @@ class Service extends BaseModel { use HasFactory, SoftDeletes; - private static $parserVersion = '3'; + private static $parserVersion = '4'; protected $guarded = []; @@ -283,9 +283,162 @@ public function extraFields() $fields = collect([]); $applications = $this->applications()->get(); foreach ($applications as $application) { - $image = str($application->image)->before(':')->value(); + $image = str($application->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)?->contains('rabbitmq'): + 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(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => 'LABEL_STUDIO_USERNAME', + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Label Studio', $data->toArray()); + break; + case $image->contains('litellm'): + $data = collect([]); + $username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Litellm', $data->toArray()); + break; + case $image->contains('langfuse'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first(); + if ($email) { + $data = $data->merge([ + 'Admin 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' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Langfuse', $data->toArray()); + break; + case $image->contains('invoiceninja'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first(); + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($email, 'key'), + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Invoice Ninja', $data->toArray()); + break; + case $image->contains('argilla'): + $data = collect([]); + $api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first(); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first(); + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Argilla', $data->toArray()); + break; + case $image->contains('rabbitmq'): $data = collect([]); $host_port = $this->environment_variables()->where('key', 'PORT')->first(); $username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first(); @@ -320,7 +473,7 @@ public function extraFields() } $fields->put('RabbitMQ', $data->toArray()); break; - case str($image)?->contains('tolgee'): + case $image->contains('tolgee'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first(); $data = $data->merge([ @@ -334,7 +487,7 @@ public function extraFields() 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, @@ -343,7 +496,7 @@ public function extraFields() } $fields->put('Tolgee', $data->toArray()); break; - case str($image)?->contains('logto'): + case $image->contains('logto'): $data = collect([]); $logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first(); $logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); @@ -367,7 +520,7 @@ public function extraFields() } $fields->put('Logto', $data->toArray()); break; - case str($image)?->contains('unleash-server'): + case $image->contains('unleash-server'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first(); $data = $data->merge([ @@ -381,7 +534,7 @@ public function extraFields() 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, @@ -390,7 +543,7 @@ public function extraFields() } $fields->put('Unleash', $data->toArray()); break; - case str($image)?->contains('grafana'): + case $image->contains('grafana'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first(); $data = $data->merge([ @@ -404,7 +557,7 @@ public function extraFields() 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, @@ -413,7 +566,7 @@ public function extraFields() } $fields->put('Grafana', $data->toArray()); break; - case str($image)?->contains('directus'): + case $image->contains('directus'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -439,7 +592,7 @@ public function extraFields() } $fields->put('Directus', $data->toArray()); break; - case str($image)?->contains('kong'): + case $image->contains('kong'): $data = collect([]); $dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); $dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -463,7 +616,7 @@ public function extraFields() ]); } $fields->put('Supabase', $data->toArray()); - case str($image)?->contains('minio'): + case $image->contains('minio'): $data = collect([]); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first(); @@ -516,7 +669,7 @@ public function extraFields() $fields->put('MinIO', $data->toArray()); break; - case str($image)?->contains('weblate'): + case $image->contains('weblate'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first(); @@ -542,7 +695,7 @@ public function extraFields() } $fields->put('Weblate', $data->toArray()); break; - case str($image)?->contains('meilisearch'): + case $image->contains('meilisearch'): $data = collect([]); $SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first(); if ($SERVICE_PASSWORD_MEILISEARCH) { @@ -556,7 +709,7 @@ public function extraFields() } $fields->put('Meilisearch', $data->toArray()); break; - case str($image)?->contains('ghost'): + case $image->contains('ghost'): $data = collect([]); $MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first(); $MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first(); @@ -616,45 +769,8 @@ public function extraFields() $fields->put('Ghost', $data->toArray()); break; - default: - $data = collect([]); - $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); - // Chaskiq - $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); - $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); - if ($admin_user) { - $data = $data->merge([ - 'User' => [ - 'key' => 'SERVICE_USER_ADMIN', - 'value' => data_get($admin_user, 'value', 'admin'), - 'readonly' => true, - 'rules' => 'required', - ], - ]); - } - if ($admin_password) { - $data = $data->merge([ - 'Password' => [ - 'key' => 'SERVICE_PASSWORD_ADMIN', - 'value' => data_get($admin_password, 'value'), - 'rules' => 'required', - 'isPassword' => true, - ], - ]); - } - if ($admin_email) { - $data = $data->merge([ - 'Email' => [ - 'key' => 'ADMIN_EMAIL', - 'value' => data_get($admin_email, 'value'), - 'rules' => 'required|email', - ], - ]); - } - $fields->put('Admin', $data->toArray()); - break; - case str($image)?->contains('vaultwarden'): + case $image->contains('vaultwarden'): $data = collect([]); $DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first(); @@ -720,7 +836,7 @@ public function extraFields() $fields->put('Vaultwarden', $data); break; - case str($image)->contains('gitlab/gitlab'): + case $image->contains('gitlab/gitlab'): $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first(); $data = collect([]); if ($password) { @@ -744,7 +860,7 @@ public function extraFields() $fields->put('GitLab', $data->toArray()); break; - case str($image)->contains('code-server'): + case $image->contains('code-server'): $data = collect([]); $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first(); if ($password) { @@ -770,14 +886,78 @@ public function extraFields() } $fields->put('Code Server', $data->toArray()); break; + case $image->contains('elestio/strapi'): + $data = collect([]); + $license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first(); + if ($license) { + $data = $data->merge([ + 'License' => [ + 'key' => data_get($license, 'key'), + 'value' => data_get($license, 'value'), + ], + ]); + } + $nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first(); + if ($nodeEnv) { + $data = $data->merge([ + 'Node Environment' => [ + 'key' => data_get($nodeEnv, 'key'), + 'value' => data_get($nodeEnv, 'value'), + ], + ]); + } + + $fields->put('Strapi', $data->toArray()); + break; + default: + $data = collect([]); + $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); + // Chaskiq + $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); + + $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); + if ($admin_user) { + $data = $data->merge([ + 'User' => [ + 'key' => data_get($admin_user, 'key'), + 'value' => data_get($admin_user, 'value', 'admin'), + 'readonly' => true, + 'rules' => 'required', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($admin_password, 'key'), + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + if ($admin_email) { + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($admin_email, 'key'), + 'value' => data_get($admin_email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + $fields->put('Admin', $data->toArray()); + break; } } $databases = $this->databases()->get(); foreach ($databases as $database) { - $image = str($database->image)->before(':')->value(); + $image = str($database->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)->contains('postgres'): + case $image->contains('postgres'): $userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL']; $passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL']; $dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB']; @@ -815,10 +995,10 @@ public function extraFields() } $fields->put('PostgreSQL', $data->toArray()); break; - case str($image)->contains('mysql'): + 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(); @@ -865,7 +1045,7 @@ public function extraFields() } $fields->put('MySQL', $data->toArray()); break; - case str($image)->contains('mariadb'): + case $image->contains('mariadb'): $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER']; $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD']; $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD']; @@ -928,6 +1108,7 @@ public function saveExtraFields($fields) foreach ($fields as $field) { $key = data_get($field, 'key'); $value = data_get($field, 'value'); + ray($key, $value); $found = $this->environment_variables()->where('key', $key)->first(); if ($found) { $found->value = $value; @@ -1051,13 +1232,12 @@ public function scheduled_tasks(): HasMany public function environment_variables(): HasMany { - - return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC"); + return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function environment_variables_preview(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); + return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function workdir() @@ -1095,7 +1275,21 @@ public function saveComposeConfigs() return 3; }); foreach ($sorted as $env) { - $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + if (version_compare($env->version, '4.0.0-beta.347', '<=')) { + $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + } else { + $real_value = $env->real_value; + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->real_value; + } else { + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + } + $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; + } } if ($sorted->count() === 0) { $commands[] = 'touch .env'; @@ -1105,7 +1299,7 @@ public function saveComposeConfigs() public function parse(bool $isNew = false): Collection { - if ($this->compose_parsing_version === '3') { + if ((int) $this->compose_parsing_version >= 3) { return newParser($this); } elseif ($this->docker_compose_raw) { return parseDockerComposeFile($this, $isNew); @@ -1121,4 +1315,20 @@ public function networks() return $networks; } + + protected function isDeployable(): Attribute + { + return Attribute::make( + get: function () { + $envs = $this->environment_variables()->where('is_required', true)->get(); + foreach ($envs as $env) { + if ($env->is_really_required) { + return false; + } + } + return true; + } + ); + } + } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index d312fab96..0e79e1e2e 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -112,4 +112,9 @@ public function getFilesFromServer(bool $isInit = false) { getFilesystemVolumesFromServer($this, $isInit); } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 6b96738e8..927527118 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -115,4 +115,13 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function isBackupSolutionAvailable() + { + return str($this->databaseType())->contains('mysql') || + str($this->databaseType())->contains('postgres') || + str($this->databaseType())->contains('postgis') || + str($this->databaseType())->contains('mariadb') || + str($this->databaseType())->contains('mongodb'); + } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index ee5c3becc..6274f51b2 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 361abf110..3555e7afd 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index e05879371..4725ca533 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index c1e6c85d7..8f1a2c1ee 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index e5ed0a5f4..41b2ce9eb 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -292,7 +292,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -314,4 +314,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index bd4a7abb7..da2ac070f 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -273,7 +273,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -295,4 +295,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index db771c7cd..e0f42269d 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -274,7 +274,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -296,4 +296,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ba582733c..e71eacc45 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -287,7 +287,7 @@ public function getMetrics(int $mins = 5) $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}/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?'); @@ -309,4 +309,9 @@ public function getMetrics(int $mins = 5) return $parsedCollection->toArray(); } } -} \ No newline at end of file + public function isBackupSolutionAvailable() + { + return false; + } +} + diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 549fc6cd3..cc7d76ebf 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -13,7 +13,7 @@ class TransactionalEmailChannel { public function send(User $notifiable, Notification $notification): void { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) { Log::info('SMTP/Resend not enabled'); diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 8b1c02d39..3938a8da7 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -18,7 +18,7 @@ class ResetPassword extends Notification public function __construct($token) { - $this->settings = \App\Models\InstanceSettings::get(); + $this->settings = instanceSettings(); $this->token = $token; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index cd90918ad..8b4c2eef2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,10 +2,8 @@ namespace App\Providers; -use App\Models\InstanceSettings; use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Laravel\Sanctum\Sanctum; @@ -30,9 +28,5 @@ public function boot(): void ])->baseUrl($api_url); } }); - // if (! env('CI')) { - // View::share('instanceSettings', InstanceSettings::get()); - // } - } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 53a2e9281..b916b6234 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -46,7 +46,7 @@ public function boot(): void Fortify::registerView(function () { $isFirstUser = User::count() === 0; - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { return redirect()->route('login'); } @@ -60,7 +60,7 @@ public function boot(): void }); Fortify::loginView(function () { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $enabled_oauth_providers = OauthSetting::where('enabled', true)->get(); $users = User::count(); if ($users == 0) { diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 6c9378cac..fbd7b0b15 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -22,6 +22,7 @@ public function __construct( public bool $allowToPeak = true, public bool $isMultiline = false, public string $defaultClass = 'input', + public string $autocomplete = 'off', ) {} public function render(): View|Closure|string diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 8e14ef9ee..d7c16b607 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -2,6 +2,7 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; +use App\Enums\StaticImageTypes; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; use Illuminate\Validation\Rule; @@ -89,6 +90,7 @@ function sharedDataApplications() 'git_branch' => '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', @@ -175,4 +177,6 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('instant_deploy'); $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 1eeec8f94..303fcab8e 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -20,12 +20,16 @@ const DATABASE_DOCKER_IMAGES = [ 'bitnami/mariadb', 'bitnami/mongodb', - 'bitnami/mysql', - 'bitnami/postgresql', 'bitnami/redis', 'mysql', + 'bitnami/mysql', + 'mysql/mysql-server', 'mariadb', + 'postgis/postgis', 'postgres', + 'bitnami/postgresql', + 'supabase/postgres', + 'elestio/postgres', 'mongo', 'redis', 'memcached', @@ -33,10 +37,10 @@ 'neo4j', 'influxdb', 'clickhouse/clickhouse-server', - 'supabase/postgres', ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', + 'minio/minio', 'svhd/logto', ]; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index e252bda10..397bce029 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -325,38 +325,20 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); - $basic_auth = false; - $basic_auth_middleware = null; - $redirect = false; - $redirect_middleware = null; + $middlewares_from_labels = collect([]); if ($serviceLabels) { - $basic_auth = $serviceLabels->contains(function ($value) { - return str_contains($value, 'basicauth'); - }); - if ($basic_auth) { - $basic_auth_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } - $redirect = $serviceLabels->contains(function ($value) { - return str_contains($value, 'redirectregex'); - }); - if ($redirect) { - $redirect_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } + $middlewares_from_labels = $serviceLabels->map(function ($item) { + if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) { + return $matches[1]; + } + if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { + return explode(',', $matches[1]); + } + return null; + })->flatten() + ->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { @@ -404,20 +386,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); } 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"); } if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -425,10 +402,13 @@ 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); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -437,13 +417,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_gzip_enabled) { $middlewares->push('gzip'); - } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } + } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -455,6 +429,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -490,12 +467,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -507,6 +478,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -516,12 +490,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -533,6 +501,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index c4c15b8fe..e2693a2cd 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server) "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to $network network.'", + "echo 'Proxy started and configured successfully!'", ]; }); } else { @@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server) "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to $network network.'", + "echo 'Proxy started and configured successfully!'", ]; }); } @@ -144,6 +148,7 @@ function generate_default_proxy_configuration(Server $server) 'traefik.http.routers.traefik.service=api@internal', 'traefik.http.services.traefik.loadbalancer.server.port=8080', 'coolify.managed=true', + 'coolify.proxy=true', ]; $config = [ 'networks' => $array_of_networks->toArray(), @@ -159,6 +164,7 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', '8080:8080', ], 'healthcheck' => [ @@ -182,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', @@ -217,7 +224,6 @@ function generate_default_proxy_configuration(Server $server) } } elseif ($proxy_type === 'CADDY') { $config = [ - 'version' => '3.8', 'networks' => $array_of_networks->toArray(), 'services' => [ 'caddy' => [ @@ -235,13 +241,11 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', + ], + 'labels' => [ + 'coolify.managed=true', ], - // "healthcheck" => [ - // "test" => "wget -qO- http://localhost:80|| exit 1", - // "interval" => "4s", - // "timeout" => "2s", - // "retries" => 5, - // ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', "{$proxy_path}/dynamic:/dynamic", diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php index 4a2252016..2ee7bf44a 100644 --- a/bootstrap/helpers/s3.php +++ b/bootstrap/helpers/s3.php @@ -1,14 +1,11 @@ endpoint) { - $is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com'); - } + config()->set('filesystems.disks.custom-s3', [ 'driver' => 's3', 'region' => $s3['region'], @@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3) 'bucket' => $s3['bucket'], 'endpoint' => $s3['endpoint'], 'use_path_style_endpoint' => true, - 'bucket_endpoint' => $is_digital_ocean, + 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(), 'aws_url' => $s3->awsUrl(), ]); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 350f16837..f70c705c7 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -164,10 +164,10 @@ 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()); @@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { if (! $settings) { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); @@ -281,7 +281,7 @@ function base_ip(): string if (isDev()) { return 'localhost'; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->public_ipv4) { return "$settings->public_ipv4"; } @@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn) */ function base_url(bool $withPort = true): string { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->fqdn) { return $settings->fqdn; } @@ -343,6 +343,11 @@ function isSubscribed() { return isSubscriptionActive() || auth()->user()->isInstanceAdmin(); } + +function isProduction(): bool +{ + return ! isDev(); +} function isDev(): bool { return config('app.env') === 'local'; @@ -384,7 +389,7 @@ function send_internal_notification(string $message): void } function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); if (! $type) { throw new Exception('No email settings found.'); @@ -517,6 +522,11 @@ function sslip(Server $server) function get_service_templates(bool $force = false): Collection { + if (isDev()) { + $services = File::get(base_path('templates/service-templates.json')); + + return collect(json_decode($services))->sortKeys(); + } if ($force) { try { $response = Http::retry(3, 1000)->get(config('constants.services.official')); @@ -703,7 +713,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -753,7 +765,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -819,6 +833,31 @@ function convertToArray($collection) return $collection; } +function parseCommandFromMagicEnvVariable(Str|string $key): Stringable +{ + $value = str($key); + $count = substr_count($value->value(), '_'); + if ($count === 2) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } else { + // SERVICE_BASE64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + if ($count === 3) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI_1000 + $command = $value->after('SERVICE_')->before('_'); + } else { + // SERVICE_BASE64_64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + + return str($command); +} function parseEnvVariable(Str|string $value) { $value = str($value); @@ -850,6 +889,7 @@ function parseEnvVariable(Str|string $value) } else { // SERVICE_BASE64_64_UMAMI $command = $value->after('SERVICE_')->beforeLast('_'); + ray($command); } } } @@ -970,7 +1010,7 @@ function validate_dns_entry(string $fqdn, Server $server) if (str($host)->contains('sslip.io')) { return true; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); if (! $is_dns_validation_enabled) { return true; @@ -1090,7 +1130,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = if ($domainFound) { return true; } - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $domain = data_get($settings, 'fqdn'); if (str($domain)->endsWith('/')) { @@ -1134,10 +1174,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}"); } } } @@ -1153,16 +1193,16 @@ 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}"); } } } } if ($resource) { - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (data_get($settings, 'fqdn')) { $domain = data_get($settings, 'fqdn'); if (str($domain)->endsWith('/')) { @@ -1179,12 +1219,26 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null function parseCommandsByLineForSudo(Collection $commands, Server $server): array { $commands = $commands->map(function ($line) { - if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) { + if ( + ! str(trim($line))->startsWith([ + 'cd', + 'command', + 'echo', + 'true', + 'if', + 'fi', + ]) + ) { return "sudo $line"; } + if (str(trim($line))->startsWith('if')) { + return str_replace('if', 'if sudo', $line); + } + return $line; }); + $commands = $commands->map(function ($line) use ($server) { if (Str::startsWith($line, 'sudo mkdir -p')) { return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); @@ -1192,6 +1246,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array return $line; }); + $commands = $commands->map(function ($line) { $line = str($line); if (str($line)->contains('$(')) { @@ -1236,8 +1291,6 @@ function parseLineForSudo(string $command, Server $server): string function get_public_ips() { try { - echo "Refreshing public ips!\n"; - $settings = \App\Models\InstanceSettings::get(); [$first, $second] = Process::concurrently(function (Pool $pool) { $pool->path(__DIR__)->command('curl -4s https://ifconfig.io'); $pool->path(__DIR__)->command('curl -6s https://ifconfig.io'); @@ -1251,7 +1304,7 @@ function get_public_ips() return; } - $settings->update(['public_ipv4' => $ipv4]); + InstanceSettings::get()->update(['public_ipv4' => $ipv4]); } } catch (\Exception $e) { echo "Error: {$e->getMessage()}\n"; @@ -1266,7 +1319,7 @@ function get_public_ips() return; } - $settings->update(['public_ipv6' => $ipv6]); + InstanceSettings::get()->update(['public_ipv6' => $ipv6]); } } catch (\Throwable $e) { echo "Error: {$e->getMessage()}\n"; @@ -1285,13 +1338,6 @@ function isAnyDeploymentInprogress() exit(0); } -function generateSentinelToken() -{ - $token = Str::random(64); - - return $token; -} - function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; @@ -1590,7 +1636,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -2505,7 +2553,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -2966,11 +3016,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $predefinedPort = '8000'; } if ($isDatabase) { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, @@ -3080,7 +3141,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int foreach ($magicEnvironments as $key => $value) { $key = str($key); $value = replaceVariables($value); - $command = $key->after('SERVICE_')->before('_'); + $command = parseCommandFromMagicEnvVariable($key); $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first(); if ($found) { continue; @@ -3113,6 +3174,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, @@ -3191,12 +3253,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($serviceName === 'plausible') { $predefinedPort = '8000'; } + if ($isDatabase) { - $savedService = ServiceDatabase::firstOrCreate([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, @@ -3266,7 +3340,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { $volume = $source->value().':'.$target->value(); } else { - $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } elseif ($isService) { + $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + } + } else { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } $source = replaceLocalSource($source, $mainDirectory); if ($isApplication && $isPullRequest) { $source = $source."-pr-$pullRequestId"; @@ -3286,6 +3368,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'resource_type' => get_class($originalResource), ] ); + if (isDev()) { + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } elseif ($isService) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); + } + } else { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } + } $volume = "$source:$target"; } } elseif ($type->value() === 'volume') { @@ -3469,6 +3562,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(':'); @@ -3483,11 +3577,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 @@ -3498,6 +3594,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; @@ -3511,6 +3608,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'value' => $value, 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); } @@ -3608,6 +3706,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int }); } $serviceLabels = $labels->merge($defaultLabels); + if ($serviceLabels->count() > 0) { + if ($isApplication) { + $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); + } else { + $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); + } + if ($isContainerLabelEscapeEnabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if ($isApplication) { $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; @@ -3784,6 +3894,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 { @@ -3828,14 +3940,37 @@ function convertComposeEnvironmentToArray($environment) { $convertedServiceVariables = collect([]); if (isAssociativeArray($environment)) { + // Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux']; + if ($environment instanceof Collection) { + $changedEnvironment = collect([]); + $environment->each(function ($value, $key) use ($changedEnvironment) { + if (is_numeric($key)) { + $parts = explode('=', $value, 2); + if (count($parts) === 2) { + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + $changedEnvironment->put($key, $realValue); + } else { + $changedEnvironment->put($key, $value); + } + } else { + $changedEnvironment->put($key, $value); + } + }); + + return $changedEnvironment; + } $convertedServiceVariables = $environment; } else { + // Example: $environment = ['FOO=bar', 'BAZ=qux']; foreach ($environment as $value) { - $parts = explode('=', $value, 2); - $key = $parts[0]; - $realValue = $parts[1] ?? ''; - if ($key) { - $convertedServiceVariables->put($key, $realValue); + if (is_string($value)) { + $parts = explode('=', $value, 2); + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + if ($key) { + $convertedServiceVariables->put($key, $realValue); + } } } } @@ -3843,3 +3978,35 @@ function convertComposeEnvironmentToArray($environment) return $convertedServiceVariables; } +function instanceSettings() +{ + return InstanceSettings::get(); +} + +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) { + return; + } + $uuid = new Cuid2(); + $cloneCommand = "git clone --no-checkout -b $branch $repository ."; + $workdir = rtrim($base_directory, '/'); + $fileList = collect([".$workdir/coolify.json"]); + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir/coolify.json", + 'rm -rf /tmp/{$uuid}', + ]); + try { + return instant_remote_process($commands, $server); + } catch (\Exception $e) { + // continue + } +} diff --git a/composer.json b/composer.json index 17432c532..fbd77d0cf 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "guzzlehttp/guzzle": "^7.5.0", "laravel/fortify": "^v1.16.0", "laravel/framework": "^v11", - "laravel/horizon": "^5.27.1", + "laravel/horizon": "^5.29.1", "laravel/prompts": "^0.1.6", "laravel/sanctum": "^v4.0", "laravel/socialite": "^v5.14.0", @@ -48,6 +48,7 @@ "zircote/swagger-php": "^4.10" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.13", "fakerphp/faker": "^v1.21.0", "laravel/dusk": "^v8.0", "laravel/pint": "^1.16", diff --git a/composer.lock b/composer.lock index fffb320d3..0b8da82d0 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": "96f8146407d0e6e897ff097c5eccd3a4", + "content-hash": "c47adf3684eb727e22503937435c0914", "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,9 @@ ], "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/prompts", @@ -3199,16 +3204,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 +3264,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 +3325,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 +3470,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 +3530,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 +3599,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 +3656,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 +3668,7 @@ "type": "patreon" } ], - "time": "2024-04-11T23:07:54+00:00" + "time": "2024-10-08T22:06:45+00:00" }, { "name": "league/commonmark", @@ -3855,16 +3860,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 +3937,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 +3992,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 +4041,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 +4090,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 +4136,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 +4148,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 +4960,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 +5016,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 +5108,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 +5160,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 +5902,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 +5992,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 +6008,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 +6053,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 +6113,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 +6819,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 +6861,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 +7153,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 +7200,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 +7208,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 +7499,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 +7572,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 +7584,7 @@ "type": "custom" } ], - "time": "2024-08-15T19:03:01+00:00" + "time": "2024-09-19T12:58:53+00:00" }, { "name": "socialiteproviders/manager", @@ -8580,16 +8585,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 +8658,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 +8674,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 +9105,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 +9162,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 +9178,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 +9276,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 +9292,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 +9356,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 +9372,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 +9440,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 +9456,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 +10243,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 +10284,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 +10300,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 +10613,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 +10680,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 +10696,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 +10774,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 +10790,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 +10872,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 +10926,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 +10942,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 +11009,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 +11025,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 +11081,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 +11097,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 +11747,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 +11770,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,12 +11822,96 @@ ], "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.14.3", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd" + }, + "dist": { + "type": "zip", + "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.23.0", + "php": "^8.0", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7|^8|^9", + "phpunit/phpunit": "^9.6|^10.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.14-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.3" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-10-02T09:17:49+00:00" + }, { "name": "brianium/paratest", "version": "v7.4.3", @@ -12043,26 +12132,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", @@ -12102,7 +12191,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": [ { @@ -12110,7 +12199,7 @@ "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12165,16 +12254,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": { @@ -12231,22 +12320,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": { @@ -12299,7 +12388,75 @@ "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.23.2", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "689720d724c771ac4add859056744b7b3f2406da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", + "reference": "689720d724c771ac4add859056744b7b3f2406da", + "shasum": "" + }, + "require": { + "php": "^7.2|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "support": { + "issues": "https://github.com/maximebf/php-debugbar/issues", + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" + }, + "time": "2024-09-16T11:23:09+00:00" }, { "name": "mockery/mockery", @@ -14740,16 +14897,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": { @@ -14813,7 +14970,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": [ { @@ -14829,7 +14986,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 new file mode 100644 index 000000000..eae406ba7 --- /dev/null +++ b/config/debugbar.php @@ -0,0 +1,325 @@ + env('DEBUGBAR_ENABLED', null), + 'except' => [ + 'telescope*', + 'horizon*', + ], + + /* + |-------------------------------------------------------------------------- + | Storage settings + |-------------------------------------------------------------------------- + | + | DebugBar stores data for session/ajax requests. + | You can disable this, so the debugbar stores data in headers/session, + | but this can cause problems with large data collectors. + | By default, file storage (in the storage folder) is used. Redis and PDO + | can also be used. For PDO, run the package migrations first. + | + | Warning: Enabling storage.open will allow everyone to access previous + | request, do not enable open storage in publicly available environments! + | Specify a callback if you want to limit based on IP or authentication. + | Leaving it to null will allow localhost only. + */ + 'storage' => [ + 'enabled' => true, + 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. + 'driver' => 'file', // redis, file, pdo, socket, custom + 'path' => storage_path('debugbar'), // For file driver + 'connection' => null, // Leave null for default connection (Redis/PDO) + 'provider' => '', // Instance of StorageInterface for custom driver + 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver + 'port' => 2304, // Port to use with the "socket" driver + ], + + /* + |-------------------------------------------------------------------------- + | Editor + |-------------------------------------------------------------------------- + | + | Choose your preferred editor to use when clicking file name. + | + | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", + | "vscode-insiders-remote", "vscodium", "textmate", "emacs", + | "sublime", "atom", "nova", "macvim", "idea", "netbeans", + | "xdebug", "espresso" + | + */ + + 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | Remote Path Mapping + |-------------------------------------------------------------------------- + | + | If you are using a remote dev server, like Laravel Homestead, Docker, or + | even a remote VPS, it will be necessary to specify your path mapping. + | + | Leaving one, or both of these, empty or null will not trigger the remote + | URL changes and Debugbar will treat your editor links as local files. + | + | "remote_sites_path" is an absolute base path for your sites or projects + | in Homestead, Vagrant, Docker, or another remote development server. + | + | Example value: "/home/vagrant/Code" + | + | "local_sites_path" is an absolute base path for your sites or projects + | on your local computer where your IDE or code editor is running on. + | + | Example values: "/Users//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), + 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), + + /* + |-------------------------------------------------------------------------- + | Vendors + |-------------------------------------------------------------------------- + | + | Vendor files are included by default, but can be set to false. + | This can also be set to 'js' or 'css', to only include javascript or css vendor files. + | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + | and for js: jquery and highlight.js + | So if you want syntax highlighting, set it to true. + | jQuery is set to not conflict with existing jQuery scripts. + | + */ + + 'include_vendors' => true, + + /* + |-------------------------------------------------------------------------- + | Capture Ajax Requests + |-------------------------------------------------------------------------- + | + | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + | you can use this option to disable sending the data through the headers. + | + | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + | + | Note for your request to be identified as ajax requests they must either send the header + | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. + | + | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. + | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. + */ + + 'capture_ajax' => true, + 'add_ajax_timing' => false, + 'ajax_handler_auto_show' => true, + 'ajax_handler_enable_tab' => true, + + /* + |-------------------------------------------------------------------------- + | Custom Error Handler for Deprecated warnings + |-------------------------------------------------------------------------- + | + | When enabled, the Debugbar shows deprecated warnings for Symfony components + | in the Messages tab. + | + */ + 'error_handler' => false, + + /* + |-------------------------------------------------------------------------- + | Clockwork integration + |-------------------------------------------------------------------------- + | + | The Debugbar can emulate the Clockwork headers, so you can use the Chrome + | Extension, without the server-side code. It uses Debugbar collectors instead. + | + */ + 'clockwork' => false, + + /* + |-------------------------------------------------------------------------- + | DataCollectors + |-------------------------------------------------------------------------- + | + | Enable/disable DataCollectors + | + */ + + 'collectors' => [ + 'phpinfo' => true, // Php version + 'messages' => true, // Messages + 'time' => true, // Time Datalogger + 'memory' => true, // Memory usage + 'exceptions' => true, // Exception displayer + 'log' => true, // Logs from Monolog (merged in messages if enabled) + 'db' => true, // Show database (PDO) queries and bindings + 'views' => true, // Views with their data + 'route' => true, // Current route information + 'auth' => false, // Display Laravel authentication status + 'gate' => true, // Display Laravel Gate checks + 'session' => true, // Display session data + 'symfony_request' => true, // Only one can be enabled.. + 'mail' => true, // Catch mail messages + 'laravel' => false, // Laravel version and environment + 'events' => false, // All events fired + 'default_request' => false, // Regular or special Symfony request logger + 'logs' => false, // Add the latest log messages + 'files' => false, // Show the included files + 'config' => false, // Display config settings + 'cache' => false, // Display cache events + 'models' => true, // Display models + 'livewire' => true, // Display Livewire (when available) + 'jobs' => false, // Display dispatched jobs + ], + + /* + |-------------------------------------------------------------------------- + | Extra options + |-------------------------------------------------------------------------- + | + | Configure some DataCollectors + | + */ + + 'options' => [ + 'time' => [ + 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate + ], + 'messages' => [ + 'trace' => true, // Trace the origin of the debug message + ], + 'memory' => [ + 'reset_peak' => false, // run memory_reset_peak_usage before collecting + 'with_baseline' => false, // Set boot memory usage as memory peak baseline + 'precision' => 0, // Memory rounding precision + ], + 'auth' => [ + 'show_name' => true, // Also show the users name/email in the debugbar + 'show_guards' => true, // Show the guards that are used + ], + 'db' => [ + 'with_params' => true, // Render SQL with the parameters substituted + 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. + 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) + 'timeline' => false, // Add the queries to the timeline + 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute. + 'explain' => [ // Show EXPLAIN output on queries + 'enabled' => false, + 'types' => ['SELECT'], // Deprecated setting, is always only SELECT + ], + 'hints' => false, // Show hints for common mistakes + 'show_copy' => false, // Show copy button next to the query, + 'slow_threshold' => false, // Only track queries that last longer than this time in ms + 'memory_usage' => false, // Show queries memory usage + 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured + 'hard_limit' => 500, // After the hard limit, queries are ignored + ], + 'mail' => [ + 'timeline' => false, // Add mails to the timeline + 'show_body' => true, + ], + 'views' => [ + 'timeline' => false, // Add the views to the timeline (Experimental) + 'data' => false, //true for all data, 'keys' for only names, false for no parameters. + 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force + 'exclude_paths' => [ // Add the paths which you don't want to appear in the views + 'vendor/filament', // Exclude Filament components by default + ], + ], + 'route' => [ + 'label' => true, // show complete route on bar + ], + 'session' => [ + 'hiddens' => [], // hides sensitive values using array paths + ], + 'symfony_request' => [ + 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password + ], + 'events' => [ + 'data' => false, // collect events data, listeners + ], + 'logs' => [ + 'file' => null, + ], + 'cache' => [ + 'values' => true, // collect cache values + ], + ], + + /* + |-------------------------------------------------------------------------- + | Inject Debugbar in Response + |-------------------------------------------------------------------------- + | + | Usually, the debugbar is added just before , by listening to the + | Response after the App is done. If you disable this, you have to add them + | in your template yourself. See http://phpdebugbar.com/docs/rendering.html + | + */ + + 'inject' => true, + + /* + |-------------------------------------------------------------------------- + | DebugBar route prefix + |-------------------------------------------------------------------------- + | + | Sometimes you want to set route prefix to be used by DebugBar to load + | its resources from. Usually the need comes from misconfigured web server or + | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + | + */ + 'route_prefix' => '_debugbar', + + /* + |-------------------------------------------------------------------------- + | DebugBar route middleware + |-------------------------------------------------------------------------- + | + | Additional middleware to run on the Debugbar routes + */ + 'route_middleware' => [], + + /* + |-------------------------------------------------------------------------- + | DebugBar route domain + |-------------------------------------------------------------------------- + | + | By default DebugBar route served from the same domain that request served. + | To override default domain, specify it as a non-empty value. + */ + 'route_domain' => null, + + /* + |-------------------------------------------------------------------------- + | DebugBar theme + |-------------------------------------------------------------------------- + | + | Switches between light and dark theme. If set to auto it will respect system preferences + | Possible values: auto, light, dark + */ + 'theme' => env('DEBUGBAR_THEME', 'auto'), + + /* + |-------------------------------------------------------------------------- + | Backtrace stack limit + |-------------------------------------------------------------------------- + | + | By default, the DebugBar limits the number of frames returned by the 'debug_backtrace()' function. + | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. + */ + 'debug_backtrace_limit' => 50, +]; diff --git a/config/sentry.php b/config/sentry.php index 60f866b7b..e8b6ab098 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,8 @@ // 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.346', + '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/version.php b/config/version.php index cbee66d3e..0e83ff40e 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ schemalessAttributes('smtp'); }); - $instance_setting = InstanceSettings::get(); + $instance_setting = instanceSettings(); $instance_setting->smtp = [ 'enabled' => $instance_setting->smtp_enabled, 'from_address' => $instance_setting->smtp_from_address, diff --git a/database/migrations/2024_06_18_105948_move_server_metrics.php b/database/migrations/2024_06_18_105948_move_server_metrics.php index 26a1d1684..a6bccd16a 100644 --- a/database/migrations/2024_06_18_105948_move_server_metrics.php +++ b/database/migrations/2024_06_18_105948_move_server_metrics.php @@ -18,7 +18,7 @@ public function up(): void $table->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_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 @@ 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_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php b/database/migrations/2024_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php new file mode 100644 index 000000000..b3c58afe3 --- /dev/null +++ b/database/migrations/2024_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php @@ -0,0 +1,24 @@ +boolean('delete_unused_volumes')->default(false); + $table->boolean('delete_unused_networks')->default(false); + }); + } + + public function down() + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('delete_unused_volumes'); + $table->dropColumn('delete_unused_networks'); + }); + } +}; diff --git a/database/migrations/2024_09_26_083441_disable_api_by_default.php b/database/migrations/2024_09_26_083441_disable_api_by_default.php new file mode 100644 index 000000000..d0803f751 --- /dev/null +++ b/database/migrations/2024_09_26_083441_disable_api_by_default.php @@ -0,0 +1,18 @@ +boolean('is_api_enabled')->default(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php new file mode 100644 index 000000000..b1f301bd3 --- /dev/null +++ b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php @@ -0,0 +1,28 @@ +boolean('dump_all')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->dropColumn('dump_all'); + }); + } +}; 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..737dfd5ee --- /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(true); + $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/seeders/ApplicationPreviewSeeder.php b/database/seeders/ApplicationPreviewSeeder.php deleted file mode 100644 index 764939073..000000000 --- a/database/seeders/ApplicationPreviewSeeder.php +++ /dev/null @@ -1,20 +0,0 @@ - $application_1->id, - // 'pull_request_id' => 1 - // ]); - } -} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 874762aef..cec05c8fe 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,23 +17,16 @@ public function run(): void ServerSeeder::class, ServerSettingSeeder::class, ProjectSeeder::class, - ProjectSettingSeeder::class, - EnvironmentSeeder::class, StandaloneDockerSeeder::class, - SwarmDockerSeeder::class, - KubernetesSeeder::class, GithubAppSeeder::class, GitlabAppSeeder::class, ApplicationSeeder::class, ApplicationSettingsSeeder::class, - ApplicationPreviewSeeder::class, - EnvironmentVariableSeeder::class, LocalPersistentVolumeSeeder::class, S3StorageSeeder::class, StandalonePostgresqlSeeder::class, - ScheduledDatabaseBackupSeeder::class, - ScheduledDatabaseBackupExecutionSeeder::class, OauthSettingSeeder::class, + SentinelSeeder::class, ]); } } diff --git a/database/seeders/EnvironmentSeeder.php b/database/seeders/EnvironmentSeeder.php deleted file mode 100644 index 1c6d562a9..000000000 --- a/database/seeders/EnvironmentSeeder.php +++ /dev/null @@ -1,13 +0,0 @@ - 'NODE_ENV', - // 'value' => 'production', - // 'is_build_time' => true, - // 'application_id' => 1, - // ]); - } -} diff --git a/database/seeders/GitSeeder.php b/database/seeders/GitSeeder.php deleted file mode 100644 index c8dc3ab6d..000000000 --- a/database/seeders/GitSeeder.php +++ /dev/null @@ -1,26 +0,0 @@ - 'https://api.github.com', - // 'html_url' => 'https://github.com', - // 'is_public' => false, - // 'private_key_id' => $private_key_1->id, - // 'project_id' => $project->id, - // ]); - } -} diff --git a/database/seeders/InstanceSettingsSeeder.php b/database/seeders/InstanceSettingsSeeder.php index c3182a2dd..35fc8506b 100644 --- a/database/seeders/InstanceSettingsSeeder.php +++ b/database/seeders/InstanceSettingsSeeder.php @@ -27,14 +27,14 @@ public function run(): void $ipv4 = Process::run('curl -4s https://ifconfig.io')->output(); $ipv4 = trim($ipv4); $ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv4) && $ipv4) { $settings->update(['public_ipv4' => $ipv4]); } $ipv6 = Process::run('curl -6s https://ifconfig.io')->output(); $ipv6 = trim($ipv6); $ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); - $settings = \App\Models\InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv6) && $ipv6) { $settings->update(['public_ipv6' => $ipv6]); } diff --git a/database/seeders/KubernetesSeeder.php b/database/seeders/KubernetesSeeder.php deleted file mode 100644 index f6a852e05..000000000 --- a/database/seeders/KubernetesSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ -name."\n"; $key->storeInFileSystem(); } }); diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index d0f0f10f4..90b9d46ff 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -101,18 +101,13 @@ public function run(): void } if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { - echo "Checking localhost key.\n"; $coolify_key_name = '@host.docker.internal'; $ssh_keys_directory = Storage::disk('ssh-keys')->files(); $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name)); - $found = PrivateKey::find(0); - if ($found) { - echo 'Private Key found in database.\n'; - if ($coolify_key) { - echo "SSH key found for the Coolify host machine (localhost).\n"; - } - } else { + $server = Server::find(0); + $found = $server->privateKey; + if (! $found) { if ($coolify_key) { $user = str($coolify_key)->before('@')->after('id.'); $coolify_key = Storage::disk('ssh-keys')->get($coolify_key); @@ -125,17 +120,7 @@ public function run(): void ]); $server->update(['user' => $user]); echo "SSH key found for the Coolify host machine (localhost).\n"; - } else { - PrivateKey::create( - [ - 'id' => 0, - 'team_id' => 0, - 'name' => 'localhost\'s key', - 'description' => 'The private key for the Coolify host machine (localhost).', - 'private_key' => 'Paste here you private key!!', - ] - ); echo "No SSH key found for the Coolify host machine (localhost).\n"; echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n"; echo "Your localhost connection won't work until then."; @@ -201,6 +186,7 @@ public function run(): void $this->call(OauthSettingSeeder::class); $this->call(PopulateSshKeysDirectorySeeder::class); + $this->call(SentinelSeeder::class); } } diff --git a/database/seeders/ProjectSettingSeeder.php b/database/seeders/ProjectSettingSeeder.php deleted file mode 100644 index 2a8cdfdb4..000000000 --- a/database/seeders/ProjectSettingSeeder.php +++ /dev/null @@ -1,15 +0,0 @@ -settings->wildcard_domain = 'wildcard.example.com'; - // $first_project->settings->save(); - } -} diff --git a/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php b/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php deleted file mode 100644 index 7e4c33764..000000000 --- a/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php +++ /dev/null @@ -1,28 +0,0 @@ - 'success', - // 'message' => 'Backup created successfully.', - // 'size' => '10243467789556', - // 'scheduled_database_backup_id' => 1, - // ]); - // ScheduledDatabaseBackupExecution::create([ - // 'status' => 'failed', - // 'message' => 'Backup failed.', - // 'size' => '10243456', - // 'scheduled_database_backup_id' => 1, - // ]); - } -} diff --git a/database/seeders/ScheduledDatabaseBackupSeeder.php b/database/seeders/ScheduledDatabaseBackupSeeder.php deleted file mode 100644 index fefbada0d..000000000 --- a/database/seeders/ScheduledDatabaseBackupSeeder.php +++ /dev/null @@ -1,33 +0,0 @@ - true, - // 'frequency' => '* * * * *', - // 'number_of_backups_locally' => 2, - // 'database_id' => 1, - // 'database_type' => 'App\Models\StandalonePostgresql', - // 's3_storage_id' => 1, - // 'team_id' => 0, - // ]); - // ScheduledDatabaseBackup::create([ - // 'enabled' => true, - // 'frequency' => '* * * * *', - // 'number_of_backups_locally' => 3, - // 'database_id' => 1, - // 'database_type' => 'App\Models\StandalonePostgresql', - // 'team_id' => 0, - // ]); - } -} diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php new file mode 100644 index 000000000..3f719cf6e --- /dev/null +++ b/database/seeders/SentinelSeeder.php @@ -0,0 +1,31 @@ +settings->sentinel_token)->isEmpty()) { + $server->generateSentinelToken(); + } + if (str($server->settings->sentinel_custom_url)->isEmpty()) { + $url = $server->generateSentinelUrl(); + logger()->info("Setting sentinel custom url for server {$server->id} to {$url}"); + $server->settings->sentinel_custom_url = $url; + $server->settings->save(); + } + } + }); + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + ray($e->getMessage()); + } + } +} diff --git a/database/seeders/ServiceApplicationSeeder.php b/database/seeders/ServiceApplicationSeeder.php deleted file mode 100644 index 04648f83c..000000000 --- a/database/seeders/ServiceApplicationSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ - 'Swarm Docker 1', - // 'server_id' => 1, - // ]); - } -} diff --git a/database/seeders/WaitlistSeeder.php b/database/seeders/WaitlistSeeder.php deleted file mode 100644 index de6837c60..000000000 --- a/database/seeders/WaitlistSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ - >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & TERMINAL_PID=$! # Start the Soketi process in the background with logging diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js index d6b820583..6633204b2 100755 --- a/docker/coolify-realtime/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -61,9 +61,13 @@ wss.on('connection', (ws) => { const userSession = { ws, userId, ptyProcess: null, isActive: false }; userSessions.set(userId, userSession); - ws.on('message', (message) => handleMessage(userSession, message)); + ws.on('message', (message) => { + handleMessage(userSession, message); + + }); ws.on('error', (err) => handleError(err, userId)); ws.on('close', () => handleClose(userId)); + }); const messageHandlers = { @@ -108,7 +112,6 @@ function parseMessage(message) { async function handleCommand(ws, command, userId) { const userSession = userSessions.get(userId); - if (userSession && userSession.isActive) { const result = await killPtyProcess(userId); if (!result) { @@ -127,27 +130,29 @@ async function handleCommand(ws, command, userId) { cols: 80, rows: 30, cwd: process.env.HOME, + env: {}, }; // NOTE: - Initiates a process within the Terminal container // Establishes an SSH connection to root@coolify with RequestTTY enabled // Executes the 'docker exec' command to connect to a specific container - // If the user types 'exit', it terminates the container connection and reverts to the server. - const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options); + const ptyProcess = pty.spawn('ssh', sshArgs.concat([hereDocContent]), options); + userSession.ptyProcess = ptyProcess; userSession.isActive = true; - ptyProcess.write(hereDocContent + '\n'); - // clear the terminal if the user has clear command - ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n'); ws.send('pty-ready'); - ptyProcess.onData((data) => ws.send(data)); + ptyProcess.onData((data) => { + ws.send(data); + }); // when parent closes ptyProcess.onExit(({ exitCode, signal }) => { console.error(`Process exited with code ${exitCode} and signal ${signal}`); + ws.send('pty-exited'); userSession.isActive = false; + }); if (timeout) { @@ -181,7 +186,7 @@ async function killPtyProcess(userId) { // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 - session.ptyProcess.write('kill -TERM -$$ && exit\n'); + session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n'); setTimeout(() => { if (!session.isActive || !session.ptyProcess) { @@ -230,5 +235,5 @@ function extractHereDocContent(commandString) { } server.listen(6002, () => { - console.log('Server listening on port 6002'); + console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!'); }); 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 45fd72743..fa69c7035 100644 --- a/lang/en.json +++ b/lang/en.json @@ -32,5 +32,6 @@ "resource.non_persistent": "All non-persistent data will be deleted.", "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." + "resource.delete_configurations": "Permanently delete all configuration files from the server.", + "database.delete_backups_locally": "All backups will be permanently deleted from local storage." } diff --git a/lang/fr.json b/lang/fr.json index ae7fa0a03..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é 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 ce0503e1f..0963857c9 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -236,6 +236,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -457,6 +461,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -678,6 +686,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -850,6 +862,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1013,6 +1029,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1067,6 +1087,10 @@ paths: instant_deploy: type: boolean description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1126,9 +1150,33 @@ paths: type: string format: uuid - - name: cleanup + name: delete_configurations in: query - description: 'Delete configurations and volumes.' + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' required: false schema: type: boolean @@ -1351,6 +1399,10 @@ paths: watch_paths: type: string description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' type: object responses: '200': @@ -1738,6 +1790,52 @@ paths: security: - bearerAuth: [] + '/applications/{uuid}/execute': + post: + tags: + - Applications + summary: 'Execute Command' + description: "Execute a command on the application's current container." + operationId: execute-command-application + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Command to execute.' + required: true + content: + application/json: + schema: + properties: + command: + type: string + description: 'Command to execute.' + type: object + responses: + '200': + description: "Execute a command on the application's current container." + content: + application/json: + schema: + properties: + message: { type: string, example: 'Command executed.' } + response: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] /databases: get: tags: @@ -1809,9 +1907,33 @@ paths: type: string format: uuid - - name: cleanup + name: delete_configurations in: query - description: 'Delete configurations and volumes.' + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' required: false schema: type: boolean @@ -3812,6 +3934,38 @@ paths: required: true schema: type: string + - + name: delete_configurations + in: query + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' + required: false + schema: + type: boolean + default: true responses: '200': description: 'Delete a service by UUID' @@ -4769,6 +4923,10 @@ components: type: boolean swarm_cluster: type: string + delete_unused_volumes: + type: boolean + delete_unused_networks: + type: boolean type: object ServerSetting: description: 'Server Settings model' @@ -4801,7 +4959,7 @@ components: type: boolean is_reachable: type: boolean - is_server_api_enabled: + is_sentinel_enabled: type: boolean is_swarm_manager: type: boolean @@ -4823,11 +4981,11 @@ components: type: string logdrain_newrelic_license_key: type: string - metrics_history_days: + sentinel_metrics_refresh_rate_seconds: type: integer - metrics_refresh_rate_seconds: + sentinel_metrics_history_days: type: integer - metrics_token: + sentinel_token: type: string docker_cleanup_frequency: type: string diff --git a/other/nightly/.env.development.example b/other/nightly/.env.development.example deleted file mode 100644 index 4de434df2..000000000 --- a/other/nightly/.env.development.example +++ /dev/null @@ -1,34 +0,0 @@ -# Coolify Configuration -APP_ENV=local -APP_NAME="Coolify Development" -APP_ID=development -APP_KEY= -APP_URL=http://localhost -APP_PORT=8000 -APP_DEBUG=true -SSH_MUX_ENABLED=false - -# PostgreSQL Database Configuration -DB_DATABASE=coolify -DB_USERNAME=coolify -DB_PASSWORD=password -DB_HOST=host.docker.internal -DB_PORT=5432 - -# Ray Configuration -# Set to true to enable Ray -RAY_ENABLED=false -# Set custom ray port -# RAY_PORT= - -# Enable Laravel Telescope for debugging -TELESCOPE_ENABLED=false - -# Selenium Driver URL for Dusk -DUSK_DRIVER_URL=http://selenium:4444 - -# Special Keys for Andras -# For cache purging -BUNNY_API_KEY= -# For asset uploads -BUNNY_STORAGE_API_KEY= diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 3eb270a2a..b15a109c3 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -46,6 +46,9 @@ services: - PUSHER_APP_ID - PUSHER_APP_KEY - PUSHER_APP_SECRET + - TERMINAL_PROTOCOL + - TERMINAL_HOST + - TERMINAL_PORT - AUTOUPDATE - SELF_HOSTED - SSH_MUX_ENABLED @@ -110,7 +113,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/install.sh b/other/nightly/install.sh index feb97295a..04faf50ea 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -404,10 +404,10 @@ if [ ! -f ~/.ssh/authorized_keys ]; then fi set +e -IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) +IF_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) set -e -if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then +if [ "$IF_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then echo " - Generating SSH key." ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 7d9880107..c04a3dee6 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.346" + "version": "4.0.0-beta.354" }, "nightly": { - "version": "4.0.0-beta.347" + "version": "4.0.0-beta.355" }, "helper": { - "version": "1.0.1" + "version": "1.0.2" }, "realtime": { - "version": "1.0.1" + "version": "1.0.3" } } } diff --git a/package-lock.json b/package-lock.json index 8f6fbde08..adb1dc65a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", @@ -960,10 +960,9 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.0.tgz", + "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==", "engines": { "node": ">= 0.6" } diff --git a/package.json b/package.json index 3633a7ed1..29f8f1a37 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "dotenv": "^16.4.5", "ioredis": "5.4.1", "node-pty": "^1.0.0", 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/anythingllm.svg b/public/svgs/anythingllm.svg new file mode 100644 index 000000000..1c25f8711 --- /dev/null +++ b/public/svgs/anythingllm.svg @@ -0,0 +1,166 @@ + + + + + + + diff --git a/public/svgs/argilla.png b/public/svgs/argilla.png new file mode 100644 index 000000000..3ead32785 Binary files /dev/null and b/public/svgs/argilla.png differ 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/bitcoin.svg b/public/svgs/bitcoin.svg new file mode 100644 index 000000000..2b75c99bc --- /dev/null +++ b/public/svgs/bitcoin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + 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/clickhouse.svg b/public/svgs/clickhouse.svg new file mode 100644 index 000000000..d536536de --- /dev/null +++ b/public/svgs/clickhouse.svg @@ -0,0 +1 @@ + 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/coolify.png b/public/svgs/coolify.png new file mode 100644 index 000000000..fa01fec05 Binary files /dev/null and b/public/svgs/coolify.png differ 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/dozzle.svg b/public/svgs/dozzle.svg new file mode 100644 index 000000000..bf88e8729 --- /dev/null +++ b/public/svgs/dozzle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/svgs/easyappointments.png b/public/svgs/easyappointments.png new file mode 100644 index 000000000..8f00d3353 Binary files /dev/null and b/public/svgs/easyappointments.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/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/getoutline.jpeg b/public/svgs/getoutline.jpeg new file mode 100644 index 000000000..422e402f7 Binary files /dev/null and b/public/svgs/getoutline.jpeg 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/homarr.svg b/public/svgs/homarr.svg new file mode 100644 index 000000000..4d4289604 --- /dev/null +++ b/public/svgs/homarr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/homebox.svg b/public/svgs/homebox.svg new file mode 100644 index 000000000..08670bbb9 --- /dev/null +++ b/public/svgs/homebox.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + 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/infisical.png b/public/svgs/infisical.png new file mode 100644 index 000000000..48eddae78 Binary files /dev/null and b/public/svgs/infisical.png differ diff --git a/public/svgs/it-tools.svg b/public/svgs/it-tools.svg new file mode 100644 index 000000000..3de5ecff7 --- /dev/null +++ b/public/svgs/it-tools.svg @@ -0,0 +1,6 @@ + + + + + + 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/labelstudio.png b/public/svgs/labelstudio.png new file mode 100644 index 000000000..afa5160b9 Binary files /dev/null and b/public/svgs/labelstudio.png differ diff --git a/public/svgs/langfuse.png b/public/svgs/langfuse.png new file mode 100644 index 000000000..8dec0fe4a Binary files /dev/null and b/public/svgs/langfuse.png differ diff --git a/public/svgs/libreoffice.svg b/public/svgs/libreoffice.svg new file mode 100644 index 000000000..227471bef --- /dev/null +++ b/public/svgs/libreoffice.svg @@ -0,0 +1 @@ + \ 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/litellm.svg b/public/svgs/litellm.svg new file mode 100644 index 000000000..01830c3f6 --- /dev/null +++ b/public/svgs/litellm.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/mailpit.svg b/public/svgs/mailpit.svg new file mode 100644 index 000000000..e569e71cc --- /dev/null +++ b/public/svgs/mailpit.svg @@ -0,0 +1,5 @@ + + + + + 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/mixpost.svg b/public/svgs/mixpost.svg new file mode 100644 index 000000000..bd915e77a --- /dev/null +++ b/public/svgs/mixpost.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/nitropage.svg b/public/svgs/nitropage.svg new file mode 100644 index 000000000..67b2df17f --- /dev/null +++ b/public/svgs/nitropage.svg @@ -0,0 +1,8 @@ + + + NP + + + + + 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/ollama.svg b/public/svgs/ollama.svg new file mode 100644 index 000000000..3df9a9fba --- /dev/null +++ b/public/svgs/ollama.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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/organizr.png b/public/svgs/organizr.png new file mode 100644 index 000000000..92541ea72 Binary files /dev/null and b/public/svgs/organizr.png differ 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/postgresql.svg b/public/svgs/postgresql.svg new file mode 100644 index 000000000..1ff223856 --- /dev/null +++ b/public/svgs/postgresql.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/prefect.png b/public/svgs/prefect.png new file mode 100644 index 000000000..2f87ec0d7 Binary files /dev/null and b/public/svgs/prefect.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/qdrant.png b/public/svgs/qdrant.png new file mode 100644 index 000000000..ecb2a56d5 Binary files /dev/null and b/public/svgs/qdrant.png differ diff --git a/public/svgs/searxng.svg b/public/svgs/searxng.svg new file mode 100644 index 000000000..b94fe3728 --- /dev/null +++ b/public/svgs/searxng.svg @@ -0,0 +1,56 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/public/svgs/soketi.jpeg b/public/svgs/soketi.jpeg new file mode 100644 index 000000000..00c8d82cd Binary files /dev/null and b/public/svgs/soketi.jpeg differ diff --git a/public/svgs/strapi.svg b/public/svgs/strapi.svg new file mode 100644 index 000000000..d1a71abc2 --- /dev/null +++ b/public/svgs/strapi.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/svgs/supertokens.svg b/public/svgs/supertokens.svg new file mode 100644 index 000000000..30b435bd0 --- /dev/null +++ b/public/svgs/supertokens.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/svgs/tolgee.svg b/public/svgs/tolgee.svg new file mode 100644 index 000000000..0f216e0c6 --- /dev/null +++ b/public/svgs/tolgee.svg @@ -0,0 +1,5 @@ + + + + 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/unstructured.png b/public/svgs/unstructured.png new file mode 100644 index 000000000..a6ec855b6 Binary files /dev/null and b/public/svgs/unstructured.png differ 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/weaviate.png b/public/svgs/weaviate.png new file mode 100644 index 000000000..134294253 Binary files /dev/null and b/public/svgs/weaviate.png differ 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 21854ed63..59c9a79a8 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -16,6 +16,7 @@ export function initializeTerminalComponent() { paused: false, MAX_PENDING_WRITES: 5, keepAliveInterval: null, + reconnectInterval: null, init() { this.setupTerminal(); @@ -48,6 +49,9 @@ export function initializeTerminalComponent() { document.addEventListener(event, () => { this.checkIfProcessIsRunningAndKillIt(); clearInterval(this.keepAliveInterval); + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } }, { once: true }); }); @@ -103,11 +107,27 @@ export function initializeTerminalComponent() { }; this.socket.onclose = () => { console.log('WebSocket connection closed'); - + this.reconnect(); }; } }, + reconnect() { + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } + this.reconnectInterval = setInterval(() => { + console.log('Attempting to reconnect...'); + this.initializeWebSocket(); + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + console.log('Reconnected successfully'); + clearInterval(this.reconnectInterval); + this.reconnectInterval = null; + window.location.reload(); + } + }, 2000); + }, + handleSocketMessage(event) { this.message = '(connection closed)'; if (event.data === 'pty-ready') { @@ -125,6 +145,10 @@ export function initializeTerminalComponent() { if (this.term) this.term.reset(); this.terminalActive = false; this.message = '(sorry, something went wrong, please try again)'; + } else if (event.data === 'pty-exited') { + this.terminalActive = false; + this.term.reset(); + this.commandBuffer = ''; } else { this.pendingWrites++; this.term.write(event.data, this.flowControlCallback.bind(this)); @@ -136,9 +160,12 @@ export function initializeTerminalComponent() { if (this.pendingWrites > this.MAX_PENDING_WRITES && !this.paused) { this.paused = true; this.socket.send(JSON.stringify({ pause: true })); - } else if (this.pendingWrites <= this.MAX_PENDING_WRITES && this.paused) { + return; + } + if (this.pendingWrites <= this.MAX_PENDING_WRITES && this.paused) { this.paused = false; this.socket.send(JSON.stringify({ resume: true })); + return; } }, @@ -147,15 +174,7 @@ export function initializeTerminalComponent() { this.term.onData((data) => { this.socket.send(JSON.stringify({ message: data })); - // Handle CTRL + D or exit command - if (data === '\x04' || (data === '\r' && this.stripAnsiCommands(this.commandBuffer).trim().includes('exit'))) { - this.checkIfProcessIsRunningAndKillIt(); - setTimeout(() => { - this.terminalActive = false; - this.term.reset(); - }, 500); - this.commandBuffer = ''; - } else if (data === '\r') { + if (data === '\r') { this.commandBuffer = ''; } else { this.commandBuffer += data; @@ -165,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; } @@ -183,10 +198,6 @@ export function initializeTerminalComponent() { }); }, - stripAnsiCommands(input) { - return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); - }, - keepAlive() { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ ping: true })); diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php index 8d491a218..287f2f170 100644 --- a/resources/views/auth/confirm-password.blade.php +++ b/resources/views/auth/confirm-password.blade.php @@ -8,7 +8,7 @@
@csrf - + {{ __('auth.confirm_password') }} @if ($errors->any()) diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index 54d57a302..f66b460be 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -12,7 +12,7 @@ class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base "> @if (is_transactional_emails_active())
@csrf - + {{ __('auth.forgot_password_send_email') }} @else diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 1af8a0cd1..7d615885f 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -10,7 +10,7 @@ @csrf @env('local') + required label="{{ __('input.email') }}" /> @@ -20,7 +20,7 @@ @else + label="{{ __('input.email') }}" /> diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index 2e1a63d84..cc13989b8 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -16,7 +16,7 @@ label="{{ __('input.email') }}" />
+ label="{{ __('input.password') }}" />
diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index 6761992a9..9288ff16a 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -9,7 +9,7 @@
@csrf
- +
Enter diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 439fc4ad2..fed6ad77f 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -14,7 +14,10 @@ 'w-full' => $fullWidth, ])> @if (!$hideLabel) -
@endif - merge(['class' => $defaultClass]) }} @required($required) + merge(['class' => $defaultClass]) }} @required($required) @if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" @@ -35,7 +36,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer hover:da
@else - merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) @if ($id !== 'null') wire:model={{ $id }} @endif wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index dcb760ef9..ef6c477f2 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -151,9 +151,9 @@ class="relative w-auto h-auto"> @endif @endif